「アプリ設定/投稿/添付メディア(Pixelfed)の最大バイト数」を追加。インスタンス情報キャッシュのリファクタ。

This commit is contained in:
tateisu 2019-10-01 05:31:55 +09:00
parent 0313b5d264
commit de8a4b7575
13 changed files with 260 additions and 170 deletions

View File

@ -15,6 +15,7 @@
<w>basi</w>
<w>blockee</w>
<w>blurhash</w>
<w>bpixelfed</w>
<w>buggie</w>
<w>bumptech</w>
<w>codepoint</w>

View File

@ -239,6 +239,7 @@ class ActAppSettingChild : AppCompatActivity()
private var etCardDescriptionLength : EditText? = null
private var etMediaSizeMax : EditText? = null
private var etMovieSizeMax : EditText? = null
private var etMediaSizeMaxPixelfed : EditText? = null
private var etRoundRatio : EditText? = null
private var etBoostAlpha : EditText? = null
private var etMediaReadTimeout : EditText? = null
@ -565,6 +566,9 @@ class ActAppSettingChild : AppCompatActivity()
etMovieSizeMax = findViewById(R.id.etMovieSizeMax)
etMovieSizeMax?.addTextChangedListener(this)
etMediaSizeMaxPixelfed= findViewById(R.id.etMediaSizeMaxPixelfed)
etMediaSizeMaxPixelfed?.addTextChangedListener(this)
etRoundRatio = findViewById(R.id.etRoundRatio)
etRoundRatio?.addTextChangedListener(this)
@ -732,6 +736,7 @@ class ActAppSettingChild : AppCompatActivity()
etMediaSizeMax?.setText(Pref.spMediaSizeMax(pref))
etMovieSizeMax?.setText(Pref.spMovieSizeMax(pref))
etMediaSizeMaxPixelfed?.setText(Pref.spMediaSizeMaxPixelfed(pref))
etRoundRatio?.setText(Pref.spRoundRatio(pref))
etBoostAlpha?.setText(Pref.spBoostAlpha(pref))
@ -828,6 +833,7 @@ class ActAppSettingChild : AppCompatActivity()
putText(Pref.spPullNotificationCheckInterval, etPullNotificationCheckInterval)
putText(Pref.spMediaSizeMax, etMediaSizeMax)
putText(Pref.spMovieSizeMax, etMovieSizeMax)
putText(Pref.spMediaSizeMaxPixelfed, etMediaSizeMaxPixelfed)
putText(Pref.spRoundRatio, etRoundRatio)
putText(Pref.spBoostAlpha, etBoostAlpha)
putText(Pref.spMediaReadTimeout, etMediaReadTimeout)

View File

@ -972,9 +972,7 @@ class ActPost : AppCompatActivity(),
}
}
visibility = visibility
?: account?.visibility
?: TootVisibility.Public
visibility = visibility ?: account?.visibility ?: TootVisibility.Public
// 2017/9/13 VISIBILITY_WEB_SETTING から VISIBILITY_PUBLICに変更した
// VISIBILITY_WEB_SETTING だと 1.5未満のタンスでトラブルになるので…
@ -1271,42 +1269,30 @@ class ActPost : AppCompatActivity(),
else -> {
// インスタンス情報を確認する
val info = account.instance
if(info == null || System.currentTimeMillis() - info.time_parse >= 300000L) {
val info = TootInstance.getCached(account.host)
if(info == null || info.isExpire ) {
// 情報がないか古いなら再取得
// 同時に実行するタスクは1つまで
var lastTask = lastInstanceTask
if(lastTask?.isActive != true) {
lastTask = TootTaskRunner(this, TootTaskRunner.PROGRESS_NONE)
lastInstanceTask = lastTask
lastTask.run(account, object : TootTask {
var newInfo : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val result = if(account.isMisskey) {
client.request(
"/api/meta",
account.putMisskeyApiToken().toPostRequestBuilder()
)
} else {
client.request("/api/v1/instance")
if(lastInstanceTask?.isActive != true) {
lastInstanceTask = TootTaskRunner(this, TootTaskRunner.PROGRESS_NONE)
.run(account, object : TootTask {
var newInfo : TootInstance? = null
override fun background(client : TootApiClient) : TootApiResult? {
val (result, ti) = TootInstance.get(client, account)
newInfo = ti
return result
}
newInfo =
TootParser(this@ActPost, account).instance(result?.jsonObject)
return result
}
override fun handleResult(result : TootApiResult?) {
if(isFinishing || isDestroyed) return
if(newInfo != null) {
account.instance = newInfo
updateTextCount()
override fun handleResult(result : TootApiResult?) {
if(isFinishing || isDestroyed) return
if(newInfo != null) updateTextCount()
}
}
})
})
// fall thru
}
// fall thru
}
val max = info?.max_toot_chars
@ -2094,9 +2080,17 @@ class ActPost : AppCompatActivity(),
client.account = account
val (tiResult, ti) = TootInstance.get(client, account)
if(ti == null) return tiResult
val opener = createOpener(uri, mimeType)
val media_size_max = when {
ti.instanceType == TootInstance.InstanceType.Pixelfed -> {
1000000 * max(1, Pref.spMediaSizeMaxPixelfed.toInt(pref))
}
mimeType.startsWith("video") || mimeType.startsWith("audio") -> {
1000000 * max(1, Pref.spMovieSizeMax.toInt(pref))
}

View File

@ -122,7 +122,6 @@ class Column(
internal const val PATH_STATUSES = "/api/v1/statuses/%s" // 1:status_id
internal const val PATH_STATUSES_CONTEXT = "/api/v1/statuses/%s/context" // 1:status_id
// search args 1: query(urlencoded) , also, append "&resolve=1" if resolve non-local accounts
// internal const val PATH_INSTANCE = "/api/v1/instance"
internal const val PATH_LIST_INFO = "/api/v1/lists/%s"
const val PATH_FILTERS = "/api/v1/filters"

View File

@ -18,8 +18,6 @@ class ColumnTask_Loading(
internal val log = LogCategory("CT_Loading")
}
internal var instance_tmp : TootInstance? = null
internal var list_pinned : ArrayList<TimelineItem>? = null
override fun doInBackground(vararg unused : Void) : TootApiResult? {
@ -88,8 +86,8 @@ class ColumnTask_Loading(
val list_new = column.duplicate_map.filterDuplicate(list_pinned)
column.list_data.addAll(list_new)
}
val list_new = when(column.type){
val list_new = when(column.type) {
// 検索カラムはIDによる重複排除が不可能
ColumnType.SEARCH -> list_tmp
@ -97,7 +95,7 @@ class ColumnTask_Loading(
// 他のカラムは重複排除してから追加
else -> column.duplicate_map.filterDuplicate(list_tmp)
}
column.list_data.addAll(list_new)
}
@ -114,23 +112,7 @@ class ColumnTask_Loading(
/////////////////////////////////////////////////////////////////
// functions that called from ColumnTask.loading lambda.
internal fun getInstanceInformation(
client : TootApiClient,
instance_name : String?
) : TootApiResult? {
when {
// 「インスタンス情報」カラムをNAアカウントで開く場合
instance_name != null -> client.instance = instance_name
// カラムに紐付けられたアカウントのタンスのインスタンス情報を取得する
else -> {
}
}
val (result, ti) = client.parseInstanceInformation(client.getInstanceInformation())
instance_tmp = ti
return result
}
internal fun getStatusesPinned(client : TootApiClient, path_base : String) {
val result = client.request(path_base)
val jsonArray = result?.jsonArray
@ -700,14 +682,8 @@ class ColumnTask_Loading(
) : TootApiResult? {
// (Mastodonのみ対応)
var instance = access_info.instance
if(instance == null) {
getInstanceInformation(client, null)
if(instance_tmp != null) {
instance = instance_tmp
access_info.instance = instance
}
}
val (instanceResult, instance) = TootInstance.get(client, access_info)
if(instance == null) return instanceResult
// ステータスIDに該当するトゥート
// タンスをまたいだりすると存在しないかもしれないが、エラーは出さない
@ -722,7 +698,7 @@ class ColumnTask_Loading(
column.idRecent = null
var bInstanceTooOld = false
if(instance?.versionGE(TootInstance.VERSION_2_6_0) == true) {
if(instance.versionGE(TootInstance.VERSION_2_6_0)) {
// 指定より新しいトゥート
result = getStatusList(client, url, aroundMin = true)
if(result == null || result.error != null) return result
@ -750,14 +726,8 @@ class ColumnTask_Loading(
internal fun getAccountAroundStatuses(client : TootApiClient) : TootApiResult? {
// (Mastodonのみ対応)
var instance = access_info.instance
if(instance == null) {
getInstanceInformation(client, null)
if(instance_tmp != null) {
instance = instance_tmp
access_info.instance = instance
}
}
val (instanceResult, instance) = TootInstance.get(client, access_info)
if(instance == null) return instanceResult
// ステータスIDに該当するトゥート
// タンスをまたいだりすると存在しないかもしれない
@ -774,7 +744,7 @@ class ColumnTask_Loading(
column.idRecent = null
var bInstanceTooOld = false
if(instance?.versionGE(TootInstance.VERSION_2_6_0) == true) {
if(instance.versionGE(TootInstance.VERSION_2_6_0)) {
// 指定より新しいトゥート
result = getStatusList(client, path, aroundMin = true)
if(result == null || result.error != null) return result
@ -1007,31 +977,25 @@ class ColumnTask_Loading(
return TootApiResult(context.getString(R.string.search_is_not_available_on_pseudo_account))
}
var instance = access_info.instance
if(instance == null) {
getInstanceInformation(client, null)
if(instance_tmp != null) {
instance = instance_tmp
access_info.instance = instance
}
}
val(instanceResult,instance) = TootInstance.get(client,access_info)
if( instance==null) return instanceResult
var query="q=${column.search_query.encodePercent()}"
var query = "q=${column.search_query.encodePercent()}"
if(column.search_resolve) query += "&resolve=1"
val(apiResult,searchResult)= client.requestMastodonSearch(parser,query)
if( searchResult != null){
val (apiResult, searchResult) = client.requestMastodonSearch(parser, query)
if(searchResult != null) {
list_tmp = java.util.ArrayList()
addAll(list_tmp, searchResult.hashtags)
if(searchResult.searchApiVersion>=2 && searchResult.hashtags.isNotEmpty()) {
if(searchResult.searchApiVersion >= 2 && searchResult.hashtags.isNotEmpty()) {
addOne(list_tmp, TootSearchGap(TootSearchGap.SearchType.Hashtag))
}
addAll(list_tmp, searchResult.accounts)
if(searchResult.searchApiVersion>=2 && searchResult.accounts.isNotEmpty()) {
if(searchResult.searchApiVersion >= 2 && searchResult.accounts.isNotEmpty()) {
addOne(list_tmp, TootSearchGap(TootSearchGap.SearchType.Account))
}
addAll(list_tmp, searchResult.statuses)
if( searchResult.searchApiVersion>=2 && searchResult.statuses.isNotEmpty()) {
if(searchResult.searchApiVersion >= 2 && searchResult.statuses.isNotEmpty()) {
addOne(list_tmp, TootSearchGap(TootSearchGap.SearchType.Status))
}
}

View File

@ -57,28 +57,21 @@ enum class ColumnType(
ProfileStatusMastodon(
loading = { client ->
var instance = access_info.instance
// まだ取得してない
// 疑似アカウントの場合は過去のデータが別タンスかもしれない?
if(instance == null || access_info.isPseudo) {
getInstanceInformation(client, null)
if(instance_tmp != null) {
instance = instance_tmp
access_info.instance = instance
val(instanceResult,instance) = TootInstance.get(client,access_info)
if(instance==null){
instanceResult
}else {
val path = column.makeProfileStatusesUrl(column.profile_id)
if(instance.versionGE(TootInstance.VERSION_1_6)
// 将来的に正しく判定できる見込みがないので、Pleroma条件でのフィルタは行わない
// && instance.instanceType != TootInstance.InstanceType.Pleroma
) {
getStatusesPinned(client, "$path&pinned=true")
}
getStatusList(client, path)
}
val path = column.makeProfileStatusesUrl(column.profile_id)
if(instance?.versionGE(TootInstance.VERSION_1_6) == true
// 将来的に正しく判定できる見込みがないので、Pleroma条件でのフィルタは行わない
// && instance.instanceType != TootInstance.InstanceType.Pleroma
) {
getStatusesPinned(client, "$path&pinned=true")
}
getStatusList(client, path)
},
refresh = { client ->
@ -1102,12 +1095,26 @@ enum class ColumnType(
headerType = HeaderType.Instance,
loading = { client ->
val result = getInstanceInformation(client, column.instance_uri)
if(instance_tmp != null) {
column.instance_information = instance_tmp
column.handshake = result?.response?.handshake
val(instanceResult,instance) = TootInstance.get(client,access_info,column.instance_uri)
if(instance!=null) {
column.instance_information = instance
column.handshake = instanceResult?.response?.handshake
}
result
instanceResult
//
// // 「インスタンス情報」カラムをNAアカウントで開く場合
// instance_name != null -> client.instance = instance_name
//
// val (result, ti) = client.parseInstanceInformation(client.getInstanceInformation())
// instance_tmp = ti
// return result
// }
//
// val result = getInstanceInformation(client, column.instance_uri)
// if(instance_tmp != null) {
//
// }
// result
}
),

View File

@ -519,6 +519,7 @@ object Pref {
val spStripIconSize = StringPref("StripIconSize", "30")
val spMediaSizeMax = StringPref("max_media_size", "8")
val spMovieSizeMax = StringPref("max_movie_size", "40")
val spMediaSizeMaxPixelfed = StringPref("MediaSizeMaxPixelfed", "15")
val spTimelineFont = StringPref("timeline_font", "", skipImport = true)
val spTimelineFontBold = StringPref("timeline_font_bold", "", skipImport = true)
val spMspUserToken = StringPref("mastodon_search_portal_user_token", "")

View File

@ -1,34 +1,97 @@
package jp.juggler.subwaytooter.api.entity
import android.os.SystemClock
import jp.juggler.subwaytooter.api.TootApiClient
import jp.juggler.subwaytooter.api.TootApiResult
import jp.juggler.subwaytooter.api.TootParser
import jp.juggler.subwaytooter.table.SavedAccount
import jp.juggler.subwaytooter.util.*
import jp.juggler.util.parseInt
import jp.juggler.util.parseLong
import jp.juggler.util.parseString
import jp.juggler.util.toStringArrayList
import jp.juggler.util.*
import org.json.JSONObject
import java.util.*
import java.util.regex.Pattern
import kotlin.collections.ArrayList
import kotlin.collections.HashMap
class TootInstance(parser : TootParser, src : JSONObject) {
companion object {
private val rePleroma = Pattern.compile("\\bpleroma\\b", Pattern.CASE_INSENSITIVE)
private val rePixelfed = Pattern.compile("\\bpixelfed\\b", Pattern.CASE_INSENSITIVE)
val VERSION_1_6 = VersionString("1.6")
val VERSION_2_4_0_rc1 = VersionString("2.4.0rc1")
val VERSION_2_4_0_rc2 = VersionString("2.4.0rc2")
// val VERSION_2_4_0 = VersionString("2.4.0")
// val VERSION_2_4_1_rc1 = VersionString("2.4.1rc1")
// val VERSION_2_4_0 = VersionString("2.4.0")
// val VERSION_2_4_1_rc1 = VersionString("2.4.1rc1")
val VERSION_2_4_1 = VersionString("2.4.1")
val VERSION_2_6_0 = VersionString("2.6.0")
val VERSION_2_7_0_rc1 = VersionString("2.7.0rc1")
val VERSION_3_0_0_rc1 = VersionString("3.0.0rc1")
val MISSKEY_VERSION_11 = VersionString("11.0")
private const val EXPIRE = 600000L
private val cache = HashMap<String, TootInstance>()
// get from cache
// no request, no expiration check
fun getCached(host : String) : TootInstance? {
synchronized(cache) {
return cache[host.toLowerCase(Locale.JAPAN)]
}
}
fun get(
client : TootApiClient,
account : SavedAccount,
host : String? = null
) : Pair<TootApiResult?, TootInstance?> {
val tmpInstance = client.instance
val tmpAccount = client.account
try {
synchronized(cache) {
// re-use cached item.
val now = SystemClock.elapsedRealtime()
val item = cache[account.host.toLowerCase(Locale.JAPAN)]
if(item != null && now - item.time_parse <= EXPIRE)
return Pair(TootApiResult(), item)
// get new information
client.account = account
if(host != null) client.instance = host
val result = if(account.isMisskey) {
val params = JSONObject().apply {
put("dummy", 1)
}
client.request("/api/meta", params.toPostRequestBuilder())
} else {
client.request("/api/v1/instance")
}
val data = parseItem(
::TootInstance,
TootParser(client.context, account),
result?.jsonObject
)
if(data != null) {
cache[account.host.toLowerCase(Locale.JAPAN)] = data
}
return Pair(result, data)
}
} finally {
client.account = tmpAccount
client.instance = tmpInstance // must be last.
}
}
}
// いつ取得したか(内部利用)
var time_parse : Long = System.currentTimeMillis()
private var time_parse : Long = SystemClock.elapsedRealtime()
val isExpire : Boolean
get() = SystemClock.elapsedRealtime() - time_parse >= EXPIRE
// URI of the current instance
val uri : String?
@ -64,9 +127,11 @@ class TootInstance(parser : TootParser, src : JSONObject) {
// インスタンスの種別
enum class InstanceType {
Mastodon,
Pleroma,
Misskey
Misskey,
Pixelfed,
Pleroma
}
val instanceType : InstanceType
@ -74,15 +139,15 @@ class TootInstance(parser : TootParser, src : JSONObject) {
// XXX: urls をパースしてない。使ってないから…
init {
if(parser.serviceType == ServiceType.MISSKEY){
if(parser.serviceType == ServiceType.MISSKEY) {
this.uri = parser.accessHost
this.title = parser.accessHost
this.description = "(Misskey instance)"
val sv = src.optJSONObject("maintainer")?.parseString("url")
this.email = when{
sv?.startsWith("mailto:") ==true-> sv.substring(7)
else-> sv
this.email = when {
sv?.startsWith("mailto:") == true -> sv.substring(7)
else -> sv
}
this.version = src.parseString("version")
@ -94,15 +159,15 @@ class TootInstance(parser : TootParser, src : JSONObject) {
this.languages = src.optJSONArray("langs")?.toStringArrayList() ?: ArrayList()
this.contact_account = null
}else {
} else {
this.uri = src.parseString("uri")
this.title = src.parseString("title")
this.description = src.parseString("description")
val sv = src.parseString("email")
this.email = when{
sv?.startsWith("mailto:") ==true-> sv.substring(7)
else-> sv
this.email = when {
sv?.startsWith("mailto:") == true -> sv.substring(7)
else -> sv
}
this.version = src.parseString("version")
@ -114,6 +179,7 @@ class TootInstance(parser : TootParser, src : JSONObject) {
this.instanceType = when {
rePleroma.matcher(version ?: "").find() -> InstanceType.Pleroma
rePixelfed.matcher(version ?: "").find() -> InstanceType.Pixelfed
else -> InstanceType.Mastodon
}
@ -145,11 +211,71 @@ class TootInstance(parser : TootParser, src : JSONObject) {
val i = VersionString.compare(decoded_version, check)
return i >= 0
}
val misskeyVersion :Int
get()=when{
instanceType != InstanceType.Misskey -> 0
versionGE(MISSKEY_VERSION_11) -> 11
else->10
}
val misskeyVersion : Int
get() = when {
instanceType != InstanceType.Misskey -> 0
versionGE(MISSKEY_VERSION_11) -> 11
else -> 10
}
}
//
//import android.os.SystemClock
//import jp.juggler.subwaytooter.api.TootApiClient
//import jp.juggler.subwaytooter.api.TootApiResult
//import jp.juggler.subwaytooter.api.TootParser
//import jp.juggler.subwaytooter.api.entity.TootInstance
//import jp.juggler.subwaytooter.api.entity.parseItem
//import jp.juggler.subwaytooter.table.SavedAccount
//import jp.juggler.util.toPostRequestBuilder
//import org.json.JSONObject
//import java.util.*
//import kotlin.collections.HashMap
//
//object InstanceInformationCache {
//
//
// // var instance =
// // if(instance == null) {
// // val r2 = getInstanceInformation(client)
// // instance = instance_tmp ?: return r2
// // account.instance = instance
// // }
// // var instance_tmp : TootInstance? = null
// // fun getInstanceInformation(client : TootApiClient) : TootApiResult? {
// //
// // instance_tmp =
// // return result
// // }
// //
// // client.instance = host
// // val result = if(isMisskey) {
// // client.getInstanceInformation()
// // client.request(
// // "/api/meta",
// // account.putMisskeyApiToken().toPostRequestBuilder()
// // )
// // } else {
// // client.request("/api/v1/instance")
// // }
// // newInfo =
// // TootParser(this@ActPost, account).instance(result?.jsonObject)
// // return Pair(null,null)
// // }
//
// //private val refInstance = AtomicReference<TootInstance>(null)
// //
// //// DBには保存しない
// //var instance : TootInstance?
// // get() {
// // val instance = refInstance.get()
// // return when {
// // instance == null -> null
// // System.currentTimeMillis() - instance.time_parse > INSTANCE_INFORMATION_EXPIRE -> null
// // else -> instance
// // }
// // }
// // set(instance) = refInstance.set(instance)
//
//}

View File

@ -66,19 +66,7 @@ class SavedAccount(
var max_toot_chars = 0
private val refInstance = AtomicReference<TootInstance>(null)
// DBには保存しない
var instance : TootInstance?
get() {
val instance = refInstance.get()
return when {
instance == null -> null
System.currentTimeMillis() - instance.time_parse > INSTANCE_INFORMATION_EXPIRE -> null
else -> instance
}
}
set(instance) = refInstance.set(instance)
init {
val pos = acct.indexOf('@')
@ -431,7 +419,6 @@ class SavedAccount(
/////////////////////////////////
// login information
const val INVALID_DB_ID = - 1L
private const val INSTANCE_INFORMATION_EXPIRE = 60000L * 5
// アプリデータのインポート時に呼ばれる
fun onDBDelete(db : SQLiteDatabase) {

View File

@ -250,7 +250,6 @@ class PostHelper(
var status : TootStatus? = null
var instance_tmp : TootInstance? = null
var credential_tmp : TootAccount? = null
@ -258,19 +257,6 @@ class PostHelper(
var scheduledStatusSucceeded = false
fun getInstanceInformation(client : TootApiClient) : TootApiResult? {
val result = if(account.isMisskey) {
val params = JSONObject().apply {
put("dummy", 1)
}
client.request("/api/meta", params.toPostRequestBuilder())
} else {
client.request("/api/v1/instance")
}
instance_tmp = parseItem(::TootInstance, parser, result?.jsonObject)
return result
}
fun getCredential(client : TootApiClient) : TootApiResult? {
val result = client.request("/api/v1/accounts/verify_credentials")
credential_tmp = parser.account(result?.jsonObject)
@ -311,12 +297,8 @@ class PostHelper(
var visibility_checked : TootVisibility? = visibility
var instance = account.instance
if(instance == null) {
val r2 = getInstanceInformation(client)
instance = instance_tmp ?: return r2
account.instance = instance
}
val(ri,instance) = TootInstance.get(client,account)
if(instance==null) return ri
if(visibility == TootVisibility.WebSetting) {
visibility_checked =

View File

@ -79,6 +79,25 @@
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:labelFor="@+id/etMediaSizeMaxPixelfed"
android:text="@string/media_attachment_max_byte_size_pixelfed"
/>
<LinearLayout style="@style/setting_row_form">
<EditText
android:id="@+id/etMediaSizeMaxPixelfed"
style="@style/setting_horizontal_stretch"
android:inputType="number"
android:gravity="center"
/>
</LinearLayout>
<View style="@style/setting_divider"/>
<TextView
style="@style/setting_row_label"
android:text="@string/refresh_after_toot"

View File

@ -414,6 +414,7 @@
<string name="media_attachment_empty">添付メディアがありません</string>
<string name="media_attachment_max_byte_size">添付メディア(静止画)の最大バイト数(単位:MB。デフォルトは8)</string>
<string name="media_attachment_max_byte_size_movie">添付メディア(動画)の最大バイト数(単位:MB。デフォルトは40)</string>
<string name="media_attachment_max_byte_size_pixelfed">添付メディア(Pixelfed)の最大バイト数(単位:MB。デフォルトは15)</string>
<string name="media_attachment_still_uploading">添付メディアのアップロードが終わってません</string>
<string name="media_attachment_type_error">添付メディアの種類\"%1$s\"はこのアプリでは開けません。下の「…」にある「ブラウザで開く」を試すことができます</string>
<string name="media_description">(添付 %1$d) %2$s</string>

View File

@ -572,6 +572,9 @@
<string name="your_lists">Your lists</string>
<string name="media_attachment_max_byte_size">Media attachment (image) bytes size limit (Unit:MB. The default is 8)</string>
<string name="media_attachment_max_byte_size_movie">Media attachment (movie) bytes size limit (Unit:MB. The default is 40)</string>
<string name="media_attachment_max_byte_size_pixelfed">Media attachment (Pixelfed) bytes size limit (Unit:MB. The default is 15)</string>
<string name="tootsearch">tootsearch (JP)</string>
<string name="cant_handle_uri_of">Subway Tooter can\'t handle URI \"%1$s\". Please select other app.</string>
<string name="use_internal_media_viewer">Use built-in media viewer</string>