|
@@ -46,9 +46,9 @@ import io.legado.app.ui.replace.ReplaceEditRoute
|
|
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
|
|
import io.legado.app.utils.StartActivityContract
|
|
import io.legado.app.utils.StartActivityContract
|
|
|
import io.legado.app.utils.takePersistablePermissionSafely
|
|
import io.legado.app.utils.takePersistablePermissionSafely
|
|
|
-import kotlinx.coroutines.coroutineScope
|
|
|
|
|
|
|
+import kotlinx.coroutines.CompletableDeferred
|
|
|
|
|
+import kotlinx.coroutines.flow.onSubscription
|
|
|
import kotlinx.coroutines.launch
|
|
import kotlinx.coroutines.launch
|
|
|
-import kotlinx.coroutines.yield
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
data class ReadBookViewRefs(
|
|
data class ReadBookViewRefs(
|
|
@@ -233,130 +233,130 @@ fun ReadBookRouteScreen(
|
|
|
// ── Effect collection: route handles launcher effects, rest goes to bridge ──
|
|
// ── Effect collection: route handles launcher effects, rest goes to bridge ──
|
|
|
|
|
|
|
|
LaunchedEffect(viewModel) {
|
|
LaunchedEffect(viewModel) {
|
|
|
- coroutineScope {
|
|
|
|
|
- val collector = launch {
|
|
|
|
|
- viewModel.effects.collect { effect ->
|
|
|
|
|
- try {
|
|
|
|
|
- when (effect) {
|
|
|
|
|
- // Launcher-dependent effects — handled directly by route
|
|
|
|
|
- is ReadBookEffect.OpenChapterList -> {
|
|
|
|
|
- tocLauncher.launch(effect.bookUrl)
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenSourceEdit -> {
|
|
|
|
|
- sourceEditLauncher.launch { putExtra("sourceUrl", effect.sourceUrl) }
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenBookInfo -> {
|
|
|
|
|
- bookInfoLauncher.launch {
|
|
|
|
|
- putExtra("name", effect.name)
|
|
|
|
|
- putExtra("author", effect.author)
|
|
|
|
|
- putExtra("bookUrl", effect.bookUrl)
|
|
|
|
|
|
|
+ val effectsSubscribed = CompletableDeferred<Unit>()
|
|
|
|
|
+ launch {
|
|
|
|
|
+ viewModel.effects
|
|
|
|
|
+ .onSubscription { effectsSubscribed.complete(Unit) }
|
|
|
|
|
+ .collect { effect ->
|
|
|
|
|
+ try {
|
|
|
|
|
+ when (effect) {
|
|
|
|
|
+ // Launcher-dependent effects — handled directly by route
|
|
|
|
|
+ is ReadBookEffect.OpenChapterList -> {
|
|
|
|
|
+ tocLauncher.launch(effect.bookUrl)
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.ShowLogin -> {
|
|
|
|
|
- context.startActivity(
|
|
|
|
|
- Intent(context, SourceLoginActivity::class.java).apply {
|
|
|
|
|
- putExtra("type", "bookSource")
|
|
|
|
|
- putExtra("key", effect.sourceUrl)
|
|
|
|
|
- }
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenWebView -> {
|
|
|
|
|
- context.startActivity(
|
|
|
|
|
- Intent(context, WebViewActivity::class.java).apply {
|
|
|
|
|
- putExtra("title", effect.title)
|
|
|
|
|
- putExtra("url", effect.url)
|
|
|
|
|
- putExtra("sourceOrigin", effect.sourceOrigin)
|
|
|
|
|
- putExtra("sourceName", effect.sourceName)
|
|
|
|
|
- effect.sourceType?.let { putExtra("sourceType", it) }
|
|
|
|
|
|
|
+ is ReadBookEffect.OpenSourceEdit -> {
|
|
|
|
|
+ sourceEditLauncher.launch { putExtra("sourceUrl", effect.sourceUrl) }
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenBookInfo -> {
|
|
|
|
|
+ bookInfoLauncher.launch {
|
|
|
|
|
+ putExtra("name", effect.name)
|
|
|
|
|
+ putExtra("author", effect.author)
|
|
|
|
|
+ putExtra("bookUrl", effect.bookUrl)
|
|
|
}
|
|
}
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenSearchActivity -> {
|
|
|
|
|
- val currentState = viewModel.uiState.value
|
|
|
|
|
- val lambda: (Intent.() -> Unit)? = { intent ->
|
|
|
|
|
- intent.putExtra("bookUrl", effect.bookUrl)
|
|
|
|
|
- intent.putExtra("searchWord", effect.word)
|
|
|
|
|
- intent.putExtra("searchResultIndex", currentState.searchResultIndex)
|
|
|
|
|
- currentState.searchResultList.firstOrNull()?.let {
|
|
|
|
|
- if (it.query == currentState.searchContentQuery) {
|
|
|
|
|
- IntentData.put("searchResultList", currentState.searchResultList)
|
|
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.ShowLogin -> {
|
|
|
|
|
+ context.startActivity(
|
|
|
|
|
+ Intent(context, SourceLoginActivity::class.java).apply {
|
|
|
|
|
+ putExtra("type", "bookSource")
|
|
|
|
|
+ putExtra("key", effect.sourceUrl)
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenWebView -> {
|
|
|
|
|
+ context.startActivity(
|
|
|
|
|
+ Intent(context, WebViewActivity::class.java).apply {
|
|
|
|
|
+ putExtra("title", effect.title)
|
|
|
|
|
+ putExtra("url", effect.url)
|
|
|
|
|
+ putExtra("sourceOrigin", effect.sourceOrigin)
|
|
|
|
|
+ putExtra("sourceName", effect.sourceName)
|
|
|
|
|
+ effect.sourceType?.let { putExtra("sourceType", it) }
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenSearchActivity -> {
|
|
|
|
|
+ val currentState = viewModel.uiState.value
|
|
|
|
|
+ val lambda: (Intent.() -> Unit)? = { intent ->
|
|
|
|
|
+ intent.putExtra("bookUrl", effect.bookUrl)
|
|
|
|
|
+ intent.putExtra("searchWord", effect.word)
|
|
|
|
|
+ intent.putExtra("searchResultIndex", currentState.searchResultIndex)
|
|
|
|
|
+ currentState.searchResultList.firstOrNull()?.let {
|
|
|
|
|
+ if (it.query == currentState.searchContentQuery) {
|
|
|
|
|
+ IntentData.put("searchResultList", currentState.searchResultList)
|
|
|
|
|
+ }
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
+ searchContentLauncher.launch(lambda)
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.MenuSettingReplace -> {
|
|
|
|
|
+ replaceLauncher.launch(Intent(context, ReplaceRuleActivity::class.java))
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.TextActionReplace -> {
|
|
|
|
|
+ val scopes = arrayListOf<String>()
|
|
|
|
|
+ effect.bookName?.let { scopes.add(it) }
|
|
|
|
|
+ effect.bookSourceUrl?.let { scopes.add(it) }
|
|
|
|
|
+ val text = effect.text.lineSequence().map { it.trim() }.joinToString("\n")
|
|
|
|
|
+ val editRoute = ReplaceEditRoute(
|
|
|
|
|
+ id = -1, pattern = text,
|
|
|
|
|
+ scope = scopes.joinToString(";"),
|
|
|
|
|
+ isScopeTitle = false, isScopeContent = true,
|
|
|
|
|
+ )
|
|
|
|
|
+ replaceLauncher.launch(ReplaceRuleActivity.startIntent(context, editRoute))
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenReplaceEditor -> {
|
|
|
|
|
+ val editRoute = ReplaceEditRoute(id = effect.id, pattern = effect.pattern)
|
|
|
|
|
+ replaceLauncher.launch(ReplaceRuleActivity.startIntent(context, editRoute))
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.MenuTocRegex -> {
|
|
|
|
|
+ val intent = Intent(context, TxtTocRuleActivity::class.java)
|
|
|
|
|
+ intent.putExtra("tocRegex", effect.tocRegex)
|
|
|
|
|
+ txtTocRuleLauncher.launch(intent)
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenFontFolderPicker -> {
|
|
|
|
|
+ fontFolderPicker.launch(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenBooksDirPicker -> {
|
|
|
|
|
+ booksDirPicker.launch(null)
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenReadStyleImagePicker -> {
|
|
|
|
|
+ readStyleImagePicker.launch(arrayOf("image/*"))
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenReadStyleImagePickerForMode -> {
|
|
|
|
|
+ pendingReadStyleImageIsNight = effect.isNight
|
|
|
|
|
+ readStyleImagePickerForMode.launch(arrayOf("image/*"))
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenReadStyleImport -> {
|
|
|
|
|
+ readStyleImportPicker.launch(
|
|
|
|
|
+ arrayOf("application/zip", "application/octet-stream", "*/*")
|
|
|
|
|
+ )
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenReadStyleExport -> {
|
|
|
|
|
+ readStyleExportPicker.launch("readConfig.zip")
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenMenuCustomIconPicker -> {
|
|
|
|
|
+ pendingMenuCustomIconId = effect.id
|
|
|
|
|
+ menuCustomIconPicker.launch("image/*")
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenTitleBarCustomIconPicker -> {
|
|
|
|
|
+ pendingTitleBarCustomIconId = effect.id
|
|
|
|
|
+ titleBarCustomIconPicker.launch("image/*")
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.OpenSystemTtsSettings -> {
|
|
|
|
|
+ IntentHelp.openTTSSetting()
|
|
|
|
|
+ }
|
|
|
|
|
+ is ReadBookEffect.TtsCacheCleared -> {
|
|
|
|
|
+ context.toastOnUi(effect.message)
|
|
|
}
|
|
}
|
|
|
- searchContentLauncher.launch(lambda)
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.MenuSettingReplace -> {
|
|
|
|
|
- replaceLauncher.launch(Intent(context, ReplaceRuleActivity::class.java))
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.TextActionReplace -> {
|
|
|
|
|
- val scopes = arrayListOf<String>()
|
|
|
|
|
- effect.bookName?.let { scopes.add(it) }
|
|
|
|
|
- effect.bookSourceUrl?.let { scopes.add(it) }
|
|
|
|
|
- val text = effect.text.lineSequence().map { it.trim() }.joinToString("\n")
|
|
|
|
|
- val editRoute = ReplaceEditRoute(
|
|
|
|
|
- id = -1, pattern = text,
|
|
|
|
|
- scope = scopes.joinToString(";"),
|
|
|
|
|
- isScopeTitle = false, isScopeContent = true,
|
|
|
|
|
- )
|
|
|
|
|
- replaceLauncher.launch(ReplaceRuleActivity.startIntent(context, editRoute))
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenReplaceEditor -> {
|
|
|
|
|
- val editRoute = ReplaceEditRoute(id = effect.id, pattern = effect.pattern)
|
|
|
|
|
- replaceLauncher.launch(ReplaceRuleActivity.startIntent(context, editRoute))
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.MenuTocRegex -> {
|
|
|
|
|
- val intent = Intent(context, TxtTocRuleActivity::class.java)
|
|
|
|
|
- intent.putExtra("tocRegex", effect.tocRegex)
|
|
|
|
|
- txtTocRuleLauncher.launch(intent)
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenFontFolderPicker -> {
|
|
|
|
|
- fontFolderPicker.launch(null)
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenBooksDirPicker -> {
|
|
|
|
|
- booksDirPicker.launch(null)
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenReadStyleImagePicker -> {
|
|
|
|
|
- readStyleImagePicker.launch(arrayOf("image/*"))
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenReadStyleImagePickerForMode -> {
|
|
|
|
|
- pendingReadStyleImageIsNight = effect.isNight
|
|
|
|
|
- readStyleImagePickerForMode.launch(arrayOf("image/*"))
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenReadStyleImport -> {
|
|
|
|
|
- readStyleImportPicker.launch(
|
|
|
|
|
- arrayOf("application/zip", "application/octet-stream", "*/*")
|
|
|
|
|
- )
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenReadStyleExport -> {
|
|
|
|
|
- readStyleExportPicker.launch("readConfig.zip")
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenMenuCustomIconPicker -> {
|
|
|
|
|
- pendingMenuCustomIconId = effect.id
|
|
|
|
|
- menuCustomIconPicker.launch("image/*")
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenTitleBarCustomIconPicker -> {
|
|
|
|
|
- pendingTitleBarCustomIconId = effect.id
|
|
|
|
|
- titleBarCustomIconPicker.launch("image/*")
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.OpenSystemTtsSettings -> {
|
|
|
|
|
- IntentHelp.openTTSSetting()
|
|
|
|
|
- }
|
|
|
|
|
- is ReadBookEffect.TtsCacheCleared -> {
|
|
|
|
|
- context.toastOnUi(effect.message)
|
|
|
|
|
- }
|
|
|
|
|
|
|
|
|
|
- // All other effects — delegate to bridge (View/Window/Activity operations)
|
|
|
|
|
- else -> controller.handleEffect(effect)
|
|
|
|
|
|
|
+ // All other effects — delegate to bridge (View/Window/Activity operations)
|
|
|
|
|
+ else -> controller.handleEffect(effect)
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (e: Exception) {
|
|
|
|
|
+ AppLog.put("ReadBook effect处理异常: ${effect::class.simpleName}", e)
|
|
|
}
|
|
}
|
|
|
- } catch (e: Exception) {
|
|
|
|
|
- AppLog.put("ReadBook effect处理异常: ${effect::class.simpleName}", e)
|
|
|
|
|
}
|
|
}
|
|
|
- }
|
|
|
|
|
- }
|
|
|
|
|
- yield()
|
|
|
|
|
- onEffectsReady()
|
|
|
|
|
- collector.join()
|
|
|
|
|
}
|
|
}
|
|
|
|
|
+ effectsSubscribed.await()
|
|
|
|
|
+ onEffectsReady()
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// ── System UI sync ────────────────────────────────────────────────
|
|
// ── System UI sync ────────────────────────────────────────────────
|