言語フィルタ画面のリファクタ。 /api/v1/instance/languages で言語リストを読む。
This commit is contained in:
parent
50d0139a08
commit
add0014304
|
@ -169,6 +169,7 @@ dependencies {
|
||||||
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
|
implementation("androidx.appcompat:appcompat:${Vers.androidxAppcompat}")
|
||||||
implementation("androidx.browser:browser:1.8.0")
|
implementation("androidx.browser:browser:1.8.0")
|
||||||
implementation("androidx.compose.material3:material3:1.2.1")
|
implementation("androidx.compose.material3:material3:1.2.1")
|
||||||
|
implementation("androidx.compose.material:material-icons-extended-android:${Vers.androidxComposeMaterialIcons}")
|
||||||
implementation("androidx.compose.runtime:runtime-livedata:${Vers.androidxComposeRuntime}")
|
implementation("androidx.compose.runtime:runtime-livedata:${Vers.androidxComposeRuntime}")
|
||||||
implementation("androidx.compose.ui:ui-tooling-preview:${Vers.androidxComposeUi}")
|
implementation("androidx.compose.ui:ui-tooling-preview:${Vers.androidxComposeUi}")
|
||||||
implementation("androidx.compose.ui:ui:${Vers.androidxComposeUi}")
|
implementation("androidx.compose.ui:ui:${Vers.androidxComposeUi}")
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -298,7 +298,7 @@
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".ActLanguageFilter"
|
android:name=".ui.languageFilter.LanguageFilterActivity"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:label="@string/language_filter"
|
android:label="@string/language_filter"
|
||||||
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
android:windowSoftInputMode="adjustResize|stateAlwaysHidden" />
|
||||||
|
|
|
@ -1,478 +0,0 @@
|
||||||
package jp.juggler.subwaytooter
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Intent
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.os.PersistableBundle
|
|
||||||
import android.os.Process
|
|
||||||
import android.text.Editable
|
|
||||||
import android.text.TextWatcher
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.*
|
|
||||||
import androidx.appcompat.app.AlertDialog
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.content.FileProvider
|
|
||||||
import jp.juggler.subwaytooter.api.entity.TootStatus
|
|
||||||
import jp.juggler.subwaytooter.column.Column
|
|
||||||
import jp.juggler.subwaytooter.databinding.ActLanguageFilterBinding
|
|
||||||
import jp.juggler.subwaytooter.dialog.actionsDialog
|
|
||||||
import jp.juggler.subwaytooter.pref.FILE_PROVIDER_AUTHORITY
|
|
||||||
import jp.juggler.util.*
|
|
||||||
import jp.juggler.util.coroutine.launchAndShowError
|
|
||||||
import jp.juggler.util.coroutine.launchProgress
|
|
||||||
import jp.juggler.util.data.*
|
|
||||||
import jp.juggler.util.log.LogCategory
|
|
||||||
import jp.juggler.util.log.showToast
|
|
||||||
import jp.juggler.util.ui.*
|
|
||||||
import org.jetbrains.anko.textColor
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.util.*
|
|
||||||
|
|
||||||
class ActLanguageFilter : AppCompatActivity(), View.OnClickListener {
|
|
||||||
|
|
||||||
private class MyItem(
|
|
||||||
val code: String,
|
|
||||||
var allow: Boolean,
|
|
||||||
)
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
|
|
||||||
internal val log = LogCategory("ActLanguageFilter")
|
|
||||||
|
|
||||||
internal const val EXTRA_COLUMN_INDEX = "column_index"
|
|
||||||
private const val STATE_LANGUAGE_LIST = "language_list"
|
|
||||||
|
|
||||||
fun createIntent(activity: ActMain, idx: Int) =
|
|
||||||
Intent(activity, ActLanguageFilter::class.java).apply {
|
|
||||||
putExtra(EXTRA_COLUMN_INDEX, idx)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val languageComparator = Comparator<MyItem> { a, b ->
|
|
||||||
when {
|
|
||||||
a.code == TootStatus.LANGUAGE_CODE_DEFAULT -> -1
|
|
||||||
b.code == TootStatus.LANGUAGE_CODE_DEFAULT -> 1
|
|
||||||
a.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> -1
|
|
||||||
b.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> 1
|
|
||||||
else -> a.code.compareTo(b.code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun equalsLanguageList(a: JsonObject?, b: JsonObject?): Boolean {
|
|
||||||
fun JsonObject.encodeToString(): String {
|
|
||||||
val clone = this.toString().decodeJsonObject()
|
|
||||||
if (!clone.contains(TootStatus.LANGUAGE_CODE_DEFAULT)) {
|
|
||||||
clone[TootStatus.LANGUAGE_CODE_DEFAULT] = true
|
|
||||||
}
|
|
||||||
return clone.keys.sorted().joinToString(",") { "$it=${this[it]}" }
|
|
||||||
}
|
|
||||||
|
|
||||||
val a_sign = (a ?: JsonObject()).encodeToString()
|
|
||||||
val b_sign = (b ?: JsonObject()).encodeToString()
|
|
||||||
return a_sign == b_sign
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val languageNameMap by lazy {
|
|
||||||
HashMap<String, String>().apply {
|
|
||||||
|
|
||||||
// from https://github.com/google/cld3/blob/master/src/task_context_params.cc#L43
|
|
||||||
val languageNamesCld3 = arrayOf(
|
|
||||||
"eo", "co", "eu", "ta", "de", "mt", "ps", "te", "su", "uz", "zh-Latn", "ne",
|
|
||||||
"nl", "sw", "sq", "hmn", "ja", "no", "mn", "so", "ko", "kk", "sl", "ig",
|
|
||||||
"mr", "th", "zu", "ml", "hr", "bs", "lo", "sd", "cy", "hy", "uk", "pt",
|
|
||||||
"lv", "iw", "cs", "vi", "jv", "be", "km", "mk", "tr", "fy", "am", "zh",
|
|
||||||
"da", "sv", "fi", "ht", "af", "la", "id", "fil", "sm", "ca", "el", "ka",
|
|
||||||
"sr", "it", "sk", "ru", "ru-Latn", "bg", "ny", "fa", "haw", "gl", "et",
|
|
||||||
"ms", "gd", "bg-Latn", "ha", "is", "ur", "mi", "hi", "bn", "hi-Latn", "fr",
|
|
||||||
"yi", "hu", "xh", "my", "tg", "ro", "ar", "lb", "el-Latn", "st", "ceb",
|
|
||||||
"kn", "az", "si", "ky", "mg", "en", "gu", "es", "pl", "ja-Latn", "ga", "lt",
|
|
||||||
"sn", "yo", "pa", "ku"
|
|
||||||
)
|
|
||||||
|
|
||||||
for (src1 in languageNamesCld3) {
|
|
||||||
val src2 = src1.replace("-Latn", "")
|
|
||||||
val isLatn = src2 != src1
|
|
||||||
val locale = Locale(src2)
|
|
||||||
log.w("languageNameMap $src1 ${locale.language} ${locale.country} ${locale.displayName}")
|
|
||||||
put(
|
|
||||||
src1, if (isLatn) {
|
|
||||||
"${locale.displayName}(Latn)"
|
|
||||||
} else {
|
|
||||||
locale.displayName
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
put(TootStatus.LANGUAGE_CODE_DEFAULT, getString(R.string.language_code_default))
|
|
||||||
put(TootStatus.LANGUAGE_CODE_UNKNOWN, getString(R.string.language_code_unknown))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getDesc(item: MyItem): String {
|
|
||||||
val code = item.code
|
|
||||||
return languageNameMap[code] ?: getString(R.string.custom)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var columnIndex: Int = 0
|
|
||||||
internal lateinit var column: Column
|
|
||||||
internal lateinit var appState: AppState
|
|
||||||
internal var density: Float = 0f
|
|
||||||
|
|
||||||
private val views by lazy {
|
|
||||||
ActLanguageFilterBinding.inflate(layoutInflater)
|
|
||||||
}
|
|
||||||
private lateinit var adapter: MyAdapter
|
|
||||||
private val languageList = ArrayList<MyItem>()
|
|
||||||
private var loadingBusy: Boolean = false
|
|
||||||
|
|
||||||
private val arExport = ActivityResultHandler(log) {
|
|
||||||
}
|
|
||||||
|
|
||||||
private val arImport = ActivityResultHandler(log) { r ->
|
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
|
||||||
r.data?.checkMimeTypeAndGrant(contentResolver)
|
|
||||||
?.firstOrNull()?.uri?.let { import2(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
backPressed { confirmBack() }
|
|
||||||
arExport.register(this)
|
|
||||||
arImport.register(this)
|
|
||||||
|
|
||||||
App1.setActivityTheme(this)
|
|
||||||
initUI()
|
|
||||||
|
|
||||||
appState = App1.getAppState(this)
|
|
||||||
density = appState.density
|
|
||||||
columnIndex = intent.int(EXTRA_COLUMN_INDEX) ?: 0
|
|
||||||
column = appState.column(columnIndex)!!
|
|
||||||
|
|
||||||
if (savedInstanceState != null) {
|
|
||||||
try {
|
|
||||||
val sv = savedInstanceState.getString(STATE_LANGUAGE_LIST, null)
|
|
||||||
if (sv != null) {
|
|
||||||
val list = sv.decodeJsonObject()
|
|
||||||
load(list)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
log.e(ex, "restore failed.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
load(column.languageFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
|
|
||||||
super.onSaveInstanceState(outState, outPersistentState)
|
|
||||||
outState.putString(STATE_LANGUAGE_LIST, encodeLanguageList().toString())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initUI() {
|
|
||||||
setContentView(views.root)
|
|
||||||
setSupportActionBar(views.toolbar)
|
|
||||||
setNavigationBack(views.toolbar)
|
|
||||||
fixHorizontalMargin(views.llContent)
|
|
||||||
|
|
||||||
arrayOf(
|
|
||||||
views.btnAdd,
|
|
||||||
views.btnSave,
|
|
||||||
views.btnMore,
|
|
||||||
).forEach {
|
|
||||||
it.setOnClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
adapter = MyAdapter()
|
|
||||||
views.listView.adapter = adapter
|
|
||||||
views.listView.onItemClickListener = adapter
|
|
||||||
}
|
|
||||||
|
|
||||||
// UIのデータをJsonObjectにエンコード
|
|
||||||
private fun encodeLanguageList() = buildJsonObject {
|
|
||||||
for (item in languageList) {
|
|
||||||
put(item.code, item.allow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun load(src: JsonObject?) {
|
|
||||||
loadingBusy = true
|
|
||||||
try {
|
|
||||||
languageList.clear()
|
|
||||||
|
|
||||||
if (src != null) {
|
|
||||||
for (key in src.keys) {
|
|
||||||
languageList.add(MyItem(key, src.boolean(key) ?: true))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (languageList.none { it.code == TootStatus.LANGUAGE_CODE_DEFAULT }) {
|
|
||||||
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
|
|
||||||
}
|
|
||||||
|
|
||||||
languageList.sortWith(languageComparator)
|
|
||||||
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
} finally {
|
|
||||||
loadingBusy = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save() {
|
|
||||||
column.languageFilter = encodeLanguageList()
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class MyAdapter : BaseAdapter(), AdapterView.OnItemClickListener {
|
|
||||||
|
|
||||||
override fun getCount(): Int = languageList.size
|
|
||||||
override fun getItemId(idx: Int): Long = 0L
|
|
||||||
override fun getItem(idx: Int): Any = languageList[idx]
|
|
||||||
|
|
||||||
override fun getView(idx: Int, viewArg: View?, parent: ViewGroup?): View {
|
|
||||||
val tv = (viewArg ?: layoutInflater.inflate(
|
|
||||||
R.layout.lv_language_filter,
|
|
||||||
parent,
|
|
||||||
false
|
|
||||||
)) as TextView
|
|
||||||
val item = languageList[idx]
|
|
||||||
tv.text =
|
|
||||||
"${item.code} ${getDesc(item)} : ${getString(if (item.allow) R.string.language_show else R.string.language_hide)}"
|
|
||||||
tv.textColor = attrColor(
|
|
||||||
when (item.allow) {
|
|
||||||
true -> R.attr.colorTextContent
|
|
||||||
false -> R.attr.colorRegexFilterError
|
|
||||||
}
|
|
||||||
)
|
|
||||||
return tv
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemClick(parent: AdapterView<*>?, viewArg: View?, idx: Int, id: Long) {
|
|
||||||
if (idx in languageList.indices) edit(languageList[idx])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onClick(v: View) {
|
|
||||||
when (v.id) {
|
|
||||||
R.id.btnSave -> {
|
|
||||||
save()
|
|
||||||
val data = Intent()
|
|
||||||
data.putExtra(EXTRA_COLUMN_INDEX, columnIndex)
|
|
||||||
setResult(RESULT_OK, data)
|
|
||||||
finish()
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.btnAdd -> edit(null)
|
|
||||||
|
|
||||||
R.id.btnMore -> {
|
|
||||||
launchAndShowError {
|
|
||||||
actionsDialog {
|
|
||||||
action(getString(R.string.clear_all)) {
|
|
||||||
languageList.clear()
|
|
||||||
languageList.add(MyItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
action(getString(R.string.export)) { export() }
|
|
||||||
action(getString(R.string.import_)) { import() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun edit(myItem: MyItem?) =
|
|
||||||
DlgLanguageFilter.open(this, myItem, object : DlgLanguageFilter.Callback {
|
|
||||||
override fun onOK(code: String, allow: Boolean) {
|
|
||||||
val it = languageList.iterator()
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val item = it.next()
|
|
||||||
if (item.code == code) {
|
|
||||||
item.allow = allow
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
languageList.add(MyItem(code, allow))
|
|
||||||
languageList.sortWith(languageComparator)
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDelete(code: String) {
|
|
||||||
val it = languageList.iterator()
|
|
||||||
while (it.hasNext()) {
|
|
||||||
val item = it.next()
|
|
||||||
if (item.code == code) it.remove()
|
|
||||||
}
|
|
||||||
adapter.notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
private object DlgLanguageFilter {
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
|
|
||||||
fun onOK(code: String, allow: Boolean)
|
|
||||||
fun onDelete(code: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
fun open(activity: ActLanguageFilter, item: MyItem?, callback: Callback) {
|
|
||||||
|
|
||||||
val view = activity.layoutInflater.inflate(R.layout.dlg_language_filter, null, false)
|
|
||||||
|
|
||||||
val etLanguage: EditText = view.findViewById(R.id.etLanguage)
|
|
||||||
val btnPresets: ImageButton = view.findViewById(R.id.btnPresets)
|
|
||||||
val tvLanguage: TextView = view.findViewById(R.id.tvLanguage)
|
|
||||||
|
|
||||||
val rbShow: RadioButton = view.findViewById(R.id.rbShow)
|
|
||||||
val rbHide: RadioButton = view.findViewById(R.id.rbHide)
|
|
||||||
|
|
||||||
when (item?.allow ?: true) {
|
|
||||||
true -> rbShow.isChecked = true
|
|
||||||
else -> rbHide.isChecked = true
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateDesc() {
|
|
||||||
val code = etLanguage.text.toString().trim()
|
|
||||||
val desc = activity.languageNameMap[code] ?: activity.getString(R.string.custom)
|
|
||||||
tvLanguage.text = desc
|
|
||||||
}
|
|
||||||
|
|
||||||
val languageList =
|
|
||||||
activity.languageNameMap.map { MyItem(it.key, true) }.sortedWith(languageComparator)
|
|
||||||
btnPresets.setOnClickListener {
|
|
||||||
activity.run {
|
|
||||||
launchAndShowError {
|
|
||||||
actionsDialog(getString(R.string.presets)) {
|
|
||||||
for (a in languageList) {
|
|
||||||
action("${a.code} ${activity.getDesc(a)}") {
|
|
||||||
etLanguage.setText(a.code)
|
|
||||||
updateDesc()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
etLanguage.setText(item?.code ?: "")
|
|
||||||
updateDesc()
|
|
||||||
|
|
||||||
etLanguage.addTextChangedListener(object : TextWatcher {
|
|
||||||
override fun afterTextChanged(p0: Editable?) {
|
|
||||||
updateDesc()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (item != null) {
|
|
||||||
etLanguage.isEnabledAlpha = false
|
|
||||||
btnPresets.setEnabledColor(
|
|
||||||
activity,
|
|
||||||
R.drawable.ic_edit,
|
|
||||||
activity.attrColor(R.attr.colorTextContent),
|
|
||||||
false
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
val builder = AlertDialog.Builder(activity)
|
|
||||||
.setView(view)
|
|
||||||
.setCancelable(true)
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.setPositiveButton(R.string.ok) { _, _ ->
|
|
||||||
callback.onOK(etLanguage.text.toString().trim(), rbShow.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item != null && item.code != TootStatus.LANGUAGE_CODE_DEFAULT) {
|
|
||||||
builder.setNeutralButton(R.string.delete) { _, _ ->
|
|
||||||
callback.onDelete(etLanguage.text.toString().trim())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
builder.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private fun export() {
|
|
||||||
launchProgress(
|
|
||||||
"export language filter",
|
|
||||||
doInBackground = {
|
|
||||||
val data = JsonObject().apply {
|
|
||||||
for (item in languageList) {
|
|
||||||
put(item.code, item.allow)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.toString()
|
|
||||||
.encodeUTF8()
|
|
||||||
|
|
||||||
val cacheDir = this@ActLanguageFilter.cacheDir
|
|
||||||
cacheDir.mkdir()
|
|
||||||
|
|
||||||
val file = File(
|
|
||||||
cacheDir,
|
|
||||||
"SubwayTooter-language-filter.${Process.myPid()}.${Process.myTid()}.json"
|
|
||||||
)
|
|
||||||
FileOutputStream(file).use {
|
|
||||||
it.write(data)
|
|
||||||
}
|
|
||||||
file
|
|
||||||
},
|
|
||||||
afterProc = {
|
|
||||||
val uri = FileProvider.getUriForFile(
|
|
||||||
this@ActLanguageFilter,
|
|
||||||
FILE_PROVIDER_AUTHORITY,
|
|
||||||
it
|
|
||||||
)
|
|
||||||
val intent = Intent(Intent.ACTION_SEND)
|
|
||||||
intent.type = contentResolver.getType(uri)
|
|
||||||
intent.putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter language filter data")
|
|
||||||
intent.putExtra(Intent.EXTRA_STREAM, uri)
|
|
||||||
|
|
||||||
intent.addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
|
||||||
|
|
||||||
arExport.launch(intent)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun import() {
|
|
||||||
arImport.launch(intentOpenDocument("*/*"))
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("BlockingMethodInNonBlockingContext")
|
|
||||||
private fun import2(uri: Uri) {
|
|
||||||
launchProgress(
|
|
||||||
"import language filter",
|
|
||||||
doInBackground = {
|
|
||||||
log.d("import2 type=${contentResolver.getType(uri)}")
|
|
||||||
try {
|
|
||||||
contentResolver.openInputStream(uri)!!.use {
|
|
||||||
it.readBytes().decodeUTF8().decodeJsonObject()
|
|
||||||
}
|
|
||||||
} catch (ex: Throwable) {
|
|
||||||
showToast(ex, "openInputStream failed.")
|
|
||||||
null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
afterProc = { load(it) }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun confirmBack() {
|
|
||||||
if (equalsLanguageList(column.languageFilter, encodeLanguageList())) {
|
|
||||||
finish()
|
|
||||||
} else {
|
|
||||||
AlertDialog.Builder(this)
|
|
||||||
.setMessage(R.string.language_filter_quit_waring)
|
|
||||||
.setPositiveButton(R.string.ok) { _, _ -> finish() }
|
|
||||||
.setNegativeButton(R.string.cancel, null)
|
|
||||||
.show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -90,6 +90,7 @@ import jp.juggler.subwaytooter.pref.PrefS
|
||||||
import jp.juggler.subwaytooter.span.MyClickableSpan
|
import jp.juggler.subwaytooter.span.MyClickableSpan
|
||||||
import jp.juggler.subwaytooter.span.MyClickableSpanHandler
|
import jp.juggler.subwaytooter.span.MyClickableSpanHandler
|
||||||
import jp.juggler.subwaytooter.table.daoSavedAccount
|
import jp.juggler.subwaytooter.table.daoSavedAccount
|
||||||
|
import jp.juggler.subwaytooter.ui.languageFilter.LanguageFilterActivity
|
||||||
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.reloadEmojiScale
|
import jp.juggler.subwaytooter.util.DecodeOptions.Companion.reloadEmojiScale
|
||||||
import jp.juggler.subwaytooter.util.EmojiDecoder
|
import jp.juggler.subwaytooter.util.EmojiDecoder
|
||||||
import jp.juggler.subwaytooter.util.openBrowser
|
import jp.juggler.subwaytooter.util.openBrowser
|
||||||
|
@ -309,11 +310,10 @@ class ActMain : AppCompatActivity(),
|
||||||
}
|
}
|
||||||
|
|
||||||
val arLanguageFilter = ActivityResultHandler(log) { r ->
|
val arLanguageFilter = ActivityResultHandler(log) { r ->
|
||||||
if (r.isNotOk) return@ActivityResultHandler
|
LanguageFilterActivity.decodeResult(r)?.let { columnIndex ->
|
||||||
appState.saveColumnList()
|
appState.saveColumnList()
|
||||||
r.data?.int(ActLanguageFilter.EXTRA_COLUMN_INDEX)
|
appState.column(columnIndex) ?.onLanguageFilterChanged()
|
||||||
?.let { appState.column(it) }
|
}
|
||||||
?.onLanguageFilterChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
val arNickname = ActivityResultHandler(log) { r ->
|
val arNickname = ActivityResultHandler(log) { r ->
|
||||||
|
|
|
@ -3,7 +3,6 @@ package jp.juggler.subwaytooter.columnviewholder
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.widget.CompoundButton
|
import android.widget.CompoundButton
|
||||||
import jp.juggler.subwaytooter.ActColumnCustomize
|
import jp.juggler.subwaytooter.ActColumnCustomize
|
||||||
import jp.juggler.subwaytooter.ActLanguageFilter
|
|
||||||
import jp.juggler.subwaytooter.App1
|
import jp.juggler.subwaytooter.App1
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.action.accountResendConfirmMail
|
import jp.juggler.subwaytooter.action.accountResendConfirmMail
|
||||||
|
@ -12,7 +11,13 @@ import jp.juggler.subwaytooter.action.notificationDeleteAll
|
||||||
import jp.juggler.subwaytooter.actmain.closeColumn
|
import jp.juggler.subwaytooter.actmain.closeColumn
|
||||||
import jp.juggler.subwaytooter.actmain.closeColumnAll
|
import jp.juggler.subwaytooter.actmain.closeColumnAll
|
||||||
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
import jp.juggler.subwaytooter.api.entity.TootAnnouncement
|
||||||
import jp.juggler.subwaytooter.column.*
|
import jp.juggler.subwaytooter.column.Column
|
||||||
|
import jp.juggler.subwaytooter.column.ColumnType
|
||||||
|
import jp.juggler.subwaytooter.column.addColumnViewHolder
|
||||||
|
import jp.juggler.subwaytooter.column.fireShowContent
|
||||||
|
import jp.juggler.subwaytooter.column.isSearchColumn
|
||||||
|
import jp.juggler.subwaytooter.column.startLoading
|
||||||
|
import jp.juggler.subwaytooter.ui.languageFilter.LanguageFilterActivity.Companion.openLanguageFilterActivity
|
||||||
import jp.juggler.util.log.showToast
|
import jp.juggler.util.log.showToast
|
||||||
import jp.juggler.util.log.withCaption
|
import jp.juggler.util.log.withCaption
|
||||||
import jp.juggler.util.ui.hideKeyboard
|
import jp.juggler.util.ui.hideKeyboard
|
||||||
|
@ -252,9 +257,9 @@ fun ColumnViewHolder.onClickImpl(v: View?) {
|
||||||
|
|
||||||
btnLanguageFilter ->
|
btnLanguageFilter ->
|
||||||
activity.appState.columnIndex(column)?.let { colIdx ->
|
activity.appState.columnIndex(column)?.let { colIdx ->
|
||||||
|
openLanguageFilterActivity(
|
||||||
activity.arLanguageFilter.launch(
|
activity.arLanguageFilter,
|
||||||
ActLanguageFilter.createIntent(activity, colIdx)
|
colIdx
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,12 +1,12 @@
|
||||||
package jp.juggler.subwaytooter.dialog
|
package jp.juggler.subwaytooter.dialog
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
|
import android.app.Activity
|
||||||
import android.text.method.LinkMovementMethod
|
import android.text.method.LinkMovementMethod
|
||||||
import android.text.util.Linkify
|
import android.text.util.Linkify
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.annotation.StringRes
|
import androidx.annotation.StringRes
|
||||||
import androidx.appcompat.app.AlertDialog
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import jp.juggler.subwaytooter.R
|
import jp.juggler.subwaytooter.R
|
||||||
import jp.juggler.subwaytooter.databinding.DlgConfirmBinding
|
import jp.juggler.subwaytooter.databinding.DlgConfirmBinding
|
||||||
import jp.juggler.util.ui.dismissSafe
|
import jp.juggler.util.ui.dismissSafe
|
||||||
|
@ -16,57 +16,8 @@ import kotlin.coroutines.resume
|
||||||
import kotlin.coroutines.resumeWithException
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
object DlgConfirm {
|
object DlgConfirm {
|
||||||
|
|
||||||
// interface Callback {
|
|
||||||
// var isConfirmEnabled: Boolean
|
|
||||||
//
|
|
||||||
// fun onOK()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @SuppressLint("InflateParams")
|
|
||||||
// fun open(activity: Activity, message: String, callback: Callback): Dialog {
|
|
||||||
//
|
|
||||||
// if (!callback.isConfirmEnabled) {
|
|
||||||
// callback.onOK()
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
|
||||||
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
|
||||||
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
|
||||||
// tvMessage.text = message
|
|
||||||
//
|
|
||||||
// AlertDialog.Builder(activity)
|
|
||||||
// .setView(view)
|
|
||||||
// .setCancelable(true)
|
|
||||||
// .setNegativeButton(R.string.cancel, null)
|
|
||||||
// .setPositiveButton(R.string.ok) { _, _ ->
|
|
||||||
// if (cbSkipNext.isChecked) {
|
|
||||||
// callback.isConfirmEnabled = false
|
|
||||||
// }
|
|
||||||
// callback.onOK()
|
|
||||||
// }
|
|
||||||
// .show()
|
|
||||||
// }
|
|
||||||
|
|
||||||
// @SuppressLint("InflateParams")
|
|
||||||
// fun openSimple(activity: Activity, message: String, callback: () -> Unit) {
|
|
||||||
// val view = activity.layoutInflater.inflate(R.layout.dlg_confirm, null, false)
|
|
||||||
// val tvMessage = view.findViewById<TextView>(R.id.tvMessage)
|
|
||||||
// val cbSkipNext = view.findViewById<CheckBox>(R.id.cbSkipNext)
|
|
||||||
// tvMessage.text = message
|
|
||||||
// cbSkipNext.visibility = View.GONE
|
|
||||||
//
|
|
||||||
// AlertDialog.Builder(activity)
|
|
||||||
// .setView(view)
|
|
||||||
// .setCancelable(true)
|
|
||||||
// .setNegativeButton(R.string.cancel, null)
|
|
||||||
// .setPositiveButton(R.string.ok) { _, _ -> callback() }
|
|
||||||
// .show()
|
|
||||||
// }
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
suspend inline fun AppCompatActivity.confirm(
|
suspend inline fun Activity.confirm(
|
||||||
message: String,
|
message: String,
|
||||||
isConfirmEnabled: Boolean,
|
isConfirmEnabled: Boolean,
|
||||||
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
|
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
|
||||||
|
@ -94,10 +45,10 @@ object DlgConfirm {
|
||||||
if (skipNext) setConfirmEnabled(false)
|
if (skipNext) setConfirmEnabled(false)
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun AppCompatActivity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
suspend fun Activity.confirm(@StringRes messageId: Int, vararg args: Any?) =
|
||||||
confirm(getString(messageId, *args))
|
confirm(getString(messageId, *args))
|
||||||
|
|
||||||
suspend fun AppCompatActivity.confirm(message: CharSequence, title: CharSequence? = null) {
|
suspend fun Activity.confirm(message: CharSequence, title: CharSequence? = null) {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
try {
|
try {
|
||||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||||
|
@ -124,10 +75,10 @@ object DlgConfirm {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
suspend fun AppCompatActivity.okDialog(@StringRes messageId: Int, vararg args: Any?) =
|
suspend fun Activity.okDialog(@StringRes messageId: Int, vararg args: Any?) =
|
||||||
okDialog(getString(messageId, *args))
|
okDialog(getString(messageId, *args))
|
||||||
|
|
||||||
suspend fun AppCompatActivity.okDialog(message: CharSequence, title: CharSequence? = null) {
|
suspend fun Activity.okDialog(message: CharSequence, title: CharSequence? = null) {
|
||||||
suspendCancellableCoroutine { cont ->
|
suspendCancellableCoroutine { cont ->
|
||||||
try {
|
try {
|
||||||
val views = DlgConfirmBinding.inflate(layoutInflater)
|
val views = DlgConfirmBinding.inflate(layoutInflater)
|
||||||
|
|
|
@ -0,0 +1,452 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Intent
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.PersistableBundle
|
||||||
|
import android.view.Window
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.result.ActivityResult
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.requiredHeightIn
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.wrapContentHeight
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.Add
|
||||||
|
import androidx.compose.material.icons.outlined.MoreVert
|
||||||
|
import androidx.compose.material.icons.outlined.Save
|
||||||
|
import androidx.compose.material3.CircularProgressIndicator
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.HorizontalDivider
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Scaffold
|
||||||
|
import androidx.compose.material3.SnackbarHost
|
||||||
|
import androidx.compose.material3.SnackbarHostState
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TopAppBar
|
||||||
|
import androidx.compose.material3.TopAppBarDefaults
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.tooling.preview.Preview
|
||||||
|
import androidx.compose.ui.unit.LayoutDirection
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.content.FileProvider
|
||||||
|
import jp.juggler.subwaytooter.App1
|
||||||
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
|
import jp.juggler.subwaytooter.dialog.DlgConfirm.confirm
|
||||||
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
|
import jp.juggler.subwaytooter.pref.FILE_PROVIDER_AUTHORITY
|
||||||
|
import jp.juggler.subwaytooter.util.StColorScheme
|
||||||
|
import jp.juggler.subwaytooter.util.collectOnLifeCycle
|
||||||
|
import jp.juggler.subwaytooter.util.dummyStColorTheme
|
||||||
|
import jp.juggler.subwaytooter.util.fireBackPressed
|
||||||
|
import jp.juggler.subwaytooter.util.getStColorTheme
|
||||||
|
import jp.juggler.subwaytooter.util.provideViewModel
|
||||||
|
import jp.juggler.util.backPressed
|
||||||
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
|
import jp.juggler.util.data.checkMimeTypeAndGrant
|
||||||
|
import jp.juggler.util.data.intentOpenDocument
|
||||||
|
import jp.juggler.util.int
|
||||||
|
import jp.juggler.util.log.LogCategory
|
||||||
|
import jp.juggler.util.log.showError
|
||||||
|
import jp.juggler.util.ui.ActivityResultHandler
|
||||||
|
import jp.juggler.util.ui.isNotOk
|
||||||
|
import jp.juggler.util.ui.isOk
|
||||||
|
import jp.juggler.util.ui.launch
|
||||||
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class LanguageFilterActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val log = LogCategory("LanguageFilterActivity")
|
||||||
|
|
||||||
|
fun openLanguageFilterActivity(
|
||||||
|
launcher: ActivityResultHandler,
|
||||||
|
columnIndex: Int,
|
||||||
|
) {
|
||||||
|
Intent(
|
||||||
|
launcher.context
|
||||||
|
?: error("openLanguageFilterActivity: launcher is not registered."),
|
||||||
|
LanguageFilterActivity::class.java
|
||||||
|
).apply {
|
||||||
|
putExtra(LanguageFilterViewModel.EXTRA_COLUMN_INDEX, columnIndex)
|
||||||
|
}.launch(launcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun decodeResult(r: ActivityResult): Int? =
|
||||||
|
when {
|
||||||
|
r.isOk -> r.data?.int(LanguageFilterViewModel.EXTRA_COLUMN_INDEX)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val viewModel by lazy {
|
||||||
|
provideViewModel(this) {
|
||||||
|
LanguageFilterViewModel(application)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val stColorScheme by lazy {
|
||||||
|
getStColorTheme()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val arImport = ActivityResultHandler(log) { r ->
|
||||||
|
if (r.isNotOk) return@ActivityResultHandler
|
||||||
|
r.data?.checkMimeTypeAndGrant(contentResolver)
|
||||||
|
?.firstOrNull()?.uri?.let {
|
||||||
|
viewModel.import2(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(outState: Bundle, outPersistentState: PersistableBundle) {
|
||||||
|
super.onSaveInstanceState(outState, outPersistentState)
|
||||||
|
viewModel.saveState(outState)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
|
arImport.register(this)
|
||||||
|
super.onCreate(savedInstanceState)
|
||||||
|
App1.setActivityTheme(this)
|
||||||
|
|
||||||
|
backPressed {
|
||||||
|
launchAndShowError {
|
||||||
|
if (viewModel.isLanguageListChanged()) {
|
||||||
|
confirm(R.string.language_filter_quit_waring)
|
||||||
|
}
|
||||||
|
finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.restoreOrInitialize(
|
||||||
|
this,
|
||||||
|
savedInstanceState,
|
||||||
|
intent,
|
||||||
|
)
|
||||||
|
|
||||||
|
// ステータスバーの色にattr色を使っているので、テーマの指定は必要
|
||||||
|
requestWindowFeature(Window.FEATURE_NO_TITLE)
|
||||||
|
App1.setActivityTheme(this)
|
||||||
|
val stColorScheme = getStColorTheme()
|
||||||
|
setContent {
|
||||||
|
Screen(
|
||||||
|
viewModel = viewModel,
|
||||||
|
stColorScheme = stColorScheme,
|
||||||
|
languageListFlow = viewModel.languageList,
|
||||||
|
progressMessageFlow = viewModel.progressMessage,
|
||||||
|
saveAction = {
|
||||||
|
setResult(RESULT_OK, viewModel.save())
|
||||||
|
finish()
|
||||||
|
},
|
||||||
|
getDisplayName = { langDesc(it, viewModel.languageNameMap) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
collectOnLifeCycle(viewModel.error) {
|
||||||
|
it ?: return@collectOnLifeCycle
|
||||||
|
showError(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun edit(myItem: LanguageFilterItem?) {
|
||||||
|
val result = dialogLanguageFilterEdit(
|
||||||
|
myItem,
|
||||||
|
viewModel.languageNameMap,
|
||||||
|
stColorScheme,
|
||||||
|
)
|
||||||
|
viewModel.handleEditResult(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 言語フィルタのエクスポート
|
||||||
|
* - 現在のデータをjsonエンコードする
|
||||||
|
* - 保存先アプリに渡す
|
||||||
|
* 保存自体はすぐ終わるのでプログレス表示やFlow経由の結果受取はない
|
||||||
|
*/
|
||||||
|
suspend fun export() {
|
||||||
|
val file = withContext(Dispatchers.IO) {
|
||||||
|
viewModel.createExportCacheFile()
|
||||||
|
}
|
||||||
|
val uri = FileProvider.getUriForFile(
|
||||||
|
this@LanguageFilterActivity,
|
||||||
|
FILE_PROVIDER_AUTHORITY,
|
||||||
|
file,
|
||||||
|
)
|
||||||
|
val intent = Intent(Intent.ACTION_SEND).apply {
|
||||||
|
type = contentResolver.getType(uri)
|
||||||
|
putExtra(Intent.EXTRA_SUBJECT, "SubwayTooter language filter data")
|
||||||
|
putExtra(Intent.EXTRA_STREAM, uri)
|
||||||
|
addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION or Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
}
|
||||||
|
startActivity(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Preview
|
||||||
|
@Composable
|
||||||
|
fun DefaultPreview() {
|
||||||
|
Screen(
|
||||||
|
viewModel = LanguageFilterViewModel(Application()),
|
||||||
|
stColorScheme = dummyStColorTheme(),
|
||||||
|
languageListFlow = MutableStateFlow(
|
||||||
|
listOf(
|
||||||
|
LanguageFilterItem(
|
||||||
|
code = TootStatus.LANGUAGE_CODE_DEFAULT,
|
||||||
|
allow = true,
|
||||||
|
),
|
||||||
|
LanguageFilterItem(
|
||||||
|
code = TootStatus.LANGUAGE_CODE_UNKNOWN,
|
||||||
|
allow = false,
|
||||||
|
),
|
||||||
|
LanguageFilterItem(
|
||||||
|
code = "xxx",
|
||||||
|
allow = true,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
progressMessageFlow = MutableStateFlow(
|
||||||
|
StringResAndArgs(R.string.wait_previous_operation)
|
||||||
|
),
|
||||||
|
saveAction = {},
|
||||||
|
getDisplayName = { "言語の名前" },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun Screen(
|
||||||
|
viewModel: LanguageFilterViewModel,
|
||||||
|
stColorScheme: StColorScheme,
|
||||||
|
languageListFlow: StateFlow<List<LanguageFilterItem>>,
|
||||||
|
progressMessageFlow: StateFlow<StringResAndArgs?>,
|
||||||
|
saveAction: () -> Unit,
|
||||||
|
getDisplayName: (String) -> String,
|
||||||
|
) {
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
val snackbarHostState = remember { SnackbarHostState() }
|
||||||
|
val progressMessageState = progressMessageFlow.collectAsState()
|
||||||
|
MaterialTheme(colorScheme = stColorScheme.materialColorScheme) {
|
||||||
|
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
|
||||||
|
Scaffold(
|
||||||
|
snackbarHost = {
|
||||||
|
SnackbarHost(hostState = snackbarHostState)
|
||||||
|
},
|
||||||
|
topBar = {
|
||||||
|
TopAppBar(
|
||||||
|
colors = TopAppBarDefaults.topAppBarColors(),
|
||||||
|
title = {
|
||||||
|
Text(stringResource(R.string.language_filter))
|
||||||
|
},
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
fireBackPressed()
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
// imageVector = AutoMirrored.Outlined.ArrowBack,
|
||||||
|
imageVector = Icons.AutoMirrored.Outlined.ArrowBack,
|
||||||
|
contentDescription = stringResource(R.string.close)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
edit(null)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.Add,
|
||||||
|
contentDescription = stringResource(R.string.add),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { saveAction() }
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Save,
|
||||||
|
contentDescription = stringResource(R.string.close)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
actionsDialog {
|
||||||
|
action(getString(R.string.clear_all)) {
|
||||||
|
viewModel.clearAllLanguage()
|
||||||
|
}
|
||||||
|
action(getString(R.string.export)) {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
export()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action(getString(R.string.import_)) {
|
||||||
|
arImport.launch(intentOpenDocument("*/*"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Outlined.MoreVert,
|
||||||
|
contentDescription = stringResource(R.string.more),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
},
|
||||||
|
) { innerPadding ->
|
||||||
|
ScrollContent(
|
||||||
|
scope = scope,
|
||||||
|
innerPadding = innerPadding,
|
||||||
|
languageListFlow = languageListFlow,
|
||||||
|
getDisplayName = getDisplayName,
|
||||||
|
stColorScheme = stColorScheme,
|
||||||
|
)
|
||||||
|
val progressMessage = progressMessageState.value?.let {
|
||||||
|
stringResource(it.stringId, *it.args)
|
||||||
|
}
|
||||||
|
if (progressMessage != null) {
|
||||||
|
ProgressCircleAndText(stColorScheme, progressMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ProgressCircleAndText(
|
||||||
|
stColorScheme: StColorScheme,
|
||||||
|
progressMessage: String,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxSize().clickable {
|
||||||
|
// 奥の要素をクリックできなくする
|
||||||
|
}.background(
|
||||||
|
color = stColorScheme.colorProgressBackground,
|
||||||
|
),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center,
|
||||||
|
) {
|
||||||
|
CircularProgressIndicator(
|
||||||
|
modifier = Modifier.size(160.dp),
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.size(20.dp))
|
||||||
|
Text(progressMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ScrollContent(
|
||||||
|
stColorScheme: StColorScheme,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
innerPadding: PaddingValues,
|
||||||
|
languageListFlow: StateFlow<List<LanguageFilterItem>>,
|
||||||
|
getDisplayName: (String) -> String,
|
||||||
|
) {
|
||||||
|
val stateLanguageList = languageListFlow.collectAsState()
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.padding(innerPadding).fillMaxSize(),
|
||||||
|
) {
|
||||||
|
items(stateLanguageList.value) {
|
||||||
|
LanguageItemCard(
|
||||||
|
it,
|
||||||
|
scope,
|
||||||
|
getDisplayName,
|
||||||
|
)
|
||||||
|
HorizontalDivider(
|
||||||
|
color = stColorScheme.colorDivider,
|
||||||
|
thickness = 1.dp
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun LanguageItemCard(
|
||||||
|
item: LanguageFilterItem,
|
||||||
|
scope: CoroutineScope,
|
||||||
|
getDisplayName: (String) -> String,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
// .requiredHeightIn(min = 48.dp)
|
||||||
|
.clickable {
|
||||||
|
scope.launch {
|
||||||
|
try {
|
||||||
|
edit(item)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
showError(ex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = Modifier
|
||||||
|
.requiredHeightIn(min = 56.dp)
|
||||||
|
.wrapContentHeight()
|
||||||
|
.padding(
|
||||||
|
horizontal = 12.dp,
|
||||||
|
vertical = 6.dp,
|
||||||
|
),
|
||||||
|
|
||||||
|
text = "${
|
||||||
|
item.code
|
||||||
|
} ${
|
||||||
|
getDisplayName(item.code)
|
||||||
|
} : ${
|
||||||
|
stringResource(
|
||||||
|
when {
|
||||||
|
item.allow -> R.string.language_show
|
||||||
|
else -> R.string.language_hide
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}",
|
||||||
|
color = when (item.allow) {
|
||||||
|
true -> MaterialTheme.colorScheme.onBackground
|
||||||
|
false -> MaterialTheme.colorScheme.error
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,99 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
|
import jp.juggler.subwaytooter.databinding.DlgLanguageFilterBinding
|
||||||
|
import jp.juggler.subwaytooter.dialog.actionsDialog
|
||||||
|
import jp.juggler.subwaytooter.util.StColorScheme
|
||||||
|
import jp.juggler.util.coroutine.launchAndShowError
|
||||||
|
import jp.juggler.util.ui.dismissSafe
|
||||||
|
import jp.juggler.util.ui.isEnabledAlpha
|
||||||
|
import jp.juggler.util.ui.setEnabledColor
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
|
sealed interface LanguageFilterEditResult {
|
||||||
|
class Update(val code: String, val allow: Boolean) : LanguageFilterEditResult
|
||||||
|
class Delete(val code: String) : LanguageFilterEditResult
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 言語コード1つを追加/編集/削除するダイアログ
|
||||||
|
*/
|
||||||
|
suspend fun ComponentActivity.dialogLanguageFilterEdit(
|
||||||
|
// 既存項目の編集時は非null
|
||||||
|
item: LanguageFilterItem?,
|
||||||
|
// 言語コード→表示名のマップ
|
||||||
|
nameMap: Map<String, LanguageInfo>,
|
||||||
|
// 色スキーマ
|
||||||
|
stColorScheme: StColorScheme,
|
||||||
|
): LanguageFilterEditResult = suspendCancellableCoroutine { cont ->
|
||||||
|
val views = DlgLanguageFilterBinding.inflate(layoutInflater, null, false)
|
||||||
|
|
||||||
|
views.apply {
|
||||||
|
fun updateDesc() {
|
||||||
|
val code = etLanguage.text.toString().trim()
|
||||||
|
tvLanguage.text = nameMap[code]?.displayName ?: getString(R.string.custom)
|
||||||
|
}
|
||||||
|
when (item?.allow ?: true) {
|
||||||
|
true -> rbShow.isChecked = true
|
||||||
|
else -> rbHide.isChecked = true
|
||||||
|
}
|
||||||
|
btnPresets.setOnClickListener {
|
||||||
|
launchAndShowError {
|
||||||
|
actionsDialog(getString(R.string.presets)) {
|
||||||
|
val languageList = nameMap.map {
|
||||||
|
LanguageFilterItem(it.key, true)
|
||||||
|
}.sortedWith(languageFilterItemComparator)
|
||||||
|
for (a in languageList) {
|
||||||
|
action("${a.code} ${langDesc(a.code, nameMap)}") {
|
||||||
|
etLanguage.setText(a.code)
|
||||||
|
updateDesc()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
etLanguage.addTextChangedListener { updateDesc() }
|
||||||
|
etLanguage.setText(item?.code ?: "")
|
||||||
|
updateDesc()
|
||||||
|
// 編集時は言語コードを変更できない
|
||||||
|
etLanguage.isEnabledAlpha = item == null
|
||||||
|
btnPresets.setEnabledColor(
|
||||||
|
btnPresets.context,
|
||||||
|
R.drawable.ic_edit,
|
||||||
|
stColorScheme.colorTextContent.toArgb(),
|
||||||
|
item == null
|
||||||
|
)
|
||||||
|
fun getCode() = etLanguage.text.toString().trim()
|
||||||
|
fun isAllow() = rbShow.isChecked
|
||||||
|
AlertDialog.Builder(this@dialogLanguageFilterEdit).apply {
|
||||||
|
setView(views.root)
|
||||||
|
setCancelable(true)
|
||||||
|
setNegativeButton(R.string.cancel, null)
|
||||||
|
setPositiveButton(R.string.ok) { _, _ ->
|
||||||
|
if (cont.isActive) cont.resume(
|
||||||
|
LanguageFilterEditResult.Update(getCode(), isAllow())
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
if (item != null && item.code != TootStatus.LANGUAGE_CODE_DEFAULT) {
|
||||||
|
setNeutralButton(R.string.delete) { _, _ ->
|
||||||
|
if (cont.isActive) cont.resume(
|
||||||
|
LanguageFilterEditResult.Delete(item.code)
|
||||||
|
) {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.create().also { dialog ->
|
||||||
|
dialog.setOnDismissListener {
|
||||||
|
if (cont.isActive) cont.resumeWithException(CancellationException())
|
||||||
|
}
|
||||||
|
cont.invokeOnCancellation { dialog.dismissSafe() }
|
||||||
|
dialog.show()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
|
|
||||||
|
data class LanguageFilterItem(
|
||||||
|
val code: String,
|
||||||
|
var allow: Boolean,
|
||||||
|
)
|
||||||
|
|
||||||
|
val languageFilterItemComparator = Comparator<LanguageFilterItem> { a, b ->
|
||||||
|
when {
|
||||||
|
a.code == TootStatus.LANGUAGE_CODE_DEFAULT -> -1
|
||||||
|
b.code == TootStatus.LANGUAGE_CODE_DEFAULT -> 1
|
||||||
|
a.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> -1
|
||||||
|
b.code == TootStatus.LANGUAGE_CODE_UNKNOWN -> 1
|
||||||
|
else -> a.code.compareTo(b.code)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,258 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.Process
|
||||||
|
import androidx.lifecycle.AndroidViewModel
|
||||||
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import jp.juggler.subwaytooter.App1
|
||||||
|
import jp.juggler.subwaytooter.AppState
|
||||||
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.entity.TootStatus
|
||||||
|
import jp.juggler.subwaytooter.column.Column
|
||||||
|
import jp.juggler.util.data.JsonObject
|
||||||
|
import jp.juggler.util.data.buildJsonObject
|
||||||
|
import jp.juggler.util.data.decodeJsonObject
|
||||||
|
import jp.juggler.util.data.decodeUTF8
|
||||||
|
import jp.juggler.util.data.encodeUTF8
|
||||||
|
import jp.juggler.util.int
|
||||||
|
import jp.juggler.util.log.LogCategory
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
class LanguageFilterViewModel(
|
||||||
|
application: Application,
|
||||||
|
) : AndroidViewModel(application) {
|
||||||
|
companion object {
|
||||||
|
private val log = LogCategory("LanguageFilterViewModel")
|
||||||
|
private const val STATE_LANGUAGE_LIST = "language_list"
|
||||||
|
const val EXTRA_COLUMN_INDEX = "column_index"
|
||||||
|
}
|
||||||
|
|
||||||
|
private var mastodonLanguageJsonCache: String? = null
|
||||||
|
|
||||||
|
private val context: Context
|
||||||
|
get() = getApplication()
|
||||||
|
|
||||||
|
lateinit var appState: AppState
|
||||||
|
|
||||||
|
private var columnIndex: Int = 0
|
||||||
|
|
||||||
|
private lateinit var column: Column
|
||||||
|
|
||||||
|
// 編集中の言語リスト
|
||||||
|
private val _languageList = MutableStateFlow<List<LanguageFilterItem>>(emptyList())
|
||||||
|
val languageList = _languageList.asStateFlow()
|
||||||
|
|
||||||
|
// 言語コードと名前の対応表
|
||||||
|
var languageNameMap: Map<String, LanguageInfo> = emptyMap()
|
||||||
|
|
||||||
|
// エラーイベント
|
||||||
|
private val _error = Channel<Throwable?>(capacity = Channel.CONFLATED)
|
||||||
|
val error = _error.receiveAsFlow()
|
||||||
|
|
||||||
|
// ロード中表示の文字列
|
||||||
|
private val _progressMessage = MutableStateFlow<StringResAndArgs?>(null)
|
||||||
|
val progressMessage = _progressMessage.asStateFlow()
|
||||||
|
|
||||||
|
fun saveState(outState: Bundle) {
|
||||||
|
outState.putString(STATE_LANGUAGE_LIST, encodeLanguageList().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreOrInitialize(
|
||||||
|
activityContext: Context,
|
||||||
|
savedInstanceState: Bundle?,
|
||||||
|
intent: Intent?,
|
||||||
|
) {
|
||||||
|
appState = App1.getAppState(context)
|
||||||
|
columnIndex = intent?.int(EXTRA_COLUMN_INDEX) ?: 0
|
||||||
|
column = appState.column(columnIndex) ?: error("missing column[$columnIndex]")
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
if (mastodonLanguageJsonCache == null) {
|
||||||
|
_progressMessage.value = StringResAndArgs(R.string.language_filter_loading)
|
||||||
|
mastodonLanguageJsonCache = try {
|
||||||
|
val accessInfo = column.accessInfo
|
||||||
|
if (accessInfo.isNA) {
|
||||||
|
"na"
|
||||||
|
} else if (accessInfo.isPseudo) {
|
||||||
|
loadMastodonLanguages(accessInfo.apiHost)
|
||||||
|
} else {
|
||||||
|
loadMastodonLanguages(accessInfo.apiHost, accessInfo.bearerAccessToken)
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
"error"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 端末のconfiguration changeがありうるので、言語マップはActivityのonCreateのたびに再取得する
|
||||||
|
languageNameMap = withContext(Dispatchers.IO) {
|
||||||
|
getLanguageNames(context, mastodonLanguageJsonCache).apply {
|
||||||
|
val specDefault = LanguageInfo(
|
||||||
|
code = TootStatus.LANGUAGE_CODE_DEFAULT,
|
||||||
|
name = TootStatus.LANGUAGE_CODE_DEFAULT,
|
||||||
|
displayName = activityContext.getString(R.string.language_code_default),
|
||||||
|
)
|
||||||
|
val specUnknown = LanguageInfo(
|
||||||
|
code = TootStatus.LANGUAGE_CODE_UNKNOWN,
|
||||||
|
name = TootStatus.LANGUAGE_CODE_UNKNOWN,
|
||||||
|
displayName = activityContext.getString(R.string.language_code_unknown)
|
||||||
|
)
|
||||||
|
put(specDefault.code, specDefault)
|
||||||
|
put(specUnknown.code, specUnknown)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 状態の復元
|
||||||
|
try {
|
||||||
|
savedInstanceState?.getString(STATE_LANGUAGE_LIST, null)
|
||||||
|
?.decodeJsonObject()
|
||||||
|
?.let { load(it) }
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "restore failed.")
|
||||||
|
}
|
||||||
|
// 未初期化なら初期データのロード
|
||||||
|
if (languageList.value.isEmpty()) {
|
||||||
|
load(column.languageFilter)
|
||||||
|
}
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
_error.send(ex)
|
||||||
|
} finally {
|
||||||
|
_progressMessage.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun load(src: JsonObject?) {
|
||||||
|
_languageList.value = buildList<LanguageFilterItem> {
|
||||||
|
if (src != null) {
|
||||||
|
for (key in src.keys) {
|
||||||
|
add(LanguageFilterItem(key, src.boolean(key) ?: true))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (none { it.code == TootStatus.LANGUAGE_CODE_DEFAULT }) {
|
||||||
|
add(LanguageFilterItem(TootStatus.LANGUAGE_CODE_DEFAULT, true))
|
||||||
|
}
|
||||||
|
log.i("load: list size=$size")
|
||||||
|
}.sortedWith(languageFilterItemComparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createExportCacheFile(): File {
|
||||||
|
val bytes = JsonObject().apply {
|
||||||
|
for (item in _languageList.value) {
|
||||||
|
put(item.code, item.allow)
|
||||||
|
}
|
||||||
|
}.toString().encodeUTF8()
|
||||||
|
|
||||||
|
val cacheDir = context.cacheDir
|
||||||
|
cacheDir.mkdirs()
|
||||||
|
val file = File(
|
||||||
|
cacheDir,
|
||||||
|
"SubwayTooter-language-filter.${Process.myPid()}.${Process.myTid()}.json"
|
||||||
|
)
|
||||||
|
FileOutputStream(file).use {
|
||||||
|
it.write(bytes)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("BlockingMethodInNonBlockingContext")
|
||||||
|
fun import2(uri: Uri) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_progressMessage.value = StringResAndArgs(R.string.language_filter_importing)
|
||||||
|
val jsonObject = withContext(Dispatchers.IO) {
|
||||||
|
val resolver = context.contentResolver
|
||||||
|
log.i("import2 type=${resolver.getType(uri)}")
|
||||||
|
(resolver.openInputStream(uri) ?: error("openInputStream returns null"))
|
||||||
|
.use {
|
||||||
|
it.readBytes().decodeUTF8().decodeJsonObject()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
load(jsonObject)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "import2 failed.")
|
||||||
|
_error.send(ex)
|
||||||
|
} finally {
|
||||||
|
_progressMessage.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(): Intent {
|
||||||
|
column.languageFilter = encodeLanguageList()
|
||||||
|
return Intent().apply {
|
||||||
|
putExtra(EXTRA_COLUMN_INDEX, columnIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UIのデータをJsonObjectにエンコード
|
||||||
|
private fun encodeLanguageList() = buildJsonObject {
|
||||||
|
for (item in languageList.value) {
|
||||||
|
put(item.code, item.allow)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isLanguageListChanged(): Boolean {
|
||||||
|
fun JsonObject.encodeSorted(): String {
|
||||||
|
val clone = this.toString().decodeJsonObject()
|
||||||
|
if (!clone.contains(TootStatus.LANGUAGE_CODE_DEFAULT)) {
|
||||||
|
clone[TootStatus.LANGUAGE_CODE_DEFAULT] = true
|
||||||
|
}
|
||||||
|
return clone.keys.sorted().joinToString(",") { "$it=${this[it]}" }
|
||||||
|
}
|
||||||
|
|
||||||
|
val current = (encodeLanguageList()).encodeSorted()
|
||||||
|
val initial = (column.languageFilter ?: JsonObject()).encodeSorted()
|
||||||
|
return current != initial
|
||||||
|
}
|
||||||
|
|
||||||
|
fun handleEditResult(result: LanguageFilterEditResult) {
|
||||||
|
_languageList.value = when (result) {
|
||||||
|
is LanguageFilterEditResult.Delete ->
|
||||||
|
_languageList.value.filter { it.code != result.code }
|
||||||
|
|
||||||
|
is LanguageFilterEditResult.Update -> when (
|
||||||
|
val item = languageList.value.find { it.code == result.code }
|
||||||
|
) {
|
||||||
|
null -> (_languageList.value + listOf(
|
||||||
|
LanguageFilterItem(
|
||||||
|
result.code,
|
||||||
|
result.allow
|
||||||
|
)
|
||||||
|
))
|
||||||
|
.sortedWith(languageFilterItemComparator)
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
item.allow = result.allow
|
||||||
|
_languageList.value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 言語フィルタを初期状態に戻す
|
||||||
|
fun clearAllLanguage() {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
_languageList.value = listOf(
|
||||||
|
LanguageFilterItem(TootStatus.LANGUAGE_CODE_DEFAULT, true)
|
||||||
|
)
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "clearAllLanguage failed.")
|
||||||
|
_error.send(ex)
|
||||||
|
} finally {
|
||||||
|
_progressMessage.value = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,142 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import jp.juggler.subwaytooter.App1
|
||||||
|
import jp.juggler.subwaytooter.R
|
||||||
|
import jp.juggler.subwaytooter.api.entity.Host
|
||||||
|
import jp.juggler.util.data.decodeJsonArray
|
||||||
|
import jp.juggler.util.data.loadRawResource
|
||||||
|
import jp.juggler.util.data.notBlank
|
||||||
|
import jp.juggler.util.log.LogCategory
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import okhttp3.Request
|
||||||
|
import ru.gildor.coroutines.okhttp.await
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
private val log = LogCategory("GetLanguageNames")
|
||||||
|
|
||||||
|
data class LanguageInfo(
|
||||||
|
// code,name from https://mastodon.social/api/v1/instance/languages
|
||||||
|
val code: String,
|
||||||
|
val name: String,
|
||||||
|
// displayName from locale
|
||||||
|
var displayName: String = "",
|
||||||
|
) {
|
||||||
|
val locales = HashSet<Locale>()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun Context.langDesc(code: String, nameMap: Map<String, LanguageInfo>) =
|
||||||
|
nameMap[code]?.displayName ?: getString(R.string.custom)
|
||||||
|
|
||||||
|
suspend fun loadMastodonLanguages(
|
||||||
|
apiHost: Host,
|
||||||
|
accessToken: String? = null,
|
||||||
|
) = withContext(Dispatchers.IO) {
|
||||||
|
val request = Request.Builder().apply {
|
||||||
|
url("https://${apiHost.ascii}/api/v1/instance/languages")
|
||||||
|
get()
|
||||||
|
accessToken?.notBlank()?.let {
|
||||||
|
header("Authorization", "Bearer $it")
|
||||||
|
}
|
||||||
|
}.build()
|
||||||
|
val response = App1.ok_http_client.newCall(request).await()
|
||||||
|
if (!response.isSuccessful) error("HTTP ${response.code}")
|
||||||
|
response.body.string()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val languageStringIds = buildMap {
|
||||||
|
put("cnd", R.string.language_name_montenegrin)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun mergeLanguageName(
|
||||||
|
context: Context,
|
||||||
|
jsonString: String,
|
||||||
|
) = HashMap<String, LanguageInfo>().also { dst ->
|
||||||
|
// read from mastodon languages
|
||||||
|
for (it in jsonString.decodeJsonArray().objectList()) {
|
||||||
|
val code = it.string("code")?.notBlank() ?: error("missing item.code")
|
||||||
|
val name = it.string("name")?.notBlank() ?: error("missing item.name")
|
||||||
|
dst[code] = LanguageInfo(code = code, name = name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// using Locale.forLanguageTag to find Java Locale
|
||||||
|
for (item in dst.values) {
|
||||||
|
val locale = Locale.forLanguageTag(item.code)
|
||||||
|
log.i("code=${item.code} locale=$locale lang=${locale.language} displayName=${locale.displayName}")
|
||||||
|
// 2018年に、ISO 639-2およびISO 639-3にモンテネグロ語の言語コード(cnr)が追加された
|
||||||
|
// Android Javaには定義がないが、この場合にdisplayNameは言語コードと同じ値になる
|
||||||
|
if (locale.displayName == item.code || locale.displayName.isNullOrBlank()) continue
|
||||||
|
item.locales.add(locale)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 今度はJavaのロケールを順に処理する
|
||||||
|
loop@ for (locale in Locale.getAvailableLocales().sortedBy { it.toLanguageTag() }) {
|
||||||
|
// 言語名が空やundはスキップ
|
||||||
|
if (locale.language.isEmpty() || locale.language == "und") continue
|
||||||
|
|
||||||
|
// 既に対応するmastodon言語コードが判明している
|
||||||
|
if (dst.values.any { c -> c.locales.any { it == locale } }) continue
|
||||||
|
|
||||||
|
// language名またはlanguage tagで検索
|
||||||
|
for (code in arrayOf(locale.language, locale.toLanguageTag())) {
|
||||||
|
val item = dst.values.find { it.code.equals(code, ignoreCase = true) }
|
||||||
|
if (item != null) {
|
||||||
|
item.locales.add(locale)
|
||||||
|
continue@loop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// log.w("Java locale not match to mastodon lang list. ${locale.displayLanguage}(${locale.language}, ${locale.toLanguageTag()})")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 互換性があるなかで最も言語タグが短いロケールの表示名を使う
|
||||||
|
// fallback 1: 言語コード別の文字列リソースを参照する
|
||||||
|
// fallback 2: mastodon api のフォールバック用のname
|
||||||
|
// fallback 3: "?"
|
||||||
|
for (item in dst.values) {
|
||||||
|
val locale = item.locales.sortedBy { it.toLanguageTag() }.firstOrNull()
|
||||||
|
item.displayName = locale?.displayName?.notBlank()
|
||||||
|
?: languageStringIds[item.code]?.let { context.getString(it) }
|
||||||
|
?: item.name.notBlank()
|
||||||
|
?: "?"
|
||||||
|
}
|
||||||
|
|
||||||
|
return dst
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* - Mastodonの言語リストをロードする。なければフォールバックのrawリソースを読む。
|
||||||
|
* - 言語リストを
|
||||||
|
* 現在の表示言語にあう言語コード→名前マップを返す
|
||||||
|
*/
|
||||||
|
fun getLanguageNames(
|
||||||
|
context: Context,
|
||||||
|
jsonString: String?,
|
||||||
|
): HashMap<String, LanguageInfo> {
|
||||||
|
if (jsonString?.contains("{") == true) {
|
||||||
|
try {
|
||||||
|
val map = mergeLanguageName(
|
||||||
|
context,
|
||||||
|
jsonString,
|
||||||
|
)
|
||||||
|
if (map.isEmpty()) error("map is empty. (1)")
|
||||||
|
return map
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "loadMastodonLanguages failed.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// fallback
|
||||||
|
try {
|
||||||
|
val map = mergeLanguageName(
|
||||||
|
context,
|
||||||
|
context.loadRawResource(R.raw.languages_fallback)
|
||||||
|
.decodeToString()
|
||||||
|
)
|
||||||
|
if (map.isEmpty()) error("map is empty. (2)")
|
||||||
|
return map
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
log.e(ex, "loadRawResource failed.")
|
||||||
|
}
|
||||||
|
// error
|
||||||
|
return HashMap()
|
||||||
|
}
|
|
@ -0,0 +1,20 @@
|
||||||
|
package jp.juggler.subwaytooter.ui.languageFilter
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.annotation.StringRes
|
||||||
|
|
||||||
|
class StringResAndArgs(
|
||||||
|
@StringRes val stringId: Int,
|
||||||
|
// composeのstringResource ではnullを受け付けなくなった
|
||||||
|
vararg val args: Any,
|
||||||
|
) {
|
||||||
|
fun toCharSequence(context: Context) = when {
|
||||||
|
args.isNotEmpty() -> context.getString(stringId, *args)
|
||||||
|
else -> context.getText(stringId)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toString(context: Context) = when {
|
||||||
|
args.isNotEmpty() -> context.getString(stringId, *args)
|
||||||
|
else -> context.getString(stringId)
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,6 +2,7 @@ package jp.juggler.subwaytooter.util
|
||||||
|
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.compose.material3.ColorScheme
|
import androidx.compose.material3.ColorScheme
|
||||||
import androidx.compose.material3.darkColorScheme
|
import androidx.compose.material3.darkColorScheme
|
||||||
import androidx.compose.material3.lightColorScheme
|
import androidx.compose.material3.lightColorScheme
|
||||||
|
@ -14,6 +15,10 @@ import jp.juggler.util.ui.resColor
|
||||||
class StColorScheme(
|
class StColorScheme(
|
||||||
val materialColorScheme: ColorScheme,
|
val materialColorScheme: ColorScheme,
|
||||||
val colorTextLink: Color,
|
val colorTextLink: Color,
|
||||||
|
val colorTextContent: Color,
|
||||||
|
val colorTextError: Color,
|
||||||
|
val colorProgressBackground:Color,
|
||||||
|
val colorDivider: Color,
|
||||||
)
|
)
|
||||||
|
|
||||||
fun Context.createStColorSchemeLight(): StColorScheme {
|
fun Context.createStColorSchemeLight(): StColorScheme {
|
||||||
|
@ -33,13 +38,16 @@ fun Context.createStColorSchemeLight(): StColorScheme {
|
||||||
secondary = colorTextLink,
|
secondary = colorTextLink,
|
||||||
onSecondary = Color.White,
|
onSecondary = Color.White,
|
||||||
|
|
||||||
surface = Color(resColor(R.color.Light_colorColumnSettingBackground)),
|
surface = Color(resColor(R.color.Light_colorReplyBackground)),
|
||||||
onSurface = colorTextContent,
|
onSurface = colorTextContent,
|
||||||
|
onSurfaceVariant = colorTextContent,
|
||||||
onTertiary = colorTextContent,
|
onTertiary = colorTextContent,
|
||||||
|
|
||||||
onSurfaceVariant = Color(resColor(R.color.Light_colorTextHint)),
|
|
||||||
),
|
),
|
||||||
colorTextLink = colorTextLink,
|
colorTextLink = colorTextLink,
|
||||||
|
colorTextContent = colorTextContent,
|
||||||
|
colorTextError = colorTextError,
|
||||||
|
colorProgressBackground = Color(0xC0000000),
|
||||||
|
colorDivider = Color(resColor(R.color.Light_colorTextDivider)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -61,13 +69,16 @@ fun Context.createStColorSchemeDark(): StColorScheme {
|
||||||
secondary = colorTextLink,
|
secondary = colorTextLink,
|
||||||
onSecondary = colorTextContent,
|
onSecondary = colorTextContent,
|
||||||
|
|
||||||
surface = Color(resColor(R.color.Dark_colorColumnSettingBackground)),
|
surface = Color(resColor(R.color.Dark_colorColumnHeader)),
|
||||||
onSurface = colorTextContent,
|
onSurface = colorTextContent,
|
||||||
|
onSurfaceVariant = colorTextContent,
|
||||||
onTertiary = colorTextContent,
|
onTertiary = colorTextContent,
|
||||||
|
|
||||||
onSurfaceVariant = Color(resColor(R.color.Dark_colorTextHint)),
|
|
||||||
),
|
),
|
||||||
colorTextLink = colorTextLink,
|
colorTextLink = colorTextLink,
|
||||||
|
colorTextContent = colorTextContent,
|
||||||
|
colorTextError = colorTextError,
|
||||||
|
colorProgressBackground = Color(0xC0000000),
|
||||||
|
colorDivider = Color(resColor(R.color.Dark_colorSettingDivider)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -89,13 +100,16 @@ fun Context.createStColorSchemeMastodonDark(): StColorScheme {
|
||||||
secondary = colorTextLink,
|
secondary = colorTextLink,
|
||||||
onSecondary = colorTextContent,
|
onSecondary = colorTextContent,
|
||||||
|
|
||||||
surface = Color(resColor(R.color.Mastodon_colorColumnSettingBackground)),
|
surface = Color(resColor(R.color.Mastodon_colorColumnHeader)),
|
||||||
onSurface = colorTextContent,
|
onSurface = colorTextContent,
|
||||||
|
onSurfaceVariant = colorTextContent,
|
||||||
onTertiary = colorTextContent,
|
onTertiary = colorTextContent,
|
||||||
|
|
||||||
onSurfaceVariant = Color(resColor(R.color.Mastodon_colorTextHint)),
|
|
||||||
),
|
),
|
||||||
colorTextLink = colorTextLink,
|
colorTextLink = colorTextLink,
|
||||||
|
colorTextContent = colorTextContent,
|
||||||
|
colorTextError = colorTextError,
|
||||||
|
colorProgressBackground = Color(0xC0000000),
|
||||||
|
colorDivider = Color(resColor(R.color.Mastodon_colorSettingDivider)),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,5 +126,12 @@ fun Activity.getStColorTheme(forceDark: Boolean = false): StColorScheme {
|
||||||
|
|
||||||
fun dummyStColorTheme() = StColorScheme(
|
fun dummyStColorTheme() = StColorScheme(
|
||||||
materialColorScheme = darkColorScheme(),
|
materialColorScheme = darkColorScheme(),
|
||||||
colorTextLink = Color.Cyan,
|
colorTextContent = Color.White,
|
||||||
|
colorTextLink = Color(0xff0080ff),
|
||||||
|
colorTextError = Color.Red,
|
||||||
|
colorProgressBackground = Color(0x80808080),
|
||||||
|
colorDivider = Color(0x80808080),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
fun ComponentActivity.fireBackPressed() =
|
||||||
|
onBackPressedDispatcher.onBackPressed()
|
||||||
|
|
|
@ -1,80 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="match_parent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<androidx.appcompat.widget.Toolbar
|
|
||||||
android:id="@+id/toolbar"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="?attr/actionBarSize"
|
|
||||||
android:background="@drawable/action_bar_bg"
|
|
||||||
android:elevation="4dp"
|
|
||||||
app:navigationIcon="?attr/homeAsUpIndicator" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:id="@+id/llContent"
|
|
||||||
android:orientation="vertical">
|
|
||||||
|
|
||||||
<ListView
|
|
||||||
android:id="@+id/listView"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="0dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:fadeScrollbars="false"
|
|
||||||
tools:ignore="NestedWeights" />
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="?attr/colorStatusButtonsPopupBg"
|
|
||||||
android:paddingBottom="6dp"
|
|
||||||
android:paddingEnd="12dp"
|
|
||||||
android:paddingStart="12dp"
|
|
||||||
android:paddingTop="6dp">
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btnAdd"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:contentDescription="@string/add"
|
|
||||||
android:elevation="3dp"
|
|
||||||
android:src="@drawable/ic_add"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<Button
|
|
||||||
android:id="@+id/btnSave"
|
|
||||||
android:layout_width="0dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:layout_weight="1"
|
|
||||||
android:elevation="3dp"
|
|
||||||
android:text="@string/save"
|
|
||||||
android:textAllCaps="false" />
|
|
||||||
|
|
||||||
<ImageButton
|
|
||||||
android:id="@+id/btnMore"
|
|
||||||
android:layout_width="48dp"
|
|
||||||
android:layout_height="48dp"
|
|
||||||
android:contentDescription="@string/more"
|
|
||||||
android:elevation="3dp"
|
|
||||||
android:src="@drawable/ic_more" />
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingBottom="6dp"
|
|
||||||
android:paddingEnd="12dp"
|
|
||||||
android:paddingStart="12dp"
|
|
||||||
android:text="@string/language_filter_description"
|
|
||||||
android:textSize="12sp"
|
|
||||||
/>
|
|
||||||
</LinearLayout>
|
|
||||||
</LinearLayout>
|
|
|
@ -2,6 +2,7 @@
|
||||||
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
android:layout_width="300dp"
|
android:layout_width="300dp"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
android:orientation="vertical">
|
android:orientation="vertical">
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
|
@ -33,6 +34,7 @@
|
||||||
android:elevation="3dp"
|
android:elevation="3dp"
|
||||||
android:src="@drawable/ic_edit"
|
android:src="@drawable/ic_edit"
|
||||||
android:id="@+id/btnPresets"
|
android:id="@+id/btnPresets"
|
||||||
|
app:tint="?attr/colorTextContent"
|
||||||
android:contentDescription="@string/presets"
|
android:contentDescription="@string/presets"
|
||||||
/>
|
/>
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
|
@ -1,10 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<TextView
|
|
||||||
android:id="@android:id/text1"
|
|
||||||
style="?android:attr/spinnerDropDownItemStyle"
|
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:minHeight="48dp"
|
|
||||||
android:background="@drawable/btn_bg_transparent_round6dp"
|
|
||||||
/>
|
|
File diff suppressed because one or more lines are too long
|
@ -1293,4 +1293,7 @@
|
||||||
<string name="post_404_desc">(もしくは、フォロー限定投稿にフォロー外から返信しようとしました)</string>
|
<string name="post_404_desc">(もしくは、フォロー限定投稿にフォロー外から返信しようとしました)</string>
|
||||||
<string name="rearrange">並べ替え</string>
|
<string name="rearrange">並べ替え</string>
|
||||||
<string name="attachment_rearrange_desc">ドラッグで並べ替え</string>
|
<string name="attachment_rearrange_desc">ドラッグで並べ替え</string>
|
||||||
|
<string name="language_filter_importing">言語フィルタの読み込み中です…</string>
|
||||||
|
<string name="language_filter_loading">利用可能な言語の取得中…</string>
|
||||||
|
<string name="language_name_montenegrin">モンテネグロ語</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
|
@ -1305,4 +1305,7 @@
|
||||||
<string name="post_404_desc">(Or, you reply to follower-only post from other account)</string>
|
<string name="post_404_desc">(Or, you reply to follower-only post from other account)</string>
|
||||||
<string name="rearrange">rearrange</string>
|
<string name="rearrange">rearrange</string>
|
||||||
<string name="attachment_rearrange_desc">Drag to rearrange.</string>
|
<string name="attachment_rearrange_desc">Drag to rearrange.</string>
|
||||||
|
<string name="language_filter_importing">Importing language filter…</string>
|
||||||
|
<string name="language_filter_loading">Loading available languages…</string>
|
||||||
|
<string name="language_name_montenegrin">Montenegrin</string>
|
||||||
</resources>
|
</resources>
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -105,6 +105,10 @@ dependencies {
|
||||||
// HttpUtils で使う
|
// HttpUtils で使う
|
||||||
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
|
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
|
||||||
|
|
||||||
|
// ないとなぜかIDE上にエラーが出る
|
||||||
|
implementation("androidx.activity:activity-ktx:${Vers.androidxActivity}")
|
||||||
|
|
||||||
|
|
||||||
// ==========================================================================
|
// ==========================================================================
|
||||||
// 単体テスト
|
// 単体テスト
|
||||||
testImplementation(kotlin("test"))
|
testImplementation(kotlin("test"))
|
||||||
|
|
|
@ -11,6 +11,7 @@ import android.content.pm.ResolveInfo
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.OnBackPressedCallback
|
import androidx.activity.OnBackPressedCallback
|
||||||
import androidx.annotation.AnimRes
|
import androidx.annotation.AnimRes
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
@ -138,6 +139,12 @@ fun AppCompatActivity.backPressed(block: () -> Unit) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ComponentActivity.backPressed(block: () -> Unit) {
|
||||||
|
onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
|
||||||
|
override fun handleOnBackPressed() = block()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// 型推論できる文脈だと型名を書かずにすむ
|
// 型推論できる文脈だと型名を書かずにすむ
|
||||||
inline fun <reified T> systemService(context: Context): T? =
|
inline fun <reified T> systemService(context: Context): T? =
|
||||||
/* ContextCompat. */ ContextCompat.getSystemService(context, T::class.java)
|
/* ContextCompat. */ ContextCompat.getSystemService(context, T::class.java)
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
package jp.juggler.util.coroutine
|
package jp.juggler.util.coroutine
|
||||||
|
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import jp.juggler.util.log.LogCategory
|
import jp.juggler.util.log.LogCategory
|
||||||
|
@ -81,6 +82,25 @@ fun AppCompatActivity.launchAndShowError(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
fun ComponentActivity.launchAndShowError(
|
||||||
|
errorCaption: String? = null,
|
||||||
|
block: suspend CoroutineScope.() -> Unit,
|
||||||
|
): Job = lifecycleScope.launch {
|
||||||
|
try {
|
||||||
|
block()
|
||||||
|
} catch (ex: Throwable) {
|
||||||
|
when (ex) {
|
||||||
|
is CancellationException -> {
|
||||||
|
log.w(errorCaption ?: "launchAndShowError cancelled.")
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
log.e(ex, errorCaption ?: "launchAndShowError failed.")
|
||||||
|
showError(ex, errorCaption)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/////////////////////////////////////////////////////////////////////////
|
/////////////////////////////////////////////////////////////////////////
|
||||||
|
|
||||||
|
|
|
@ -242,9 +242,8 @@ fun getStreamSize(bClose: Boolean, inStream: InputStream): Long {
|
||||||
// }
|
// }
|
||||||
//}
|
//}
|
||||||
|
|
||||||
fun Context.loadRawResource(@RawRes resId: Int): ByteArray {
|
fun Context.loadRawResource(@RawRes resId: Int): ByteArray =
|
||||||
return resources.openRawResource(resId).use { it.readBytes() }
|
resources.openRawResource(resId).use { it.readBytes() }
|
||||||
}
|
|
||||||
|
|
||||||
fun intentOpenDocument(mimeType: String): Intent {
|
fun intentOpenDocument(mimeType: String): Intent {
|
||||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.util.ui
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.result.ActivityResult
|
import androidx.activity.result.ActivityResult
|
||||||
import androidx.activity.result.ActivityResultLauncher
|
import androidx.activity.result.ActivityResultLauncher
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
@ -18,7 +19,7 @@ class ActivityResultHandler(
|
||||||
private var launcher: ActivityResultLauncher<Intent>? = null
|
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||||
private var getContext: (() -> Context?)? = null
|
private var getContext: (() -> Context?)? = null
|
||||||
|
|
||||||
private val context
|
val context
|
||||||
get() = getContext?.invoke()
|
get() = getContext?.invoke()
|
||||||
|
|
||||||
// startForActivityResultの代わりに呼び出す
|
// startForActivityResultの代わりに呼び出す
|
||||||
|
@ -37,6 +38,13 @@ class ActivityResultHandler(
|
||||||
ActivityResultContracts.StartActivityForResult()
|
ActivityResultContracts.StartActivityForResult()
|
||||||
) { callback(it) }
|
) { callback(it) }
|
||||||
}
|
}
|
||||||
|
// onCreate時に呼び出す
|
||||||
|
fun register(a: ComponentActivity) {
|
||||||
|
getContext = { a.applicationContext }
|
||||||
|
this.launcher = a.registerForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { callback(it) }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
|
fun Intent.launch(ar: ActivityResultHandler) = ar.launch(this)
|
||||||
|
|
|
@ -35,6 +35,7 @@ object Vers {
|
||||||
const val androidxArchCoreTesting = "2.2.0"
|
const val androidxArchCoreTesting = "2.2.0"
|
||||||
const val androidxComposeRuntime = "1.6.3"
|
const val androidxComposeRuntime = "1.6.3"
|
||||||
const val androidxComposeUi = "1.6.3"
|
const val androidxComposeUi = "1.6.3"
|
||||||
|
const val androidxComposeMaterialIcons = "1.6.3"
|
||||||
const val androidxCore = "1.12.0"
|
const val androidxCore = "1.12.0"
|
||||||
const val androidxEmoji2 = "1.4.0"
|
const val androidxEmoji2 = "1.4.0"
|
||||||
const val androidxLifecycle = "2.7.0"
|
const val androidxLifecycle = "2.7.0"
|
||||||
|
|
Loading…
Reference in New Issue