453 lines
18 KiB
Kotlin
453 lines
18 KiB
Kotlin
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
|
|
}
|
|
)
|
|
}
|
|
}
|
|
}
|