言語フィルタ画面のリファクタ。 /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.browser:browser:1.8.0")
|
||||
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.ui:ui-tooling-preview:${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" />
|
||||
|
||||
<activity
|
||||
android:name=".ActLanguageFilter"
|
||||
android:name=".ui.languageFilter.LanguageFilterActivity"
|
||||
android:exported="false"
|
||||
android:label="@string/language_filter"
|
||||
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.MyClickableSpanHandler
|
||||
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.EmojiDecoder
|
||||
import jp.juggler.subwaytooter.util.openBrowser
|
||||
|
@ -309,11 +310,10 @@ class ActMain : AppCompatActivity(),
|
|||
}
|
||||
|
||||
val arLanguageFilter = ActivityResultHandler(log) { r ->
|
||||
if (r.isNotOk) return@ActivityResultHandler
|
||||
LanguageFilterActivity.decodeResult(r)?.let { columnIndex ->
|
||||
appState.saveColumnList()
|
||||
r.data?.int(ActLanguageFilter.EXTRA_COLUMN_INDEX)
|
||||
?.let { appState.column(it) }
|
||||
?.onLanguageFilterChanged()
|
||||
appState.column(columnIndex) ?.onLanguageFilterChanged()
|
||||
}
|
||||
}
|
||||
|
||||
val arNickname = ActivityResultHandler(log) { r ->
|
||||
|
|
|
@ -3,7 +3,6 @@ package jp.juggler.subwaytooter.columnviewholder
|
|||
import android.view.View
|
||||
import android.widget.CompoundButton
|
||||
import jp.juggler.subwaytooter.ActColumnCustomize
|
||||
import jp.juggler.subwaytooter.ActLanguageFilter
|
||||
import jp.juggler.subwaytooter.App1
|
||||
import jp.juggler.subwaytooter.R
|
||||
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.closeColumnAll
|
||||
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.withCaption
|
||||
import jp.juggler.util.ui.hideKeyboard
|
||||
|
@ -252,9 +257,9 @@ fun ColumnViewHolder.onClickImpl(v: View?) {
|
|||
|
||||
btnLanguageFilter ->
|
||||
activity.appState.columnIndex(column)?.let { colIdx ->
|
||||
|
||||
activity.arLanguageFilter.launch(
|
||||
ActLanguageFilter.createIntent(activity, colIdx)
|
||||
openLanguageFilterActivity(
|
||||
activity.arLanguageFilter,
|
||||
colIdx
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
package jp.juggler.subwaytooter.dialog
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.text.method.LinkMovementMethod
|
||||
import android.text.util.Linkify
|
||||
import android.view.View
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import jp.juggler.subwaytooter.R
|
||||
import jp.juggler.subwaytooter.databinding.DlgConfirmBinding
|
||||
import jp.juggler.util.ui.dismissSafe
|
||||
|
@ -16,57 +16,8 @@ import kotlin.coroutines.resume
|
|||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
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")
|
||||
suspend inline fun AppCompatActivity.confirm(
|
||||
suspend inline fun Activity.confirm(
|
||||
message: String,
|
||||
isConfirmEnabled: Boolean,
|
||||
setConfirmEnabled: (newConfirmEnabled: Boolean) -> Unit,
|
||||
|
@ -94,10 +45,10 @@ object DlgConfirm {
|
|||
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))
|
||||
|
||||
suspend fun AppCompatActivity.confirm(message: CharSequence, title: CharSequence? = null) {
|
||||
suspend fun Activity.confirm(message: CharSequence, title: CharSequence? = null) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
try {
|
||||
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))
|
||||
|
||||
suspend fun AppCompatActivity.okDialog(message: CharSequence, title: CharSequence? = null) {
|
||||
suspend fun Activity.okDialog(message: CharSequence, title: CharSequence? = null) {
|
||||
suspendCancellableCoroutine { cont ->
|
||||
try {
|
||||
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.content.Context
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.compose.material3.ColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
|
@ -14,6 +15,10 @@ import jp.juggler.util.ui.resColor
|
|||
class StColorScheme(
|
||||
val materialColorScheme: ColorScheme,
|
||||
val colorTextLink: Color,
|
||||
val colorTextContent: Color,
|
||||
val colorTextError: Color,
|
||||
val colorProgressBackground:Color,
|
||||
val colorDivider: Color,
|
||||
)
|
||||
|
||||
fun Context.createStColorSchemeLight(): StColorScheme {
|
||||
|
@ -33,13 +38,16 @@ fun Context.createStColorSchemeLight(): StColorScheme {
|
|||
secondary = colorTextLink,
|
||||
onSecondary = Color.White,
|
||||
|
||||
surface = Color(resColor(R.color.Light_colorColumnSettingBackground)),
|
||||
surface = Color(resColor(R.color.Light_colorReplyBackground)),
|
||||
onSurface = colorTextContent,
|
||||
onSurfaceVariant = colorTextContent,
|
||||
onTertiary = colorTextContent,
|
||||
|
||||
onSurfaceVariant = Color(resColor(R.color.Light_colorTextHint)),
|
||||
),
|
||||
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,
|
||||
onSecondary = colorTextContent,
|
||||
|
||||
surface = Color(resColor(R.color.Dark_colorColumnSettingBackground)),
|
||||
surface = Color(resColor(R.color.Dark_colorColumnHeader)),
|
||||
onSurface = colorTextContent,
|
||||
onSurfaceVariant = colorTextContent,
|
||||
onTertiary = colorTextContent,
|
||||
|
||||
onSurfaceVariant = Color(resColor(R.color.Dark_colorTextHint)),
|
||||
),
|
||||
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,
|
||||
onSecondary = colorTextContent,
|
||||
|
||||
surface = Color(resColor(R.color.Mastodon_colorColumnSettingBackground)),
|
||||
surface = Color(resColor(R.color.Mastodon_colorColumnHeader)),
|
||||
onSurface = colorTextContent,
|
||||
onSurfaceVariant = colorTextContent,
|
||||
onTertiary = colorTextContent,
|
||||
|
||||
onSurfaceVariant = Color(resColor(R.color.Mastodon_colorTextHint)),
|
||||
),
|
||||
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(
|
||||
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"
|
||||
android:layout_width="300dp"
|
||||
android:layout_height="wrap_content"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
android:orientation="vertical">
|
||||
|
||||
<TextView
|
||||
|
@ -33,6 +34,7 @@
|
|||
android:elevation="3dp"
|
||||
android:src="@drawable/ic_edit"
|
||||
android:id="@+id/btnPresets"
|
||||
app:tint="?attr/colorTextContent"
|
||||
android:contentDescription="@string/presets"
|
||||
/>
|
||||
</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="rearrange">並べ替え</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>
|
||||
|
|
|
@ -1305,4 +1305,7 @@
|
|||
<string name="post_404_desc">(Or, you reply to follower-only post from other account)</string>
|
||||
<string name="rearrange">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>
|
||||
|
|
File diff suppressed because one or more lines are too long
|
@ -105,6 +105,10 @@ dependencies {
|
|||
// HttpUtils で使う
|
||||
implementation("com.squareup.okhttp3:okhttp:${Vers.okhttpVersion}")
|
||||
|
||||
// ないとなぜかIDE上にエラーが出る
|
||||
implementation("androidx.activity:activity-ktx:${Vers.androidxActivity}")
|
||||
|
||||
|
||||
// ==========================================================================
|
||||
// 単体テスト
|
||||
testImplementation(kotlin("test"))
|
||||
|
|
|
@ -11,6 +11,7 @@ import android.content.pm.ResolveInfo
|
|||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.OnBackPressedCallback
|
||||
import androidx.annotation.AnimRes
|
||||
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? =
|
||||
/* ContextCompat. */ ContextCompat.getSystemService(context, T::class.java)
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
package jp.juggler.util.coroutine
|
||||
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
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 {
|
||||
return resources.openRawResource(resId).use { it.readBytes() }
|
||||
}
|
||||
fun Context.loadRawResource(@RawRes resId: Int): ByteArray =
|
||||
resources.openRawResource(resId).use { it.readBytes() }
|
||||
|
||||
fun intentOpenDocument(mimeType: String): Intent {
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT)
|
||||
|
|
|
@ -3,6 +3,7 @@ package jp.juggler.util.ui
|
|||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.result.ActivityResult
|
||||
import androidx.activity.result.ActivityResultLauncher
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
|
@ -18,7 +19,7 @@ class ActivityResultHandler(
|
|||
private var launcher: ActivityResultLauncher<Intent>? = null
|
||||
private var getContext: (() -> Context?)? = null
|
||||
|
||||
private val context
|
||||
val context
|
||||
get() = getContext?.invoke()
|
||||
|
||||
// startForActivityResultの代わりに呼び出す
|
||||
|
@ -37,6 +38,13 @@ class ActivityResultHandler(
|
|||
ActivityResultContracts.StartActivityForResult()
|
||||
) { 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)
|
||||
|
|
|
@ -35,6 +35,7 @@ object Vers {
|
|||
const val androidxArchCoreTesting = "2.2.0"
|
||||
const val androidxComposeRuntime = "1.6.3"
|
||||
const val androidxComposeUi = "1.6.3"
|
||||
const val androidxComposeMaterialIcons = "1.6.3"
|
||||
const val androidxCore = "1.12.0"
|
||||
const val androidxEmoji2 = "1.4.0"
|
||||
const val androidxLifecycle = "2.7.0"
|
||||
|
|
Loading…
Reference in New Issue