fix:修复词典查询弹窗问题
This commit is contained in:
10 files changed
+351
-407
No files matched your search
@@ -263,6 +263,11 @@
|
|||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:configChanges="orientation|screenSize"
|
android:configChanges="orientation|screenSize"
|
||||||
android:hardwareAccelerated="true" />
|
android:hardwareAccelerated="true" />
|
||||||
|
<activity
|
||||||
|
android:name=".ui.dict.DictActivity"
|
||||||
|
android:enableOnBackInvokedCallback="true"
|
||||||
|
android:configChanges="orientation|screenSize"
|
||||||
|
android:hardwareAccelerated="true" />
|
||||||
<!-- 文件管理 -->
|
<!-- 文件管理 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.file.FileManageActivity"
|
android:name=".ui.file.FileManageActivity"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import io.legado.app.ui.book.read.sheet.ChangeChapterSourceSheet
|
|||||||
import io.legado.app.ui.book.read.sheet.CharsetConfigSheet
|
import io.legado.app.ui.book.read.sheet.CharsetConfigSheet
|
||||||
import io.legado.app.ui.book.read.sheet.ClickActionConfigSheet
|
import io.legado.app.ui.book.read.sheet.ClickActionConfigSheet
|
||||||
import io.legado.app.ui.book.read.sheet.ContentEditSheet
|
import io.legado.app.ui.book.read.sheet.ContentEditSheet
|
||||||
import io.legado.app.ui.book.read.sheet.DictSheet
|
import io.legado.app.ui.dict.DictSheet
|
||||||
import io.legado.app.ui.book.read.sheet.DownloadSheet
|
import io.legado.app.ui.book.read.sheet.DownloadSheet
|
||||||
import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
|
import io.legado.app.ui.book.read.sheet.EffectiveReplacesSheet
|
||||||
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
|
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
|
||||||
|
|||||||
@@ -1,170 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.sheet
|
|
||||||
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.material3.CircularProgressIndicator
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.PrimaryScrollableTabRow
|
|
||||||
import androidx.compose.material3.Tab
|
|
||||||
import androidx.compose.material3.Text
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.data.entities.DictRule
|
|
||||||
import io.legado.app.help.GlideImageGetter
|
|
||||||
import io.legado.app.ui.dict.DictViewModel
|
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
||||||
import io.legado.app.utils.setHtml
|
|
||||||
import org.koin.androidx.compose.koinViewModel
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun DictSheet(
|
|
||||||
show: Boolean,
|
|
||||||
word: String,
|
|
||||||
onDismissRequest: () -> Unit,
|
|
||||||
viewModel: DictViewModel = koinViewModel(),
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var dictRules by remember { mutableStateOf<List<DictRule>>(emptyList()) }
|
|
||||||
var selectedTab by remember { mutableIntStateOf(0) }
|
|
||||||
var isLoading by remember { mutableStateOf(true) }
|
|
||||||
var htmlContent by remember { mutableStateOf("") }
|
|
||||||
var emptyMessage by remember { mutableStateOf<String?>(null) }
|
|
||||||
var glideImageGetter by remember { mutableStateOf<GlideImageGetter?>(null) }
|
|
||||||
|
|
||||||
LaunchedEffect(show) {
|
|
||||||
if (!show) return@LaunchedEffect
|
|
||||||
viewModel.initData { rules ->
|
|
||||||
dictRules = rules
|
|
||||||
if (rules.isEmpty()) {
|
|
||||||
emptyMessage = context.getString(R.string.empty)
|
|
||||||
isLoading = false
|
|
||||||
} else {
|
|
||||||
// Auto-select first tab
|
|
||||||
viewModel.dict(rules[0], word) { result ->
|
|
||||||
isLoading = false
|
|
||||||
if (result.isBlank()) {
|
|
||||||
emptyMessage = "没有查询到结果"
|
|
||||||
} else {
|
|
||||||
htmlContent = result
|
|
||||||
emptyMessage = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppModalBottomSheet(
|
|
||||||
show = show,
|
|
||||||
onDismissRequest = {
|
|
||||||
glideImageGetter?.clear()
|
|
||||||
glideImageGetter = null
|
|
||||||
onDismissRequest()
|
|
||||||
},
|
|
||||||
title = word,
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 16.dp),
|
|
||||||
) {
|
|
||||||
// Tab row
|
|
||||||
if (dictRules.size > 1) {
|
|
||||||
PrimaryScrollableTabRow(
|
|
||||||
selectedTabIndex = selectedTab.coerceIn(
|
|
||||||
0,
|
|
||||||
(dictRules.size - 1).coerceAtLeast(0)
|
|
||||||
),
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
edgePadding = 0.dp,
|
|
||||||
) {
|
|
||||||
dictRules.forEachIndexed { index, rule ->
|
|
||||||
Tab(
|
|
||||||
selected = selectedTab == index,
|
|
||||||
onClick = {
|
|
||||||
selectedTab = index
|
|
||||||
isLoading = true
|
|
||||||
emptyMessage = null
|
|
||||||
viewModel.dict(rule, word) { result ->
|
|
||||||
isLoading = false
|
|
||||||
if (result.isBlank()) {
|
|
||||||
emptyMessage = "没有查询到结果"
|
|
||||||
} else {
|
|
||||||
htmlContent = result
|
|
||||||
emptyMessage = null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
text = { Text(rule.name) },
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content
|
|
||||||
when {
|
|
||||||
isLoading -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.height(200.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
CircularProgressIndicator()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
emptyMessage != null -> {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(32.dp),
|
|
||||||
contentAlignment = Alignment.Center,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
text = emptyMessage!!,
|
|
||||||
style = MaterialTheme.typography.bodyMedium,
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
AndroidView(
|
|
||||||
factory = { ctx ->
|
|
||||||
TextView(ctx).apply {
|
|
||||||
movementMethod = LinkMovementMethod()
|
|
||||||
setPadding(32, 16, 32, 16)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
update = { textView ->
|
|
||||||
textView.setHtml(htmlContent)
|
|
||||||
glideImageGetter?.clear()
|
|
||||||
glideImageGetter =
|
|
||||||
GlideImageGetter.create(context, textView, htmlContent)
|
|
||||||
textView.setHtml(htmlContent, glideImageGetter)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package io.legado.app.ui.dict
|
||||||
|
|
||||||
|
import android.app.Activity
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import io.legado.app.base.BaseComposeActivity
|
||||||
|
|
||||||
|
class DictActivity : BaseComposeActivity() {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
DictSheet(
|
||||||
|
show = true,
|
||||||
|
word = intent.getStringExtra(EXTRA_WORD).orEmpty(),
|
||||||
|
onDismissRequest = { finish() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private const val EXTRA_WORD = "word"
|
||||||
|
|
||||||
|
fun startIntent(context: Context, word: String): Intent {
|
||||||
|
return Intent(context, DictActivity::class.java)
|
||||||
|
.putExtra(EXTRA_WORD, word)
|
||||||
|
.apply {
|
||||||
|
if (context !is Activity) {
|
||||||
|
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.legado.app.ui.dict
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class DictUiState(
|
||||||
|
val word: String = "",
|
||||||
|
val isLoading: Boolean = false,
|
||||||
|
val rules: ImmutableList<DictRuleUi> = persistentListOf(),
|
||||||
|
val selectedIndex: Int = 0,
|
||||||
|
val htmlContent: String = "",
|
||||||
|
val emptyReason: DictEmptyReason? = null,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class DictRuleUi(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface DictEmptyReason {
|
||||||
|
data object BlankWord : DictEmptyReason
|
||||||
|
data object NoRules : DictEmptyReason
|
||||||
|
data object NoResult : DictEmptyReason
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface DictIntent {
|
||||||
|
data class Load(val word: String) : DictIntent
|
||||||
|
data class SelectRule(val index: Int) : DictIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface DictEffect {
|
||||||
|
data class ShowToast(val message: String) : DictEffect
|
||||||
|
}
|
||||||
@@ -1,123 +0,0 @@
|
|||||||
package io.legado.app.ui.dict
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.text.method.LinkMovementMethod
|
|
||||||
import android.view.View
|
|
||||||
import com.google.android.material.tabs.TabLayout
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.data.entities.DictRule
|
|
||||||
import io.legado.app.databinding.DialogDictBinding
|
|
||||||
import io.legado.app.help.GlideImageGetter
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.setHtml
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 词典
|
|
||||||
*/
|
|
||||||
class DictDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_dict) {
|
|
||||||
|
|
||||||
constructor(word: String) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString("word", word)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val viewModel by viewModel<DictViewModel>()
|
|
||||||
private val binding by viewBinding(DialogDictBinding::bind)
|
|
||||||
private var word: String? = null
|
|
||||||
private var glideImageGetter: GlideImageGetter? = null
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.tvDict.movementMethod = LinkMovementMethod()
|
|
||||||
word = arguments?.getString("word")
|
|
||||||
if (word.isNullOrEmpty()) {
|
|
||||||
toastOnUi(R.string.cannot_empty)
|
|
||||||
dismiss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setupTabs()
|
|
||||||
observeDicts()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroyView() {
|
|
||||||
glideImageGetter?.clear()
|
|
||||||
glideImageGetter = null
|
|
||||||
super.onDestroyView()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupTabs() {
|
|
||||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
|
||||||
override fun onTabReselected(tab: TabLayout.Tab) = Unit
|
|
||||||
override fun onTabUnselected(tab: TabLayout.Tab) = Unit
|
|
||||||
|
|
||||||
override fun onTabSelected(tab: TabLayout.Tab) {
|
|
||||||
val dictRule = tab.tag as DictRule
|
|
||||||
loadDict(dictRule)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun observeDicts() {
|
|
||||||
viewModel.initData { dictRules ->
|
|
||||||
if (dictRules.isEmpty()) {
|
|
||||||
showEmptyView("暂无可用词典")
|
|
||||||
return@initData
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.emptyMessageView.gone()
|
|
||||||
dictRules.forEach {
|
|
||||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
|
||||||
text = it.name
|
|
||||||
tag = it
|
|
||||||
})
|
|
||||||
}
|
|
||||||
setupTabLayoutMode(dictRules.size)
|
|
||||||
|
|
||||||
|
|
||||||
binding.tabLayout.getTabAt(0)?.select()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadDict(dictRule: DictRule) {
|
|
||||||
|
|
||||||
binding.tvDict.gone()
|
|
||||||
binding.emptyMessageView.gone()
|
|
||||||
binding.rotateLoading.visible()
|
|
||||||
|
|
||||||
viewModel.dict(dictRule, word!!) { result ->
|
|
||||||
binding.rotateLoading.gone()
|
|
||||||
if (result.isBlank()) {
|
|
||||||
showEmptyView("没有查询到结果")
|
|
||||||
} else {
|
|
||||||
binding.tvDict.visible()
|
|
||||||
binding.tvDict.setHtml(result)
|
|
||||||
glideImageGetter?.clear()
|
|
||||||
glideImageGetter = GlideImageGetter.create(requireContext(), binding.tvDict, result)
|
|
||||||
binding.tvDict.setHtml(result, glideImageGetter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setupTabLayoutMode(dictCount: Int) {
|
|
||||||
if (dictCount <= 4) {
|
|
||||||
binding.tabLayout.tabMode = TabLayout.MODE_FIXED
|
|
||||||
binding.tabLayout.tabGravity = TabLayout.GRAVITY_FILL
|
|
||||||
} else {
|
|
||||||
binding.tabLayout.tabMode = TabLayout.MODE_SCROLLABLE
|
|
||||||
binding.tabLayout.tabGravity = TabLayout.GRAVITY_CENTER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showEmptyView(message: String) {
|
|
||||||
binding.tvDict.gone()
|
|
||||||
binding.rotateLoading.gone()
|
|
||||||
binding.emptyMessageView.visible()
|
|
||||||
binding.emptyMessageView.setMessage(message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package io.legado.app.ui.dict
|
||||||
|
|
||||||
|
import android.text.method.LinkMovementMethod
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.help.GlideImageGetter
|
||||||
|
import io.legado.app.ui.widget.components.EmptyMessage
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.progressIndicator.AppCircularProgressIndicator
|
||||||
|
import io.legado.app.ui.widget.components.tabRow.AppTabRow
|
||||||
|
import io.legado.app.utils.setHtml
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import org.koin.androidx.compose.koinViewModel
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun DictSheet(
|
||||||
|
show: Boolean,
|
||||||
|
word: String,
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
viewModel: DictViewModel = koinViewModel(),
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
|
||||||
|
LaunchedEffect(show, word) {
|
||||||
|
if (show) {
|
||||||
|
viewModel.onIntent(DictIntent.Load(word))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = word,
|
||||||
|
) {
|
||||||
|
DictSheetContent(
|
||||||
|
state = state,
|
||||||
|
onIntent = viewModel::onIntent,
|
||||||
|
effects = viewModel.effects,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DictSheetContent(
|
||||||
|
state: DictUiState,
|
||||||
|
onIntent: (DictIntent) -> Unit,
|
||||||
|
effects: Flow<DictEffect>,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
LaunchedEffect(effects) {
|
||||||
|
effects.collect { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is DictEffect.ShowToast -> Unit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DictContent(
|
||||||
|
state = state,
|
||||||
|
onIntent = onIntent,
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DictContent(
|
||||||
|
state: DictUiState,
|
||||||
|
onIntent: (DictIntent) -> Unit,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val blankWordText = stringResource(R.string.cannot_empty)
|
||||||
|
val emptyText = stringResource(R.string.empty)
|
||||||
|
val searchEmptyText = stringResource(R.string.search_empty)
|
||||||
|
val tabTitles = remember(state.rules) { state.rules.map { it.name } }
|
||||||
|
val emptyMessage = when (state.emptyReason) {
|
||||||
|
DictEmptyReason.BlankWord -> blankWordText
|
||||||
|
DictEmptyReason.NoRules -> emptyText
|
||||||
|
DictEmptyReason.NoResult -> searchEmptyText
|
||||||
|
null -> null
|
||||||
|
}
|
||||||
|
|
||||||
|
Column(modifier = modifier) {
|
||||||
|
if (state.rules.size > 1) {
|
||||||
|
AppTabRow(
|
||||||
|
tabTitles = tabTitles,
|
||||||
|
selectedTabIndex = state.selectedIndex.coerceIn(0, state.rules.lastIndex),
|
||||||
|
onTabSelected = { index ->
|
||||||
|
onIntent(DictIntent.SelectRule(index))
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
isScrollable = true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
when {
|
||||||
|
state.isLoading -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(200.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
AppCircularProgressIndicator()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
emptyMessage != null -> {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(32.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
) {
|
||||||
|
EmptyMessage(message = emptyMessage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.htmlContent.isNotBlank() -> {
|
||||||
|
DictHtmlContent(
|
||||||
|
htmlContent = state.htmlContent,
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun DictHtmlContent(
|
||||||
|
htmlContent: String,
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var glideImageGetter by remember { mutableStateOf<GlideImageGetter?>(null) }
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
glideImageGetter?.clear()
|
||||||
|
glideImageGetter = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
TextView(ctx).apply {
|
||||||
|
movementMethod = LinkMovementMethod()
|
||||||
|
setPadding(32, 16, 32, 16)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update = { textView ->
|
||||||
|
glideImageGetter?.clear()
|
||||||
|
glideImageGetter = GlideImageGetter.create(context, textView, htmlContent)
|
||||||
|
textView.setHtml(htmlContent, glideImageGetter)
|
||||||
|
},
|
||||||
|
modifier = modifier,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,40 +1,112 @@
|
|||||||
package io.legado.app.ui.dict
|
package io.legado.app.ui.dict
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import io.legado.app.base.BaseViewModel
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
import io.legado.app.data.repository.DictRuleRepository
|
import io.legado.app.data.repository.DictRuleRepository
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
|
import kotlinx.coroutines.flow.asSharedFlow
|
||||||
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
class DictViewModel(
|
class DictViewModel(
|
||||||
application: Application,
|
|
||||||
private val dictRuleRepository: DictRuleRepository
|
private val dictRuleRepository: DictRuleRepository
|
||||||
) : BaseViewModel(application) {
|
) : ViewModel() {
|
||||||
|
|
||||||
private var dictJob: Coroutine<String>? = null
|
private val _uiState = MutableStateFlow(DictUiState())
|
||||||
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
fun initData(onSuccess: (List<DictRule>) -> Unit) {
|
private val _effects = MutableSharedFlow<DictEffect>(extraBufferCapacity = 16)
|
||||||
execute {
|
val effects = _effects.asSharedFlow()
|
||||||
dictRuleRepository.getEnabled()
|
|
||||||
}.onSuccess {
|
private var dictRules: List<DictRule> = emptyList()
|
||||||
onSuccess.invoke(it)
|
private var dictJob: Job? = null
|
||||||
|
|
||||||
|
fun onIntent(intent: DictIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is DictIntent.Load -> load(intent.word)
|
||||||
|
is DictIntent.SelectRule -> selectRule(intent.index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun dict(
|
private fun load(word: String) {
|
||||||
dictRule: DictRule,
|
|
||||||
word: String,
|
|
||||||
onFinally: (String) -> Unit
|
|
||||||
) {
|
|
||||||
dictJob?.cancel()
|
dictJob?.cancel()
|
||||||
dictJob = execute {
|
val query = word.trim()
|
||||||
dictRule.search(word)
|
_uiState.value = DictUiState(
|
||||||
}.onSuccess {
|
word = query,
|
||||||
onFinally.invoke(it)
|
isLoading = query.isNotBlank(),
|
||||||
}.onError {
|
emptyReason = if (query.isBlank()) DictEmptyReason.BlankWord else null,
|
||||||
onFinally.invoke(it.localizedMessage ?: "ERROR")
|
)
|
||||||
|
if (query.isBlank()) return
|
||||||
|
|
||||||
|
viewModelScope.launch {
|
||||||
|
dictRules = withContext(Dispatchers.IO) {
|
||||||
|
dictRuleRepository.getEnabled()
|
||||||
|
}
|
||||||
|
_uiState.update { state ->
|
||||||
|
state.copy(
|
||||||
|
rules = dictRules.map { DictRuleUi(it.name) }.toImmutableList(),
|
||||||
|
isLoading = dictRules.isNotEmpty(),
|
||||||
|
emptyReason = if (dictRules.isEmpty()) DictEmptyReason.NoRules else null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (dictRules.isNotEmpty()) {
|
||||||
|
searchRule(index = 0, word = query)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun selectRule(index: Int) {
|
||||||
|
val state = _uiState.value
|
||||||
|
if (index !in dictRules.indices || state.selectedIndex == index && state.isLoading) return
|
||||||
|
searchRule(index = index, word = state.word)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun searchRule(index: Int, word: String) {
|
||||||
|
dictJob?.cancel()
|
||||||
|
val rule = dictRules.getOrNull(index) ?: return
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
selectedIndex = index,
|
||||||
|
isLoading = true,
|
||||||
|
htmlContent = "",
|
||||||
|
emptyReason = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dictJob = viewModelScope.launch {
|
||||||
|
val result = runCatching {
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
rule.search(word)
|
||||||
|
}
|
||||||
|
}.getOrElse { error ->
|
||||||
|
error.localizedMessage ?: "ERROR"
|
||||||
|
}
|
||||||
|
_uiState.update {
|
||||||
|
if (result.isBlank()) {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
htmlContent = "",
|
||||||
|
emptyReason = DictEmptyReason.NoResult,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
htmlContent = result,
|
||||||
|
emptyReason = null,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCleared() {
|
||||||
|
dictJob?.cancel()
|
||||||
|
super.onCleared()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -10,14 +10,13 @@ import android.view.ViewGroup
|
|||||||
import android.webkit.JavascriptInterface
|
import android.webkit.JavascriptInterface
|
||||||
import android.webkit.WebView
|
import android.webkit.WebView
|
||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.DisposableEffect
|
import androidx.compose.runtime.DisposableEffect
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.ui.dict.DictDialog
|
import io.legado.app.ui.dict.DictActivity
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
|
||||||
@SuppressLint("SetJavaScriptEnabled")
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
@@ -83,7 +82,7 @@ class VisibleWebView(
|
|||||||
postDelayed({
|
postDelayed({
|
||||||
getSelectedText { selectedText ->
|
getSelectedText { selectedText ->
|
||||||
if (selectedText.isNotBlank()) {
|
if (selectedText.isNotBlank()) {
|
||||||
showDictDialog(selectedText)
|
showDictSheet(selectedText)
|
||||||
} else {
|
} else {
|
||||||
context.toastOnUi("未获取到选中文本,请重试")
|
context.toastOnUi("未获取到选中文本,请重试")
|
||||||
}
|
}
|
||||||
@@ -124,12 +123,8 @@ class VisibleWebView(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun showDictDialog(selectedText: String) {
|
private fun showDictSheet(selectedText: String) {
|
||||||
val activity = context as? AppCompatActivity ?: return
|
context.startActivity(DictActivity.startIntent(context, selectedText))
|
||||||
val dialog = DictDialog(selectedText)
|
|
||||||
activity.supportFragmentManager.beginTransaction()
|
|
||||||
.add(dialog, "DictDialog")
|
|
||||||
.commitAllowingStateLoss()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,82 +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"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:paddingBottom="16dp">
|
|
||||||
|
|
||||||
<LinearLayout
|
|
||||||
android:id="@+id/header_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:orientation="vertical"
|
|
||||||
android:gravity="center"
|
|
||||||
android:paddingHorizontal="16dp"
|
|
||||||
android:paddingBottom="8dp"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toTopOf="parent">
|
|
||||||
|
|
||||||
<com.google.android.material.bottomsheet.BottomSheetDragHandleView
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:paddingVertical="16dp"
|
|
||||||
android:minHeight="0dp"
|
|
||||||
android:layout_gravity="center_horizontal" />
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/bottom_sheet_title"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:text="@string/dict"
|
|
||||||
android:textSize="18sp"
|
|
||||||
android:textStyle="bold" />
|
|
||||||
</LinearLayout>
|
|
||||||
|
|
||||||
<com.google.android.material.tabs.TabLayout
|
|
||||||
android:id="@+id/tab_layout"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:background="@android:color/transparent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/header_layout" />
|
|
||||||
|
|
||||||
<com.google.android.material.loadingindicator.LoadingIndicator
|
|
||||||
android:id="@+id/rotate_loading"
|
|
||||||
style="@style/Widget.Material3.LoadingIndicator.Contained"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
android:layout_margin="32dp"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/tab_layout" />
|
|
||||||
|
|
||||||
<androidx.core.widget.NestedScrollView
|
|
||||||
android:id="@+id/scroll_container"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
app:layout_constraintBottom_toBottomOf="parent"
|
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
|
||||||
app:layout_constraintTop_toBottomOf="@id/tab_layout">
|
|
||||||
|
|
||||||
<TextView
|
|
||||||
android:id="@+id/tv_dict"
|
|
||||||
android:layout_width="match_parent"
|
|
||||||
android:layout_height="wrap_content" />
|
|
||||||
|
|
||||||
</androidx.core.widget.NestedScrollView>
|
|
||||||
|
|
||||||
<io.legado.app.ui.widget.EmptyMessageView
|
|
||||||
android:id="@+id/empty_message_view"
|
|
||||||
android:layout_width="wrap_content"
|
|
||||||
android:layout_height="wrap_content"
|
|
||||||
android:layout_gravity="center"
|
|
||||||
app:messageText="词典内容为空!"/>
|
|
||||||
|
|
||||||
</LinearLayout>
|
|
||||||
Reference in new issue
Block a user