feat(reader): 船新的基于 Compose 实现的阅读菜单界面与各项优化
* 初始版本 * refactor(reader): MVI architecture + menu settings overhaul MVI/UDF Architecture: - Add ReadBookContract: UiState, Intent, Effect, Sheet, Dialog, ConfigUpdate - Refactor ReadBookViewModel with onIntent() entry point - Add ReadBookScreen/ReadBookRouteScreen stateless composables - Add ReadBookColorTheme for reader-specific theme override - Add ReadConfigContract for read config screen MVI ReadStyleSheet Decomposition: - Split ReadStyleSheet into SystemMenuPage, TextTitleSheet, HeaderFooterPage, TitleBarIconSheet, GlobalThemePage - Each page owns its tab state, color picker, and config mutations Data Layer (DataStore-backed Repositories): - Add ReadSettingsRepository with ReadPreferences flow - Add MangaSettingsRepository with MangaPreferences flow - Add ReadAloudSettingsRepository for TTS settings - Add ReadStyleRepository (file I/O for read style configs) - Add ReadBookStyleConfigRepository (repository boundary) - Add ReadStyleResolver for background/theme resolution - Remove legacy ReadTipConfig (merged into ReadBookConfig) - Register all new repositories in Koin appModule Menu Settings: - Add unified border settings (width, color, colorNight) for both top and bottom bars with drawWithCache + drawPath - Add ColorModePill with light/dark toggle and '+' icon for no-color state in TinySettingItems - Add TinyClearColorModeSettingItem for border color config - Add menu color customization (bg, accent, container) with day/night variants and seed color mode - Add bottom bar layout settings (corner radius, margins) - Add custom icon support for menu buttons and title bar icons - Add title bar icon position setting Config Pipeline: - Consolidate all ReadBookConfig mutations into typed ConfigUpdate sealed interface with legacy UP_CONFIG code mapping - Add resolvedMenuBorderColor (auto day/night switching) - Add 20 new PreferKey constants for menu settings - Update manga config dialogs to use MangaSettingsRepository - Update ReadConfigViewModel with DataStore flow observation * feat(reader): add liquid glass menu bars and floating bottom bar - Integrate com.kyant.backdrop 2.0.0 for liquid glass blur/vibrancy effects on reading menu top bar and bottom bar (Android 13+) - Add floating bottom bar toggle replacing manual margin sliders - Extract ReadMenuConfig data class for proper MVI state management of all menu configuration properties - Enable edge-to-edge in BaseComposeActivity, remove manual navigation bar color management across base classes and controller - Fix fullScreen() to use setDecorFitsSystemWindows(false) * Refactor series button rendering * 实现液态玻璃 * 优化液态玻璃 * @ fix(reader): fix MVI behavior issues and improve sheet lifecycle Behavior fixes: - Fix AddSourceAsNewBook empty implementation — wire intent to addToBookshelf - Fix content edit loading stuck on null chapter/content — use onFinally - Fix cannot save empty content — remove isEmpty guard, use title as loaded proxy - Fix bookmark not opening — use openReadMenuRoute instead of dead activeSheet path - Fix chapter source sheet persists — animate dismiss via show param before removal - Fix chapter source reopens to TOC — reset showToc in initData - Fix SaveChapterContent saves to wrong chapter — carry chapterIndex in intent - Fix chapter source search runs after dismiss — cancel searchJob in dispose - Fix "go to background" button in ReadAloud — use CloseReadBook intent Architecture improvements: - ReadMenuConfig uses ImmutableMap for Compose stability - Fix stale remember in ReadBookMenuBar title bar icons - Move icon picker file IO from composable to ViewModel - Route launcher results through MVI intents instead of direct calls - Fix ChangeChapterSource effect collector key — use viewModel not Unit - Make ReadAloudConfigSheet UDF-compliant — stateless with intents - Remove main menu buttons from AutoRead and ReadAloud sheets @ * @ refactor(reader): use Material Icons and ConfirmDismissButtonsRow - TitleBarIconSheet: replace drawable resources with Icons library, use ConfirmDismissButtonsRow for save/cancel buttons - SystemMenuPage MenuCustomIconSheet: same icon and button treatment - ReadBookMenuBar: update ToolButtonDef/TitleBarIconDef to use ImageVector instead of drawable resource Int - Use AutoMirrored variants for List and HelpOutline icons @ * feat: 优化液态玻璃 * feat: 优化液态玻璃 * fix(AppLogSheet): 从本地文件读取日志,替代内存列表 * @ feat(reader): highlight rules, theme refactor & MVI improvements - Add HighlightRule entity, DAO, and config/edit sheets - Add ThemeConfigStore, remove OldThemeConfig - Refactor ReadBookContract/ViewModel/Screen for deeper MVI - Add CharStyle for text rendering, refactor TextLine/TextChapterLayout - Add SectionTitle, TinySettingItems UI components - Remove legacy XML layouts (regex_color_config) - Update database schema to v90 - Various sheet and widget refinements @ * @ fix: address PR review feedback - ReadStyleRepository: fix copy-paste bug (bgStrNight→bgStr) + add bgTypeNight import - BgImageSpan: replace Bitmap.createScaledBitmap with BitmapShader matrix scaling - HighlightRule: use UUID.randomUUID() instead of System.currentTimeMillis() - ChangeSourceSearchUseCase: guard against empty chapters list @ * @fix: remove residual regexColorRules references after merge - ChangeSourceSearchUseCase: adapt filter lambda to new 3-param signature - TextChapterLayout: remove globalRegexResult field, preApplyRegexColorRules call, and the entire regexColorRules fullTextBuilder block @ * fix: 图标间距
This commit is contained in:
1 parent
116762f826
commit
4c088448c3
209 files changed
+25994
-12717
No files matched your search
@@ -109,10 +109,10 @@
|
|||||||
"Bash(node *)",
|
"Bash(node *)",
|
||||||
"Bash(Select-String -Pattern \"BUILD|error|Error\")",
|
"Bash(Select-String -Pattern \"BUILD|error|Error\")",
|
||||||
"Bash(Select-Object -First 10)",
|
"Bash(Select-Object -First 10)",
|
||||||
"Bash(git push *)",
|
"Bash(Select-Object -Last 10)",
|
||||||
"Bash(gh run *)",
|
"Bash(dir /s /b \"%USERPROFILE%\\\\.gradle\\\\caches\\\\*kyant*\")",
|
||||||
"WebFetch(domain:ktor.io)",
|
"Bash(Select-String -Pattern \"\\(BUILD|error:|FAILED\\)\")",
|
||||||
"WebFetch(domain:api.ktor.io)"
|
"PowerShell(grep *)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -139,22 +139,6 @@
|
|||||||
android:enableOnBackInvokedCallback="true"
|
android:enableOnBackInvokedCallback="true"
|
||||||
android:exported="false"
|
android:exported="false"
|
||||||
android:windowSoftInputMode="adjustPan|stateHidden" />
|
android:windowSoftInputMode="adjustPan|stateHidden" />
|
||||||
<!-- 阅读界面 -->
|
|
||||||
<activity
|
|
||||||
android:name=".ui.book.read.ReadBookActivity"
|
|
||||||
android:enableOnBackInvokedCallback="true"
|
|
||||||
android:configChanges="uiMode|orientation|screenSize"
|
|
||||||
android:exported="true"
|
|
||||||
android:launchMode="singleTask">
|
|
||||||
<intent-filter>
|
|
||||||
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
|
|
||||||
</intent-filter>
|
|
||||||
|
|
||||||
<meta-data
|
|
||||||
android:name="com.samsung.android.support.REMOTE_ACTION"
|
|
||||||
android:resource="@xml/spen_remote_actions" />
|
|
||||||
</activity>
|
|
||||||
|
|
||||||
<!-- 阅读漫画界面 -->
|
<!-- 阅读漫画界面 -->
|
||||||
<activity
|
<activity
|
||||||
android:name=".ui.book.manga.ReadMangaActivity"
|
android:name=".ui.book.manga.ReadMangaActivity"
|
||||||
|
|||||||
@@ -45,8 +45,8 @@ import io.legado.app.help.LifecycleHelp
|
|||||||
import io.legado.app.help.RuleBigDataHelp
|
import io.legado.app.help.RuleBigDataHelp
|
||||||
import io.legado.app.help.book.BookHelp
|
import io.legado.app.help.book.BookHelp
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.config.OldThemeConfig.applyDayNightInit
|
import io.legado.app.help.config.ThemeConfigStore.applyDayNightInit
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.help.http.Cronet
|
import io.legado.app.help.http.Cronet
|
||||||
@@ -57,6 +57,7 @@ import io.legado.app.help.source.SourceHelp
|
|||||||
import io.legado.app.help.storage.Backup
|
import io.legado.app.help.storage.Backup
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
import io.legado.app.utils.ChineseUtils
|
import io.legado.app.utils.ChineseUtils
|
||||||
import io.legado.app.utils.FirebaseManager
|
import io.legado.app.utils.FirebaseManager
|
||||||
import io.legado.app.utils.LogUtils
|
import io.legado.app.utils.LogUtils
|
||||||
@@ -161,7 +162,7 @@ class App : Application(), ImageLoaderFactory {
|
|||||||
BookHelp.clearInvalidCache()
|
BookHelp.clearInvalidCache()
|
||||||
Backup.clearCache()
|
Backup.clearCache()
|
||||||
ReadBookConfig.clearBgAndCache()
|
ReadBookConfig.clearBgAndCache()
|
||||||
OldThemeConfig.clearBg()
|
ThemeConfigStore.clearBg()
|
||||||
//初始化简繁转换引擎
|
//初始化简繁转换引擎
|
||||||
when (AppConfig.chineseConverterType) {
|
when (AppConfig.chineseConverterType) {
|
||||||
1 -> {
|
1 -> {
|
||||||
@@ -189,6 +190,11 @@ class App : Application(), ImageLoaderFactory {
|
|||||||
// oldConfig = Configuration(newConfig)
|
// oldConfig = Configuration(newConfig)
|
||||||
// }
|
// }
|
||||||
|
|
||||||
|
override fun onTrimMemory(level: Int) {
|
||||||
|
super.onTrimMemory(level)
|
||||||
|
TextLine.trimCaches(level)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 尝试在安装了GMS的设备上(GMS或者MicroG)使用GMS内置的Conscrypt
|
* 尝试在安装了GMS的设备上(GMS或者MicroG)使用GMS内置的Conscrypt
|
||||||
* 作为首选JCE提供程序,而使Okhttp在低版本Android上
|
* 作为首选JCE提供程序,而使Okhttp在低版本Android上
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
|||||||
import androidx.core.graphics.drawable.IconCompat
|
import androidx.core.graphics.drawable.IconCompat
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.receiver.SharedReceiverActivity
|
import io.legado.app.receiver.SharedReceiverActivity
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.main.MainActivity
|
import io.legado.app.ui.main.MainActivity
|
||||||
|
|
||||||
object ShortCuts {
|
object ShortCuts {
|
||||||
@@ -34,7 +33,9 @@ object ShortCuts {
|
|||||||
val bookShelfIntent = MainActivity.createHomeIntent(context).apply {
|
val bookShelfIntent = MainActivity.createHomeIntent(context).apply {
|
||||||
action = Intent.ACTION_VIEW
|
action = Intent.ACTION_VIEW
|
||||||
}
|
}
|
||||||
val readBookIntent = buildIntent<ReadBookActivity>(context)
|
val readBookIntent = MainActivity.createReadBookIntent(context).apply {
|
||||||
|
action = Intent.ACTION_VIEW
|
||||||
|
}
|
||||||
return ShortcutInfoCompat.Builder(context, "lastRead")
|
return ShortcutInfoCompat.Builder(context, "lastRead")
|
||||||
.setShortLabel(context.getString(R.string.last_read))
|
.setShortLabel(context.getString(R.string.last_read))
|
||||||
.setLongLabel(context.getString(R.string.last_read))
|
.setLongLabel(context.getString(R.string.last_read))
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import io.legado.app.constant.EventBus
|
|||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.constant.Theme
|
import io.legado.app.constant.Theme
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.utils.applyOpenTint
|
import io.legado.app.utils.applyOpenTint
|
||||||
import io.legado.app.utils.applyTint
|
import io.legado.app.utils.applyTint
|
||||||
@@ -36,7 +36,6 @@ import io.legado.app.utils.fullScreen
|
|||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.hideSoftInput
|
import io.legado.app.utils.hideSoftInput
|
||||||
import io.legado.app.utils.observeEvent
|
import io.legado.app.utils.observeEvent
|
||||||
import io.legado.app.utils.setNavigationBarColorAuto
|
|
||||||
import io.legado.app.utils.setStatusBarColorAuto
|
import io.legado.app.utils.setStatusBarColorAuto
|
||||||
import io.legado.app.utils.themeColor
|
import io.legado.app.utils.themeColor
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
@@ -101,7 +100,6 @@ abstract class BaseActivity<VB : ViewBinding>(
|
|||||||
else{
|
else{
|
||||||
setupSystemBar()
|
setupSystemBar()
|
||||||
}
|
}
|
||||||
window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface))
|
|
||||||
//setupSystemBar()
|
//setupSystemBar()
|
||||||
setContentView(binding.root)
|
setContentView(binding.root)
|
||||||
upBackgroundImage()
|
upBackgroundImage()
|
||||||
@@ -231,7 +229,7 @@ abstract class BaseActivity<VB : ViewBinding>(
|
|||||||
open fun upBackgroundImage() {
|
open fun upBackgroundImage() {
|
||||||
if (imageBg) {
|
if (imageBg) {
|
||||||
try {
|
try {
|
||||||
OldThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
ThemeConfigStore.getBgImage(this, windowManager.windowSize)?.let {
|
||||||
window.decorView.background = it.toDrawable(resources)
|
window.decorView.background = it.toDrawable(resources)
|
||||||
}
|
}
|
||||||
} catch (e: OutOfMemoryError) {
|
} catch (e: OutOfMemoryError) {
|
||||||
@@ -261,4 +259,4 @@ abstract class BaseActivity<VB : ViewBinding>(
|
|||||||
currentFocus?.hideSoftInput()
|
currentFocus?.hideSoftInput()
|
||||||
super.finish()
|
super.finish()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -12,8 +12,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.utils.setNavigationBarColorAuto
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlin.coroutines.CoroutineContext
|
import kotlin.coroutines.CoroutineContext
|
||||||
@@ -30,7 +28,6 @@ abstract class BaseBottomSheetDialogFragment(
|
|||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
dialog?.window?.setNavigationBarColorAuto(requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
|
|
||||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||||
val screenHeight = resources.displayMetrics.heightPixels
|
val screenHeight = resources.displayMetrics.heightPixels
|
||||||
(dialog as? BottomSheetDialog)?.behavior?.apply {
|
(dialog as? BottomSheetDialog)?.behavior?.apply {
|
||||||
@@ -70,4 +67,4 @@ abstract class BaseBottomSheetDialogFragment(
|
|||||||
open fun observeLiveBus() {
|
open fun observeLiveBus() {
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
package io.legado.app.base
|
package io.legado.app.base
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Build
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
|
import androidx.activity.enableEdgeToEdge
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
@@ -9,7 +11,7 @@ import androidx.core.view.WindowCompat
|
|||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.Theme
|
import io.legado.app.constant.Theme
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.ui.theme.AppTheme
|
import io.legado.app.ui.theme.AppTheme
|
||||||
import io.legado.app.utils.disableAutoFill
|
import io.legado.app.utils.disableAutoFill
|
||||||
import io.legado.app.utils.fullScreen
|
import io.legado.app.utils.fullScreen
|
||||||
@@ -34,6 +36,10 @@ abstract class BaseComposeActivity(
|
|||||||
AppContextWrapper.applyLocaleAndFont(this)
|
AppContextWrapper.applyLocaleAndFont(this)
|
||||||
|
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
|
enableEdgeToEdge()
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||||
|
window.isNavigationBarContrastEnforced = false
|
||||||
|
}
|
||||||
|
|
||||||
setupSystemBar()
|
setupSystemBar()
|
||||||
// Compose 入口
|
// Compose 入口
|
||||||
@@ -66,7 +72,7 @@ abstract class BaseComposeActivity(
|
|||||||
|
|
||||||
open fun upBackgroundImage() {
|
open fun upBackgroundImage() {
|
||||||
try {
|
try {
|
||||||
OldThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
ThemeConfigStore.getBgImage(this, windowManager.windowSize)?.let {
|
||||||
window.setBackgroundDrawable(it.toDrawable(resources))
|
window.setBackgroundDrawable(it.toDrawable(resources))
|
||||||
}
|
}
|
||||||
} catch (_: Exception) {}
|
} catch (_: Exception) {}
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ object PreferKey {
|
|||||||
const val showBrightnessView = "showBrightnessView"
|
const val showBrightnessView = "showBrightnessView"
|
||||||
const val useUnderline = "useUnderline"
|
const val useUnderline = "useUnderline"
|
||||||
const val regexColorRules = "regexColorRules"
|
const val regexColorRules = "regexColorRules"
|
||||||
|
const val highlightRuleItems = "highlightRuleItems"
|
||||||
|
const val highlightRuleDialog = "highlightRuleDialog"
|
||||||
|
const val highlightRuleBookTitle = "highlightRuleBookTitle"
|
||||||
|
const val highlightRuleBracketNote = "highlightRuleBracketNote"
|
||||||
const val adaptSpecialStyle = "adaptSpecialStyle"
|
const val adaptSpecialStyle = "adaptSpecialStyle"
|
||||||
const val autoClearExpired = "autoClearExpired"
|
const val autoClearExpired = "autoClearExpired"
|
||||||
const val autoChangeSource = "autoChangeSource"
|
const val autoChangeSource = "autoChangeSource"
|
||||||
@@ -288,6 +292,35 @@ object PreferKey {
|
|||||||
const val isPredictiveBackEnabled = "isPredictiveBackEnabled"
|
const val isPredictiveBackEnabled = "isPredictiveBackEnabled"
|
||||||
const val replaceSortMode = "desc"
|
const val replaceSortMode = "desc"
|
||||||
const val readBarStyle = "readBarStyle"
|
const val readBarStyle = "readBarStyle"
|
||||||
|
const val readMenuBgColor = "readMenuBgColor"
|
||||||
|
const val readMenuAccentColor = "readMenuAccentColor"
|
||||||
|
const val readMenuContainerColor = "readMenuContainerColor"
|
||||||
|
const val readMenuBgColorNight = "readMenuBgColorNight"
|
||||||
|
const val readMenuAccentColorNight = "readMenuAccentColorNight"
|
||||||
|
const val readMenuContainerColorNight = "readMenuContainerColorNight"
|
||||||
|
const val readMenuColorMode = "readMenuColorMode"
|
||||||
|
const val readMenuIconShowText = "readMenuIconShowText"
|
||||||
|
const val readMenuIconStyle = "readMenuIconStyle"
|
||||||
|
const val readMenuIconItemsPerRow = "readMenuIconItemsPerRow"
|
||||||
|
const val readMenuIconRowCount = "readMenuIconRowCount"
|
||||||
|
const val readMenuBottomCornerRadius = "readMenuBottomCornerRadius"
|
||||||
|
const val readMenuFloatingBottomBar = "readMenuFloatingBottomBar"
|
||||||
|
const val readMenuTopBarBlurMode = "readMenuTopBarBlurMode"
|
||||||
|
const val readMenuBottomBarBlurMode = "readMenuBottomBarBlurMode"
|
||||||
|
const val readMenuTopBarLiquidGlassButtons = "readMenuTopBarLiquidGlassButtons"
|
||||||
|
const val readMenuBottomBarLiquidGlassButtons = "readMenuBottomBarLiquidGlassButtons"
|
||||||
|
const val readMenuTopBarBlurStyle = "readMenuTopBarBlurStyle"
|
||||||
|
const val readMenuBottomBarBlurStyle = "readMenuBottomBarBlurStyle"
|
||||||
|
const val readMenuBlurRadius = "readMenuBlurRadius"
|
||||||
|
const val readMenuBlurAlpha = "readMenuBlurAlpha"
|
||||||
|
const val readMenuLensRadius = "readMenuLensRadius"
|
||||||
|
const val readMenuBorderWidth = "readMenuBorderWidth"
|
||||||
|
const val readMenuBorderColor = "readMenuBorderColor"
|
||||||
|
const val readMenuBorderColorNight = "readMenuBorderColorNight"
|
||||||
|
const val readMenuCustomIcons = "readMenuCustomIcons"
|
||||||
|
const val titleBarCustomIcons = "titleBarCustomIcons"
|
||||||
|
const val titleBarIconPosition = "titleBarIconPosition"
|
||||||
|
const val showTitleBarIcons = "showTitleBarIcons"
|
||||||
const val disableReturnKey = "disableReturnKey"
|
const val disableReturnKey = "disableReturnKey"
|
||||||
const val selectText = "selectText"
|
const val selectText = "selectText"
|
||||||
//我在干什么
|
//我在干什么
|
||||||
@@ -351,3 +384,14 @@ object PreferKey {
|
|||||||
const val eyeProtectionStartTime = "eyeProtectionStartTime"
|
const val eyeProtectionStartTime = "eyeProtectionStartTime"
|
||||||
const val eyeProtectionEndTime = "eyeProtectionEndTime"
|
const val eyeProtectionEndTime = "eyeProtectionEndTime"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
object ReadMenuBlurMode {
|
||||||
|
const val None = 0
|
||||||
|
const val LiquidGlass = 1
|
||||||
|
const val Haze = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
object ReadMenuBlurStyle {
|
||||||
|
const val Solid = 0
|
||||||
|
const val Progressive = 1
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ import io.legado.app.data.dao.BookmarkDao
|
|||||||
import io.legado.app.data.dao.CacheDao
|
import io.legado.app.data.dao.CacheDao
|
||||||
import io.legado.app.data.dao.CookieDao
|
import io.legado.app.data.dao.CookieDao
|
||||||
import io.legado.app.data.dao.DictRuleDao
|
import io.legado.app.data.dao.DictRuleDao
|
||||||
|
import io.legado.app.data.dao.HighlightRuleDao
|
||||||
import io.legado.app.data.dao.HomepageCustomSetDao
|
import io.legado.app.data.dao.HomepageCustomSetDao
|
||||||
import io.legado.app.data.dao.HomepageModuleDao
|
import io.legado.app.data.dao.HomepageModuleDao
|
||||||
import io.legado.app.data.dao.HttpTTSDao
|
import io.legado.app.data.dao.HttpTTSDao
|
||||||
@@ -41,6 +42,7 @@ import io.legado.app.data.entities.Cache
|
|||||||
import io.legado.app.data.entities.Cookie
|
import io.legado.app.data.entities.Cookie
|
||||||
import io.legado.app.data.entities.DictRule
|
import io.legado.app.data.entities.DictRule
|
||||||
import io.legado.app.data.entities.HomepageCustomSet
|
import io.legado.app.data.entities.HomepageCustomSet
|
||||||
|
import io.legado.app.data.entities.HighlightRule
|
||||||
import io.legado.app.data.entities.HomepageModule
|
import io.legado.app.data.entities.HomepageModule
|
||||||
import io.legado.app.data.entities.HttpTTS
|
import io.legado.app.data.entities.HttpTTS
|
||||||
import io.legado.app.data.entities.KeyboardAssist
|
import io.legado.app.data.entities.KeyboardAssist
|
||||||
@@ -73,7 +75,7 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 89,
|
version = 90,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
@@ -81,7 +83,8 @@ val appDb by lazy {
|
|||||||
RssReadRecord::class, ReadRecordDetail::class, ReadRecordSession::class,
|
RssReadRecord::class, ReadRecordDetail::class, ReadRecordSession::class,
|
||||||
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
|
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
|
||||||
RuleSub::class, DictRule::class, KeyboardAssist::class, Server::class,
|
RuleSub::class, DictRule::class, KeyboardAssist::class, Server::class,
|
||||||
SearchContentHistory::class, HomepageModule::class, HomepageCustomSet::class],
|
SearchContentHistory::class, HomepageModule::class, HomepageCustomSet::class,
|
||||||
|
HighlightRule::class],
|
||||||
views = [BookSourcePart::class],
|
views = [BookSourcePart::class],
|
||||||
autoMigrations = [
|
autoMigrations = [
|
||||||
AutoMigration(from = 43, to = 44),
|
AutoMigration(from = 43, to = 44),
|
||||||
@@ -129,7 +132,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 85, to = 86),
|
AutoMigration(from = 85, to = 86),
|
||||||
AutoMigration(from = 86, to = 87),
|
AutoMigration(from = 86, to = 87),
|
||||||
AutoMigration(from = 87, to = 88),
|
AutoMigration(from = 87, to = 88),
|
||||||
AutoMigration(from = 88, to = 89)
|
AutoMigration(from = 88, to = 89),
|
||||||
|
AutoMigration(from = 89, to = 90)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
@@ -158,6 +162,7 @@ abstract class AppDatabase : RoomDatabase() {
|
|||||||
abstract val searchContentHistoryDao: SearchContentHistoryDao
|
abstract val searchContentHistoryDao: SearchContentHistoryDao
|
||||||
abstract val homepageModuleDao: HomepageModuleDao
|
abstract val homepageModuleDao: HomepageModuleDao
|
||||||
abstract val homepageCustomSetDao: HomepageCustomSetDao
|
abstract val homepageCustomSetDao: HomepageCustomSetDao
|
||||||
|
abstract val highlightRuleDao: HighlightRuleDao
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package io.legado.app.data.dao
|
||||||
|
|
||||||
|
import androidx.room.Dao
|
||||||
|
import androidx.room.Delete
|
||||||
|
import androidx.room.Insert
|
||||||
|
import androidx.room.OnConflictStrategy
|
||||||
|
import androidx.room.Query
|
||||||
|
import androidx.room.Transaction
|
||||||
|
import androidx.room.Update
|
||||||
|
import io.legado.app.data.entities.HighlightRule
|
||||||
|
|
||||||
|
@Dao
|
||||||
|
interface HighlightRuleDao {
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlightRules ORDER BY position ASC")
|
||||||
|
fun getAll(): List<HighlightRule>
|
||||||
|
|
||||||
|
@Query("SELECT * FROM highlightRules WHERE enabled = 1 AND pattern != '' ORDER BY position ASC")
|
||||||
|
fun getEnabled(): List<HighlightRule>
|
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||||
|
fun insertAll(rules: List<HighlightRule>)
|
||||||
|
|
||||||
|
@Update
|
||||||
|
fun update(rule: HighlightRule)
|
||||||
|
|
||||||
|
@Delete
|
||||||
|
fun delete(rule: HighlightRule)
|
||||||
|
|
||||||
|
@Query("DELETE FROM highlightRules")
|
||||||
|
fun deleteAll()
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM highlightRules")
|
||||||
|
fun count(): Int
|
||||||
|
|
||||||
|
@Transaction
|
||||||
|
fun replaceAll(rules: List<HighlightRule>) {
|
||||||
|
deleteAll()
|
||||||
|
insertAll(rules)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
|
import androidx.room.Entity
|
||||||
|
import androidx.room.PrimaryKey
|
||||||
|
import java.util.UUID
|
||||||
|
|
||||||
|
@Entity(tableName = "highlightRules")
|
||||||
|
data class HighlightRule(
|
||||||
|
@PrimaryKey
|
||||||
|
var id: String = UUID.randomUUID().toString(),
|
||||||
|
var name: String = "",
|
||||||
|
var pattern: String = "",
|
||||||
|
var sampleText: String = "",
|
||||||
|
var targetScope: Int = TARGET_ALL,
|
||||||
|
var enabled: Boolean = true,
|
||||||
|
var position: Int = 0,
|
||||||
|
var textColor: Int? = null,
|
||||||
|
var bgColor: Int? = null,
|
||||||
|
var underlineMode: Int = 0,
|
||||||
|
var underlineColor: Int? = null,
|
||||||
|
var underlineWidth: Float = 1f,
|
||||||
|
var underlineOffset: Float = 2f,
|
||||||
|
var underlineSvgPath: String? = null,
|
||||||
|
var bgImage: String? = null,
|
||||||
|
var bgImageFit: Int = 0,
|
||||||
|
var bgImageScale: Float = 1f,
|
||||||
|
) {
|
||||||
|
|
||||||
|
fun styleSummary(): String {
|
||||||
|
val parts = ArrayList<String>(4)
|
||||||
|
parts.add(targetScopeLabel())
|
||||||
|
textColor?.let {
|
||||||
|
parts.add("字色 ${it.toHexColor()}")
|
||||||
|
}
|
||||||
|
bgColor?.let {
|
||||||
|
parts.add("背景色 ${it.toHexColor()}")
|
||||||
|
}
|
||||||
|
if (underlineMode != 0) {
|
||||||
|
parts.add(
|
||||||
|
when (underlineMode) {
|
||||||
|
1 -> "实线下划线"
|
||||||
|
2 -> "虚线下划线"
|
||||||
|
3 -> "波浪下划线"
|
||||||
|
4 -> "双下划线"
|
||||||
|
5 -> "自定义SVG"
|
||||||
|
else -> "下划线"
|
||||||
|
} + underlineColor?.let { " ${it.toHexColor()}" }.orEmpty()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (!bgImage.isNullOrBlank()) {
|
||||||
|
parts.add(
|
||||||
|
when (bgImageFit) {
|
||||||
|
1 -> "背景图(拉伸)"
|
||||||
|
2 -> "背景图(裁剪)"
|
||||||
|
else -> "背景图(平铺)"
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (parts.isEmpty()) {
|
||||||
|
parts.add("无样式")
|
||||||
|
}
|
||||||
|
return parts.joinToString(" / ")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun targetScopeLabel(): String {
|
||||||
|
return when (targetScope) {
|
||||||
|
TARGET_TITLE -> "作用于标题"
|
||||||
|
TARGET_BODY -> "作用于正文"
|
||||||
|
else -> "作用于全部"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun displayPattern(): String {
|
||||||
|
return pattern.ifBlank { ".*" }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun normalizedSampleText(): String {
|
||||||
|
return sampleText.ifBlank {
|
||||||
|
"她轻声说:“今晚就出发。”\n最近在重读《百年孤独》(纪念版),节奏依然很稳。"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun copyWithNewId(): HighlightRule {
|
||||||
|
return copy(id = UUID.randomUUID().toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TARGET_ALL = 0
|
||||||
|
const val TARGET_TITLE = 1
|
||||||
|
const val TARGET_BODY = 2
|
||||||
|
|
||||||
|
fun Int.toHexColor(): String = String.format("#%08X", this)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,182 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.book.manga.config.MangaScrollMode
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class MangaPreferences(
|
||||||
|
val showMangaUi: Boolean = true,
|
||||||
|
val disableMangaScale: Boolean = true,
|
||||||
|
val disableMangaScrollAnimation: Boolean = false,
|
||||||
|
val disableMangaCrossFade: Boolean = false,
|
||||||
|
val disableClickScroll: Boolean = false,
|
||||||
|
val mangaPreDownloadNum: Int = 10,
|
||||||
|
val mangaAutoPageSpeed: Int = 3,
|
||||||
|
val mangaFooterConfig: String = "",
|
||||||
|
val mangaScrollMode: Int = MangaScrollMode.WEBTOON,
|
||||||
|
val mangaLongClick: Boolean = true,
|
||||||
|
val mangaBackground: Int = 0xFF000000.toInt(),
|
||||||
|
val mangaColorFilter: String = "",
|
||||||
|
val hideMangaTitle: Boolean = false,
|
||||||
|
val enableMangaEInk: Boolean = false,
|
||||||
|
val mangaEInkThreshold: Int = 150,
|
||||||
|
val enableMangaGray: Boolean = false,
|
||||||
|
val webtoonSidePaddingDp: Int = 0,
|
||||||
|
val mangaVolumeKeyPage: Boolean = false,
|
||||||
|
val reverseVolumeKeyPage: Boolean = false,
|
||||||
|
val clickActionTL: Int = -1,
|
||||||
|
val clickActionTC: Int = -1,
|
||||||
|
val clickActionTR: Int = 1,
|
||||||
|
val clickActionML: Int = 2,
|
||||||
|
val clickActionMC: Int = 0,
|
||||||
|
val clickActionMR: Int = 1,
|
||||||
|
val clickActionBL: Int = 2,
|
||||||
|
val clickActionBC: Int = 1,
|
||||||
|
val clickActionBR: Int = 1
|
||||||
|
)
|
||||||
|
|
||||||
|
class MangaSettingsRepository(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
val preferences: Flow<MangaPreferences> = context.dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { preferences ->
|
||||||
|
preferences.toMangaPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setShowMangaUi(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.showMangaUi, value)
|
||||||
|
|
||||||
|
suspend fun setMangaPreDownloadNum(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.mangaPreDownloadNum, value)
|
||||||
|
|
||||||
|
suspend fun setMangaAutoPageSpeed(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.mangaAutoPageSpeed, value)
|
||||||
|
|
||||||
|
suspend fun setDisableClickScroll(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.disableClickScroll, value)
|
||||||
|
|
||||||
|
suspend fun setDisableMangaScrollAnimation(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.disableMangaScrollAnimation, value)
|
||||||
|
|
||||||
|
suspend fun setDisableMangaCrossFade(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.disableMangaCrossFade, value)
|
||||||
|
|
||||||
|
suspend fun setDisableMangaScale(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.disableMangaScale, value)
|
||||||
|
|
||||||
|
suspend fun setEnableMangaEInk(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.enableMangaEInk, value)
|
||||||
|
|
||||||
|
suspend fun setMangaEInkThreshold(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.mangaEInkThreshold, value)
|
||||||
|
|
||||||
|
suspend fun setEnableMangaGray(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.enableMangaGray, value)
|
||||||
|
|
||||||
|
suspend fun setMangaAutoColorFilter(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.mangaColorFilter, value)
|
||||||
|
|
||||||
|
suspend fun setMangaBackground(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.mangaBackground, value)
|
||||||
|
|
||||||
|
suspend fun setMangaLongClick(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.mangaLongClick, value)
|
||||||
|
|
||||||
|
suspend fun setMangaVolumeKeyPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.mangaVolumeKeyPage, value)
|
||||||
|
|
||||||
|
suspend fun setReverseVolumeKeyPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.reverseVolumeKeyPage, value)
|
||||||
|
|
||||||
|
suspend fun setHideMangaTitle(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.hideMangaTitle, value)
|
||||||
|
|
||||||
|
suspend fun setMangaFooterConfig(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.mangaFooterConfig, value)
|
||||||
|
|
||||||
|
suspend fun setMangaClickAction(key: String, value: Int) =
|
||||||
|
settingsRepository.putInt(key, value)
|
||||||
|
|
||||||
|
private fun Preferences.toMangaPreferences(): MangaPreferences {
|
||||||
|
return MangaPreferences(
|
||||||
|
showMangaUi = this[Keys.ShowMangaUi] ?: true,
|
||||||
|
disableMangaScale = this[Keys.DisableMangaScale] ?: true,
|
||||||
|
disableMangaScrollAnimation = this[Keys.DisableMangaScrollAnimation] ?: false,
|
||||||
|
disableMangaCrossFade = this[Keys.DisableMangaCrossFade] ?: false,
|
||||||
|
disableClickScroll = this[Keys.DisableClickScroll] ?: false,
|
||||||
|
mangaPreDownloadNum = this[Keys.MangaPreDownloadNum] ?: 10,
|
||||||
|
mangaAutoPageSpeed = this[Keys.MangaAutoPageSpeed] ?: 3,
|
||||||
|
mangaFooterConfig = this[Keys.MangaFooterConfig] ?: "",
|
||||||
|
mangaScrollMode = this[Keys.MangaScrollMode] ?: MangaScrollMode.WEBTOON,
|
||||||
|
mangaLongClick = this[Keys.MangaLongClick] ?: true,
|
||||||
|
mangaBackground = this[Keys.MangaBackground] ?: 0xFF000000.toInt(),
|
||||||
|
mangaColorFilter = this[Keys.MangaColorFilter] ?: "",
|
||||||
|
hideMangaTitle = this[Keys.HideMangaTitle] ?: false,
|
||||||
|
enableMangaEInk = this[Keys.EnableMangaEInk] ?: false,
|
||||||
|
mangaEInkThreshold = this[Keys.MangaEInkThreshold] ?: 150,
|
||||||
|
enableMangaGray = this[Keys.EnableMangaGray] ?: false,
|
||||||
|
webtoonSidePaddingDp = this[Keys.WebtoonSidePaddingDp] ?: 0,
|
||||||
|
mangaVolumeKeyPage = this[Keys.MangaVolumeKeyPage] ?: false,
|
||||||
|
reverseVolumeKeyPage = this[Keys.ReverseVolumeKeyPage] ?: false,
|
||||||
|
clickActionTL = this[Keys.ClickActionTL] ?: -1,
|
||||||
|
clickActionTC = this[Keys.ClickActionTC] ?: -1,
|
||||||
|
clickActionTR = this[Keys.ClickActionTR] ?: 1,
|
||||||
|
clickActionML = this[Keys.ClickActionML] ?: 2,
|
||||||
|
clickActionMC = this[Keys.ClickActionMC] ?: 0,
|
||||||
|
clickActionMR = this[Keys.ClickActionMR] ?: 1,
|
||||||
|
clickActionBL = this[Keys.ClickActionBL] ?: 2,
|
||||||
|
clickActionBC = this[Keys.ClickActionBC] ?: 1,
|
||||||
|
clickActionBR = this[Keys.ClickActionBR] ?: 1
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Keys {
|
||||||
|
val ShowMangaUi = booleanPreferencesKey(PreferKey.showMangaUi)
|
||||||
|
val DisableMangaScale = booleanPreferencesKey(PreferKey.disableMangaScale)
|
||||||
|
val DisableMangaScrollAnimation =
|
||||||
|
booleanPreferencesKey(PreferKey.disableMangaScrollAnimation)
|
||||||
|
val DisableMangaCrossFade = booleanPreferencesKey(PreferKey.disableMangaCrossFade)
|
||||||
|
val DisableClickScroll = booleanPreferencesKey(PreferKey.disableClickScroll)
|
||||||
|
val MangaPreDownloadNum = intPreferencesKey(PreferKey.mangaPreDownloadNum)
|
||||||
|
val MangaAutoPageSpeed = intPreferencesKey(PreferKey.mangaAutoPageSpeed)
|
||||||
|
val MangaFooterConfig = stringPreferencesKey(PreferKey.mangaFooterConfig)
|
||||||
|
val MangaScrollMode = intPreferencesKey(PreferKey.mangaScrollMode)
|
||||||
|
val MangaLongClick = booleanPreferencesKey(PreferKey.mangaLongClick)
|
||||||
|
val MangaBackground = intPreferencesKey(PreferKey.mangaBackground)
|
||||||
|
val MangaColorFilter = stringPreferencesKey(PreferKey.mangaColorFilter)
|
||||||
|
val HideMangaTitle = booleanPreferencesKey(PreferKey.hideMangaTitle)
|
||||||
|
val EnableMangaEInk = booleanPreferencesKey(PreferKey.enableMangaEInk)
|
||||||
|
val MangaEInkThreshold = intPreferencesKey(PreferKey.mangaEInkThreshold)
|
||||||
|
val EnableMangaGray = booleanPreferencesKey(PreferKey.enableMangaGray)
|
||||||
|
val WebtoonSidePaddingDp = intPreferencesKey(PreferKey.webtoonSidePaddingDp)
|
||||||
|
val MangaVolumeKeyPage = booleanPreferencesKey(PreferKey.mangaVolumeKeyPage)
|
||||||
|
val ReverseVolumeKeyPage = booleanPreferencesKey(PreferKey.reverseVolumeKeyPage)
|
||||||
|
val ClickActionTL = intPreferencesKey(PreferKey.mangaClickActionTL)
|
||||||
|
val ClickActionTC = intPreferencesKey(PreferKey.mangaClickActionTC)
|
||||||
|
val ClickActionTR = intPreferencesKey(PreferKey.mangaClickActionTR)
|
||||||
|
val ClickActionML = intPreferencesKey(PreferKey.mangaClickActionML)
|
||||||
|
val ClickActionMC = intPreferencesKey(PreferKey.mangaClickActionMC)
|
||||||
|
val ClickActionMR = intPreferencesKey(PreferKey.mangaClickActionMR)
|
||||||
|
val ClickActionBL = intPreferencesKey(PreferKey.mangaClickActionBL)
|
||||||
|
val ClickActionBC = intPreferencesKey(PreferKey.mangaClickActionBC)
|
||||||
|
val ClickActionBR = intPreferencesKey(PreferKey.mangaClickActionBR)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class ReadAloudPreferences(
|
||||||
|
val ignoreAudioFocus: Boolean = false,
|
||||||
|
val mediaButtonOnExit: Boolean = true,
|
||||||
|
val readAloudByMediaButton: Boolean = false,
|
||||||
|
val pauseReadAloudWhilePhoneCalls: Boolean = false,
|
||||||
|
val readAloudWakeLock: Boolean = false,
|
||||||
|
val mediaButtonPerNext: Boolean = false,
|
||||||
|
val readAloudByPage: Boolean = false,
|
||||||
|
val systemMediaControlCompatibilityChange: Boolean = true,
|
||||||
|
val streamReadAloudAudio: Boolean = false,
|
||||||
|
val ttsTimer: Int = 0,
|
||||||
|
val ttsFollowSys: Boolean = true,
|
||||||
|
val ttsSpeechRate: Int = 5,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReadAloudSettingsRepository(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
val preferences: Flow<ReadAloudPreferences> = context.dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { preferences ->
|
||||||
|
preferences.toReadAloudPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setIgnoreAudioFocus(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.ignoreAudioFocus, value)
|
||||||
|
|
||||||
|
suspend fun setMediaButtonOnExit(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.mediaButtonOnExit, value)
|
||||||
|
|
||||||
|
suspend fun setReadAloudByMediaButton(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readAloudByMediaButton, value)
|
||||||
|
|
||||||
|
suspend fun setPauseReadAloudWhilePhoneCalls(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.pauseReadAloudWhilePhoneCalls, value)
|
||||||
|
|
||||||
|
suspend fun setReadAloudWakeLock(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readAloudWakeLock, value)
|
||||||
|
|
||||||
|
suspend fun setMediaButtonPerNext(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(KEY_MEDIA_BUTTON_PER_NEXT, value)
|
||||||
|
|
||||||
|
suspend fun setReadAloudByPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readAloudByPage, value)
|
||||||
|
|
||||||
|
suspend fun setSystemMediaControlCompatibilityChange(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.systemMediaControlCompatibilityChange, value)
|
||||||
|
|
||||||
|
suspend fun setStreamReadAloudAudio(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.streamReadAloudAudio, value)
|
||||||
|
|
||||||
|
suspend fun setTtsTimer(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.ttsTimer, value.coerceIn(0, 180))
|
||||||
|
|
||||||
|
suspend fun setTtsFollowSys(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.ttsFollowSys, value)
|
||||||
|
|
||||||
|
suspend fun setTtsSpeechRate(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.ttsSpeechRate, value.coerceIn(0, 80))
|
||||||
|
|
||||||
|
private fun Preferences.toReadAloudPreferences(): ReadAloudPreferences {
|
||||||
|
return ReadAloudPreferences(
|
||||||
|
ignoreAudioFocus = this[Keys.IgnoreAudioFocus] ?: false,
|
||||||
|
mediaButtonOnExit = this[Keys.MediaButtonOnExit] ?: true,
|
||||||
|
readAloudByMediaButton = this[Keys.ReadAloudByMediaButton] ?: false,
|
||||||
|
pauseReadAloudWhilePhoneCalls = this[Keys.PauseReadAloudWhilePhoneCalls] ?: false,
|
||||||
|
readAloudWakeLock = this[Keys.ReadAloudWakeLock] ?: false,
|
||||||
|
mediaButtonPerNext = this[Keys.MediaButtonPerNext] ?: false,
|
||||||
|
readAloudByPage = this[Keys.ReadAloudByPage] ?: false,
|
||||||
|
systemMediaControlCompatibilityChange =
|
||||||
|
this[Keys.SystemMediaControlCompatibilityChange] ?: true,
|
||||||
|
streamReadAloudAudio = this[Keys.StreamReadAloudAudio] ?: false,
|
||||||
|
ttsTimer = this[Keys.TtsTimer] ?: 0,
|
||||||
|
ttsFollowSys = this[Keys.TtsFollowSys] ?: true,
|
||||||
|
ttsSpeechRate = this[Keys.TtsSpeechRate] ?: 5,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Keys {
|
||||||
|
val IgnoreAudioFocus = booleanPreferencesKey(PreferKey.ignoreAudioFocus)
|
||||||
|
val MediaButtonOnExit = booleanPreferencesKey(PreferKey.mediaButtonOnExit)
|
||||||
|
val ReadAloudByMediaButton = booleanPreferencesKey(PreferKey.readAloudByMediaButton)
|
||||||
|
val PauseReadAloudWhilePhoneCalls =
|
||||||
|
booleanPreferencesKey(PreferKey.pauseReadAloudWhilePhoneCalls)
|
||||||
|
val ReadAloudWakeLock = booleanPreferencesKey(PreferKey.readAloudWakeLock)
|
||||||
|
val MediaButtonPerNext = booleanPreferencesKey(KEY_MEDIA_BUTTON_PER_NEXT)
|
||||||
|
val ReadAloudByPage = booleanPreferencesKey(PreferKey.readAloudByPage)
|
||||||
|
val SystemMediaControlCompatibilityChange =
|
||||||
|
booleanPreferencesKey(PreferKey.systemMediaControlCompatibilityChange)
|
||||||
|
val StreamReadAloudAudio = booleanPreferencesKey(PreferKey.streamReadAloudAudio)
|
||||||
|
val TtsTimer = androidx.datastore.preferences.core.intPreferencesKey(PreferKey.ttsTimer)
|
||||||
|
val TtsFollowSys = booleanPreferencesKey(PreferKey.ttsFollowSys)
|
||||||
|
val TtsSpeechRate = androidx.datastore.preferences.core.intPreferencesKey(
|
||||||
|
PreferKey.ttsSpeechRate
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val KEY_MEDIA_BUTTON_PER_NEXT = "mediaButtonPerNext"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Repository boundary for the legacy read style config file.
|
||||||
|
*
|
||||||
|
* The underlying model is still [ReadBookConfig.Config] because each read style
|
||||||
|
* keeps a full layout/background/text configuration. This wrapper centralizes
|
||||||
|
* file mutations so UI and ViewModel code do not call persistence helpers directly.
|
||||||
|
*/
|
||||||
|
class ReadBookStyleConfigRepository {
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
ReadBookConfig.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addStyle(): Int {
|
||||||
|
ReadBookConfig.configList.add(ReadBookConfig.Config())
|
||||||
|
save()
|
||||||
|
return ReadBookConfig.configList.lastIndex
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteCurrentStyle(): Boolean {
|
||||||
|
val deleted = ReadBookConfig.deleteDur()
|
||||||
|
if (deleted) {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
return deleted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importCurrentStyle(bytes: ByteArray) {
|
||||||
|
ReadBookConfig.durConfig = ReadBookConfig.import(bytes)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportCurrentStyle(): ByteArray {
|
||||||
|
return ReadBookConfig.export()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBackgroundImage(inputStream: InputStream, displayName: String?): String {
|
||||||
|
return ReadBookConfig.saveBackgroundImage(inputStream, displayName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentBackgroundImage(path: String) {
|
||||||
|
ReadBookConfig.durConfig.setCurBg(2, path)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentBackgroundImageForMode(path: String, isNight: Boolean) {
|
||||||
|
if (isNight) {
|
||||||
|
ReadBookConfig.durConfig.bgTypeNight = 2
|
||||||
|
ReadBookConfig.durConfig.bgStrNight = path
|
||||||
|
} else {
|
||||||
|
ReadBookConfig.durConfig.bgType = 2
|
||||||
|
ReadBookConfig.durConfig.bgStr = path
|
||||||
|
}
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,537 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import androidx.datastore.preferences.core.Preferences
|
||||||
|
import androidx.datastore.preferences.core.booleanPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.emptyPreferences
|
||||||
|
import androidx.datastore.preferences.core.floatPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.intPreferencesKey
|
||||||
|
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.constant.ReadMenuBlurMode
|
||||||
|
import io.legado.app.constant.ReadMenuBlurStyle
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import java.io.IOException
|
||||||
|
|
||||||
|
data class ReadPreferences(
|
||||||
|
val screenOrientation: String = "0",
|
||||||
|
val keepLight: String = "0",
|
||||||
|
val hideStatusBar: Boolean = false,
|
||||||
|
val hideNavigationBar: Boolean = false,
|
||||||
|
val paddingDisplayCutouts: Boolean = false,
|
||||||
|
val titleBarMode: String = "1",
|
||||||
|
val menuAlpha: Int = 100,
|
||||||
|
val readBodyToLh: Boolean = true,
|
||||||
|
val defaultSourceChangeAll: Boolean = true,
|
||||||
|
val textFullJustify: Boolean = true,
|
||||||
|
val textBottomJustify: Boolean = true,
|
||||||
|
val adaptSpecialStyle: Boolean = true,
|
||||||
|
val useZhLayout: Boolean = false,
|
||||||
|
val showBrightnessView: Boolean = true,
|
||||||
|
val useUnderline: Boolean = false,
|
||||||
|
val readSliderMode: String = "0",
|
||||||
|
val doubleHorizontalPage: String = "0",
|
||||||
|
val progressBarBehavior: String = "page",
|
||||||
|
val mouseWheelPage: Boolean = true,
|
||||||
|
val volumeKeyPage: Boolean = true,
|
||||||
|
val volumeKeyPageOnPlay: Boolean = true,
|
||||||
|
val keyPageOnLongPress: Boolean = false,
|
||||||
|
val pageTouchSlop: Int = 0,
|
||||||
|
val sliderVibrator: Boolean = false,
|
||||||
|
val selectVibrator: Boolean = false,
|
||||||
|
val autoChangeSource: Boolean = true,
|
||||||
|
val selectText: Boolean = true,
|
||||||
|
val noAnimScrollPage: Boolean = false,
|
||||||
|
val clickImgWay: String = "2",
|
||||||
|
val optimizeRender: Boolean = false,
|
||||||
|
val disableReturnKey: Boolean = false,
|
||||||
|
val expandTextMenu: Boolean = false,
|
||||||
|
val showReadTitleAddition: Boolean = true,
|
||||||
|
val autoReadSpeed: Int = 10,
|
||||||
|
val prevKeys: String = "",
|
||||||
|
val nextKeys: String = "",
|
||||||
|
val tocUiUseReplace: Boolean = false,
|
||||||
|
val tocCountWords: Boolean = true,
|
||||||
|
val readStyleSelect: Int = 0,
|
||||||
|
val comicStyleSelect: Int = 0,
|
||||||
|
val shareLayout: Boolean = false,
|
||||||
|
val readBarStyleFollowPage: Boolean = false,
|
||||||
|
val readBarStyle: Int = 0,
|
||||||
|
val clickActionTL: Int = 2,
|
||||||
|
val clickActionTC: Int = 2,
|
||||||
|
val clickActionTR: Int = 1,
|
||||||
|
val clickActionML: Int = 2,
|
||||||
|
val clickActionMC: Int = 0,
|
||||||
|
val clickActionMR: Int = 1,
|
||||||
|
val clickActionBL: Int = 2,
|
||||||
|
val clickActionBC: Int = 1,
|
||||||
|
val clickActionBR: Int = 1,
|
||||||
|
val fontFolder: String = "",
|
||||||
|
val readMenuBgColor: Int = 0,
|
||||||
|
val readMenuAccentColor: Int = 0,
|
||||||
|
val readMenuContainerColor: Int = 0,
|
||||||
|
val readMenuBgColorNight: Int = 0,
|
||||||
|
val readMenuAccentColorNight: Int = 0,
|
||||||
|
val readMenuContainerColorNight: Int = 0,
|
||||||
|
val readMenuColorMode: Int = 1,
|
||||||
|
val readMenuIconShowText: Boolean = true,
|
||||||
|
val readMenuIconStyle: Int = 0,
|
||||||
|
val readMenuIconItemsPerRow: Int = 5,
|
||||||
|
val readMenuIconRowCount: Int = 1,
|
||||||
|
val readMenuBottomCornerRadius: Int = 0,
|
||||||
|
val readMenuFloatingBottomBar: Boolean = false,
|
||||||
|
val readMenuTopBarBlurMode: Int = ReadMenuBlurMode.None,
|
||||||
|
val readMenuBottomBarBlurMode: Int = ReadMenuBlurMode.None,
|
||||||
|
val readMenuTopBarLiquidGlassButtons: Boolean = false,
|
||||||
|
val readMenuBottomBarLiquidGlassButtons: Boolean = false,
|
||||||
|
val readMenuTopBarBlurStyle: Int = ReadMenuBlurStyle.Progressive,
|
||||||
|
val readMenuBottomBarBlurStyle: Int = ReadMenuBlurStyle.Solid,
|
||||||
|
val readMenuBlurRadius: Int = 24,
|
||||||
|
val readMenuBlurAlpha: Int = 60,
|
||||||
|
val readMenuLensRadius: Float = 24f,
|
||||||
|
val readMenuBorderWidth: Int = 0,
|
||||||
|
val readMenuBorderColor: Int = 0,
|
||||||
|
val readMenuBorderColorNight: Int = 0,
|
||||||
|
val readMenuCustomIcons: String = "",
|
||||||
|
val titleBarCustomIcons: String = "",
|
||||||
|
val titleBarIconPosition: Int = 0,
|
||||||
|
val showTitleBarIcons: Boolean = true,
|
||||||
|
val chineseConverterType: Int = 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
class ReadSettingsRepository(
|
||||||
|
private val context: Context,
|
||||||
|
private val settingsRepository: SettingsRepository
|
||||||
|
) {
|
||||||
|
|
||||||
|
val preferences: Flow<ReadPreferences> = context.dataStore.data
|
||||||
|
.catch { exception ->
|
||||||
|
if (exception is IOException) {
|
||||||
|
emit(emptyPreferences())
|
||||||
|
} else {
|
||||||
|
throw exception
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.map { preferences ->
|
||||||
|
preferences.toReadPreferences()
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setScreenOrientation(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.screenOrientation, value)
|
||||||
|
|
||||||
|
suspend fun setKeepLight(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.keepLight, value)
|
||||||
|
|
||||||
|
suspend fun setHideStatusBar(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.hideStatusBar, value)
|
||||||
|
|
||||||
|
suspend fun setHideNavigationBar(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.hideNavigationBar, value)
|
||||||
|
|
||||||
|
suspend fun setPaddingDisplayCutouts(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.paddingDisplayCutouts, value)
|
||||||
|
|
||||||
|
suspend fun setTitleBarMode(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.titleBarMode, value)
|
||||||
|
|
||||||
|
suspend fun setMenuAlpha(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.menuAlpha, value)
|
||||||
|
|
||||||
|
suspend fun setReadBodyToLh(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readBodyToLh, value)
|
||||||
|
|
||||||
|
suspend fun setDefaultSourceChangeAll(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.defaultSourceChangeAll, value)
|
||||||
|
|
||||||
|
suspend fun setTextFullJustify(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.textFullJustify, value)
|
||||||
|
|
||||||
|
suspend fun setTextBottomJustify(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.textBottomJustify, value)
|
||||||
|
|
||||||
|
suspend fun setAdaptSpecialStyle(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.adaptSpecialStyle, value)
|
||||||
|
|
||||||
|
suspend fun setUseZhLayout(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.useZhLayout, value)
|
||||||
|
|
||||||
|
suspend fun setShowBrightnessView(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.showBrightnessView, value)
|
||||||
|
|
||||||
|
suspend fun setUseUnderline(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.useUnderline, value)
|
||||||
|
|
||||||
|
suspend fun setReadSliderMode(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.readSliderMode, value)
|
||||||
|
|
||||||
|
suspend fun setDoubleHorizontalPage(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.doublePageHorizontal, value)
|
||||||
|
|
||||||
|
suspend fun setProgressBarBehavior(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.progressBarBehavior, value)
|
||||||
|
|
||||||
|
suspend fun setMouseWheelPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.mouseWheelPage, value)
|
||||||
|
|
||||||
|
suspend fun setVolumeKeyPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.volumeKeyPage, value)
|
||||||
|
|
||||||
|
suspend fun setVolumeKeyPageOnPlay(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.volumeKeyPageOnPlay, value)
|
||||||
|
|
||||||
|
suspend fun setKeyPageOnLongPress(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.keyPageOnLongPress, value)
|
||||||
|
|
||||||
|
suspend fun setPageTouchSlop(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.pageTouchSlop, value)
|
||||||
|
|
||||||
|
suspend fun setSliderVibrator(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.sliderVibrator, value)
|
||||||
|
|
||||||
|
suspend fun setSelectVibrator(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.selectVibrator, value)
|
||||||
|
|
||||||
|
suspend fun setAutoChangeSource(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.autoChangeSource, value)
|
||||||
|
|
||||||
|
suspend fun setSelectText(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.selectText, value)
|
||||||
|
|
||||||
|
suspend fun setNoAnimScrollPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.noAnimScrollPage, value)
|
||||||
|
|
||||||
|
suspend fun setClickImgWay(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.clickImgWay, value)
|
||||||
|
|
||||||
|
suspend fun setOptimizeRender(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.optimizeRender, value)
|
||||||
|
|
||||||
|
suspend fun setDisableReturnKey(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.disableReturnKey, value)
|
||||||
|
|
||||||
|
suspend fun setExpandTextMenu(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.expandTextMenu, value)
|
||||||
|
|
||||||
|
suspend fun setShowReadTitleAddition(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.showReadTitleAddition, value)
|
||||||
|
|
||||||
|
suspend fun setAutoReadSpeed(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.autoReadSpeed, value)
|
||||||
|
|
||||||
|
suspend fun setPageKeys(prevKeys: String, nextKeys: String) {
|
||||||
|
settingsRepository.putStrings(
|
||||||
|
mapOf(
|
||||||
|
PreferKey.prevKeys to prevKeys,
|
||||||
|
PreferKey.nextKeys to nextKeys
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
suspend fun setTocUiUseReplace(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.tocUiUseReplace, value)
|
||||||
|
|
||||||
|
suspend fun setTocCountWords(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.tocCountWords, value)
|
||||||
|
|
||||||
|
suspend fun setReadStyleSelect(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readStyleSelect, value)
|
||||||
|
|
||||||
|
suspend fun setComicStyleSelect(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.comicStyleSelect, value)
|
||||||
|
|
||||||
|
suspend fun setShareLayout(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.shareLayout, value)
|
||||||
|
|
||||||
|
suspend fun setReadBarStyleFollowPage(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readBarStyleFollowPage, value)
|
||||||
|
|
||||||
|
suspend fun setReadBarStyle(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readBarStyle, value.coerceIn(0, 2))
|
||||||
|
|
||||||
|
suspend fun setClickAction(key: String, value: Int) =
|
||||||
|
settingsRepository.putInt(key, value)
|
||||||
|
|
||||||
|
suspend fun setFontFolder(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.fontFolder, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuBgColor(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBgColor, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuAccentColor(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuAccentColor, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuContainerColor(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuContainerColor, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuBgColorNight(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBgColorNight, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuAccentColorNight(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuAccentColorNight, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuContainerColorNight(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuContainerColorNight, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuColorMode(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuColorMode, value.coerceIn(0, 1))
|
||||||
|
|
||||||
|
suspend fun setReadMenuIconShowText(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readMenuIconShowText, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuIconStyle(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuIconStyle, value.coerceIn(0, 2))
|
||||||
|
|
||||||
|
suspend fun setReadMenuIconItemsPerRow(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuIconItemsPerRow, value.coerceIn(2, 8))
|
||||||
|
|
||||||
|
suspend fun setReadMenuIconRowCount(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuIconRowCount, value.coerceIn(1, 2))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBottomCornerRadius(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBottomCornerRadius, value.coerceIn(0, 32))
|
||||||
|
|
||||||
|
suspend fun setReadMenuFloatingBottomBar(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readMenuFloatingBottomBar, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuTopBarBlurMode(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuTopBarBlurMode, value.coerceIn(0, 2))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBottomBarBlurMode(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBottomBarBlurMode, value.coerceIn(0, 2))
|
||||||
|
|
||||||
|
suspend fun setReadMenuTopBarLiquidGlassButtons(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readMenuTopBarLiquidGlassButtons, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuBottomBarLiquidGlassButtons(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.readMenuBottomBarLiquidGlassButtons, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuTopBarBlurStyle(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuTopBarBlurStyle, value.coerceIn(0, 1))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBottomBarBlurStyle(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBottomBarBlurStyle, value.coerceIn(0, 1))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBlurRadius(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBlurRadius, value.coerceIn(0, 32))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBlurAlpha(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBlurAlpha, value.coerceIn(0, 100))
|
||||||
|
|
||||||
|
suspend fun setReadMenuLensRadius(value: Float) =
|
||||||
|
settingsRepository.putFloat(PreferKey.readMenuLensRadius, value.coerceIn(0f, 48f))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBorderWidth(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBorderWidth, value.coerceIn(0, 4))
|
||||||
|
|
||||||
|
suspend fun setReadMenuBorderColor(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBorderColor, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuBorderColorNight(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.readMenuBorderColorNight, value)
|
||||||
|
|
||||||
|
suspend fun setReadMenuCustomIcons(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.readMenuCustomIcons, value)
|
||||||
|
|
||||||
|
suspend fun setTitleBarCustomIcons(value: String) =
|
||||||
|
settingsRepository.putString(PreferKey.titleBarCustomIcons, value)
|
||||||
|
|
||||||
|
suspend fun setTitleBarIconPosition(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.titleBarIconPosition, value.coerceIn(0, 3))
|
||||||
|
|
||||||
|
suspend fun setShowTitleBarIcons(value: Boolean) =
|
||||||
|
settingsRepository.putBoolean(PreferKey.showTitleBarIcons, value)
|
||||||
|
|
||||||
|
suspend fun setChineseConverterType(value: Int) =
|
||||||
|
settingsRepository.putInt(PreferKey.chineseConverterType, value)
|
||||||
|
|
||||||
|
suspend fun setStyleSelect(isComic: Boolean, value: Int) {
|
||||||
|
if (isComic) {
|
||||||
|
setComicStyleSelect(value)
|
||||||
|
} else {
|
||||||
|
setReadStyleSelect(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Preferences.toReadPreferences(): ReadPreferences {
|
||||||
|
val readStyleSelect = this[Keys.ReadStyleSelect] ?: 0
|
||||||
|
return ReadPreferences(
|
||||||
|
screenOrientation = this[Keys.ScreenOrientation] ?: "0",
|
||||||
|
keepLight = this[Keys.KeepLight] ?: "0",
|
||||||
|
hideStatusBar = this[Keys.HideStatusBar] ?: false,
|
||||||
|
hideNavigationBar = this[Keys.HideNavigationBar] ?: false,
|
||||||
|
paddingDisplayCutouts = this[Keys.PaddingDisplayCutouts] ?: false,
|
||||||
|
titleBarMode = this[Keys.TitleBarMode] ?: "1",
|
||||||
|
menuAlpha = this[Keys.MenuAlpha] ?: 100,
|
||||||
|
readBodyToLh = this[Keys.ReadBodyToLh] ?: true,
|
||||||
|
defaultSourceChangeAll = this[Keys.DefaultSourceChangeAll] ?: true,
|
||||||
|
textFullJustify = this[Keys.TextFullJustify] ?: true,
|
||||||
|
textBottomJustify = this[Keys.TextBottomJustify] ?: true,
|
||||||
|
adaptSpecialStyle = this[Keys.AdaptSpecialStyle] ?: true,
|
||||||
|
useZhLayout = this[Keys.UseZhLayout] ?: false,
|
||||||
|
showBrightnessView = this[Keys.ShowBrightnessView] ?: true,
|
||||||
|
useUnderline = this[Keys.UseUnderline] ?: false,
|
||||||
|
readSliderMode = this[Keys.ReadSliderMode] ?: "0",
|
||||||
|
doubleHorizontalPage = this[Keys.DoubleHorizontalPage] ?: "0",
|
||||||
|
progressBarBehavior = this[Keys.ProgressBarBehavior] ?: "page",
|
||||||
|
mouseWheelPage = this[Keys.MouseWheelPage] ?: true,
|
||||||
|
volumeKeyPage = this[Keys.VolumeKeyPage] ?: true,
|
||||||
|
volumeKeyPageOnPlay = this[Keys.VolumeKeyPageOnPlay] ?: true,
|
||||||
|
keyPageOnLongPress = this[Keys.KeyPageOnLongPress] ?: false,
|
||||||
|
pageTouchSlop = this[Keys.PageTouchSlop] ?: 0,
|
||||||
|
sliderVibrator = this[Keys.SliderVibrator] ?: false,
|
||||||
|
selectVibrator = this[Keys.SelectVibrator] ?: false,
|
||||||
|
autoChangeSource = this[Keys.AutoChangeSource] ?: true,
|
||||||
|
selectText = this[Keys.SelectText] ?: true,
|
||||||
|
noAnimScrollPage = this[Keys.NoAnimScrollPage] ?: false,
|
||||||
|
clickImgWay = this[Keys.ClickImgWay] ?: "2",
|
||||||
|
optimizeRender = this[Keys.OptimizeRender] ?: false,
|
||||||
|
disableReturnKey = this[Keys.DisableReturnKey] ?: false,
|
||||||
|
expandTextMenu = this[Keys.ExpandTextMenu] ?: false,
|
||||||
|
showReadTitleAddition = this[Keys.ShowReadTitleAddition] ?: true,
|
||||||
|
autoReadSpeed = this[Keys.AutoReadSpeed] ?: 10,
|
||||||
|
prevKeys = this[Keys.PrevKeys] ?: "",
|
||||||
|
nextKeys = this[Keys.NextKeys] ?: "",
|
||||||
|
tocUiUseReplace = this[Keys.TocUiUseReplace] ?: false,
|
||||||
|
tocCountWords = this[Keys.TocCountWords] ?: true,
|
||||||
|
readStyleSelect = readStyleSelect,
|
||||||
|
comicStyleSelect = this[Keys.ComicStyleSelect] ?: readStyleSelect,
|
||||||
|
shareLayout = this[Keys.ShareLayout] ?: false,
|
||||||
|
readBarStyleFollowPage = this[Keys.ReadBarStyleFollowPage] ?: false,
|
||||||
|
readBarStyle = this[Keys.ReadBarStyle] ?: 0,
|
||||||
|
clickActionTL = this[Keys.ClickActionTL] ?: 2,
|
||||||
|
clickActionTC = this[Keys.ClickActionTC] ?: 2,
|
||||||
|
clickActionTR = this[Keys.ClickActionTR] ?: 1,
|
||||||
|
clickActionML = this[Keys.ClickActionML] ?: 2,
|
||||||
|
clickActionMC = this[Keys.ClickActionMC] ?: 0,
|
||||||
|
clickActionMR = this[Keys.ClickActionMR] ?: 1,
|
||||||
|
clickActionBL = this[Keys.ClickActionBL] ?: 2,
|
||||||
|
clickActionBC = this[Keys.ClickActionBC] ?: 1,
|
||||||
|
clickActionBR = this[Keys.ClickActionBR] ?: 1,
|
||||||
|
fontFolder = this[Keys.FontFolder] ?: "",
|
||||||
|
readMenuBgColor = this[Keys.ReadMenuBgColor] ?: 0,
|
||||||
|
readMenuAccentColor = this[Keys.ReadMenuAccentColor] ?: 0,
|
||||||
|
readMenuContainerColor = this[Keys.ReadMenuContainerColor] ?: 0,
|
||||||
|
readMenuBgColorNight = this[Keys.ReadMenuBgColorNight] ?: 0,
|
||||||
|
readMenuAccentColorNight = this[Keys.ReadMenuAccentColorNight] ?: 0,
|
||||||
|
readMenuContainerColorNight = this[Keys.ReadMenuContainerColorNight] ?: 0,
|
||||||
|
readMenuColorMode = this[Keys.ReadMenuColorMode] ?: 1,
|
||||||
|
readMenuIconShowText = this[Keys.ReadMenuIconShowText] ?: true,
|
||||||
|
readMenuIconStyle = this[Keys.ReadMenuIconStyle] ?: 0,
|
||||||
|
readMenuIconItemsPerRow = this[Keys.ReadMenuIconItemsPerRow] ?: 5,
|
||||||
|
readMenuIconRowCount = this[Keys.ReadMenuIconRowCount] ?: 1,
|
||||||
|
readMenuBottomCornerRadius = this[Keys.ReadMenuBottomCornerRadius] ?: 0,
|
||||||
|
readMenuFloatingBottomBar = this[Keys.ReadMenuFloatingBottomBar] ?: false,
|
||||||
|
readMenuTopBarBlurMode = this[Keys.ReadMenuTopBarBlurMode] ?: ReadMenuBlurMode.None,
|
||||||
|
readMenuBottomBarBlurMode = this[Keys.ReadMenuBottomBarBlurMode]
|
||||||
|
?: ReadMenuBlurMode.None,
|
||||||
|
readMenuTopBarLiquidGlassButtons = this[Keys.ReadMenuTopBarLiquidGlassButtons] ?: false,
|
||||||
|
readMenuBottomBarLiquidGlassButtons = this[Keys.ReadMenuBottomBarLiquidGlassButtons]
|
||||||
|
?: false,
|
||||||
|
readMenuTopBarBlurStyle = this[Keys.ReadMenuTopBarBlurStyle]
|
||||||
|
?: ReadMenuBlurStyle.Progressive,
|
||||||
|
readMenuBottomBarBlurStyle = this[Keys.ReadMenuBottomBarBlurStyle]
|
||||||
|
?: ReadMenuBlurStyle.Solid,
|
||||||
|
readMenuBlurRadius = this[Keys.ReadMenuBlurRadius] ?: 24,
|
||||||
|
readMenuBlurAlpha = this[Keys.ReadMenuBlurAlpha] ?: 60,
|
||||||
|
readMenuLensRadius = this[Keys.ReadMenuLensRadius] ?: 24f,
|
||||||
|
readMenuBorderWidth = this[Keys.ReadMenuBorderWidth] ?: 0,
|
||||||
|
readMenuBorderColor = this[Keys.ReadMenuBorderColor] ?: 0,
|
||||||
|
readMenuBorderColorNight = this[Keys.ReadMenuBorderColorNight] ?: 0,
|
||||||
|
readMenuCustomIcons = this[Keys.ReadMenuCustomIcons] ?: "",
|
||||||
|
titleBarCustomIcons = this[Keys.TitleBarCustomIcons] ?: "",
|
||||||
|
titleBarIconPosition = this[Keys.TitleBarIconPosition] ?: 0,
|
||||||
|
showTitleBarIcons = this[Keys.ShowTitleBarIcons] ?: true,
|
||||||
|
chineseConverterType = this[Keys.ChineseConverterType] ?: 0,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private object Keys {
|
||||||
|
val ScreenOrientation = stringPreferencesKey(PreferKey.screenOrientation)
|
||||||
|
val KeepLight = stringPreferencesKey(PreferKey.keepLight)
|
||||||
|
val HideStatusBar = booleanPreferencesKey(PreferKey.hideStatusBar)
|
||||||
|
val HideNavigationBar = booleanPreferencesKey(PreferKey.hideNavigationBar)
|
||||||
|
val PaddingDisplayCutouts = booleanPreferencesKey(PreferKey.paddingDisplayCutouts)
|
||||||
|
val TitleBarMode = stringPreferencesKey(PreferKey.titleBarMode)
|
||||||
|
val MenuAlpha = intPreferencesKey(PreferKey.menuAlpha)
|
||||||
|
val ReadBodyToLh = booleanPreferencesKey(PreferKey.readBodyToLh)
|
||||||
|
val DefaultSourceChangeAll = booleanPreferencesKey(PreferKey.defaultSourceChangeAll)
|
||||||
|
val TextFullJustify = booleanPreferencesKey(PreferKey.textFullJustify)
|
||||||
|
val TextBottomJustify = booleanPreferencesKey(PreferKey.textBottomJustify)
|
||||||
|
val AdaptSpecialStyle = booleanPreferencesKey(PreferKey.adaptSpecialStyle)
|
||||||
|
val UseZhLayout = booleanPreferencesKey(PreferKey.useZhLayout)
|
||||||
|
val ShowBrightnessView = booleanPreferencesKey(PreferKey.showBrightnessView)
|
||||||
|
val UseUnderline = booleanPreferencesKey(PreferKey.useUnderline)
|
||||||
|
val ReadSliderMode = stringPreferencesKey(PreferKey.readSliderMode)
|
||||||
|
val DoubleHorizontalPage = stringPreferencesKey(PreferKey.doublePageHorizontal)
|
||||||
|
val ProgressBarBehavior = stringPreferencesKey(PreferKey.progressBarBehavior)
|
||||||
|
val MouseWheelPage = booleanPreferencesKey(PreferKey.mouseWheelPage)
|
||||||
|
val VolumeKeyPage = booleanPreferencesKey(PreferKey.volumeKeyPage)
|
||||||
|
val VolumeKeyPageOnPlay = booleanPreferencesKey(PreferKey.volumeKeyPageOnPlay)
|
||||||
|
val KeyPageOnLongPress = booleanPreferencesKey(PreferKey.keyPageOnLongPress)
|
||||||
|
val PageTouchSlop = intPreferencesKey(PreferKey.pageTouchSlop)
|
||||||
|
val SliderVibrator = booleanPreferencesKey(PreferKey.sliderVibrator)
|
||||||
|
val SelectVibrator = booleanPreferencesKey(PreferKey.selectVibrator)
|
||||||
|
val AutoChangeSource = booleanPreferencesKey(PreferKey.autoChangeSource)
|
||||||
|
val SelectText = booleanPreferencesKey(PreferKey.selectText)
|
||||||
|
val NoAnimScrollPage = booleanPreferencesKey(PreferKey.noAnimScrollPage)
|
||||||
|
val ClickImgWay = stringPreferencesKey(PreferKey.clickImgWay)
|
||||||
|
val OptimizeRender = booleanPreferencesKey(PreferKey.optimizeRender)
|
||||||
|
val DisableReturnKey = booleanPreferencesKey(PreferKey.disableReturnKey)
|
||||||
|
val ExpandTextMenu = booleanPreferencesKey(PreferKey.expandTextMenu)
|
||||||
|
val ShowReadTitleAddition = booleanPreferencesKey(PreferKey.showReadTitleAddition)
|
||||||
|
val AutoReadSpeed = intPreferencesKey(PreferKey.autoReadSpeed)
|
||||||
|
val PrevKeys = stringPreferencesKey(PreferKey.prevKeys)
|
||||||
|
val NextKeys = stringPreferencesKey(PreferKey.nextKeys)
|
||||||
|
val TocUiUseReplace = booleanPreferencesKey(PreferKey.tocUiUseReplace)
|
||||||
|
val TocCountWords = booleanPreferencesKey(PreferKey.tocCountWords)
|
||||||
|
val ReadStyleSelect = intPreferencesKey(PreferKey.readStyleSelect)
|
||||||
|
val ComicStyleSelect = intPreferencesKey(PreferKey.comicStyleSelect)
|
||||||
|
val ShareLayout = booleanPreferencesKey(PreferKey.shareLayout)
|
||||||
|
val ReadBarStyleFollowPage = booleanPreferencesKey(PreferKey.readBarStyleFollowPage)
|
||||||
|
val ReadBarStyle = intPreferencesKey(PreferKey.readBarStyle)
|
||||||
|
val ClickActionTL = intPreferencesKey(PreferKey.clickActionTL)
|
||||||
|
val ClickActionTC = intPreferencesKey(PreferKey.clickActionTC)
|
||||||
|
val ClickActionTR = intPreferencesKey(PreferKey.clickActionTR)
|
||||||
|
val ClickActionML = intPreferencesKey(PreferKey.clickActionML)
|
||||||
|
val ClickActionMC = intPreferencesKey(PreferKey.clickActionMC)
|
||||||
|
val ClickActionMR = intPreferencesKey(PreferKey.clickActionMR)
|
||||||
|
val ClickActionBL = intPreferencesKey(PreferKey.clickActionBL)
|
||||||
|
val ClickActionBC = intPreferencesKey(PreferKey.clickActionBC)
|
||||||
|
val ClickActionBR = intPreferencesKey(PreferKey.clickActionBR)
|
||||||
|
val FontFolder = stringPreferencesKey(PreferKey.fontFolder)
|
||||||
|
val ReadMenuBgColor = intPreferencesKey(PreferKey.readMenuBgColor)
|
||||||
|
val ReadMenuAccentColor = intPreferencesKey(PreferKey.readMenuAccentColor)
|
||||||
|
val ReadMenuContainerColor = intPreferencesKey(PreferKey.readMenuContainerColor)
|
||||||
|
val ReadMenuBgColorNight = intPreferencesKey(PreferKey.readMenuBgColorNight)
|
||||||
|
val ReadMenuAccentColorNight = intPreferencesKey(PreferKey.readMenuAccentColorNight)
|
||||||
|
val ReadMenuContainerColorNight = intPreferencesKey(PreferKey.readMenuContainerColorNight)
|
||||||
|
val ReadMenuColorMode = intPreferencesKey(PreferKey.readMenuColorMode)
|
||||||
|
val ReadMenuIconShowText = booleanPreferencesKey(PreferKey.readMenuIconShowText)
|
||||||
|
val ReadMenuIconStyle = intPreferencesKey(PreferKey.readMenuIconStyle)
|
||||||
|
val ReadMenuIconItemsPerRow = intPreferencesKey(PreferKey.readMenuIconItemsPerRow)
|
||||||
|
val ReadMenuIconRowCount = intPreferencesKey(PreferKey.readMenuIconRowCount)
|
||||||
|
val ReadMenuBottomCornerRadius = intPreferencesKey(PreferKey.readMenuBottomCornerRadius)
|
||||||
|
val ReadMenuFloatingBottomBar = booleanPreferencesKey(PreferKey.readMenuFloatingBottomBar)
|
||||||
|
val ReadMenuTopBarBlurMode = intPreferencesKey(PreferKey.readMenuTopBarBlurMode)
|
||||||
|
val ReadMenuBottomBarBlurMode = intPreferencesKey(PreferKey.readMenuBottomBarBlurMode)
|
||||||
|
val ReadMenuTopBarLiquidGlassButtons =
|
||||||
|
booleanPreferencesKey(PreferKey.readMenuTopBarLiquidGlassButtons)
|
||||||
|
val ReadMenuBottomBarLiquidGlassButtons =
|
||||||
|
booleanPreferencesKey(PreferKey.readMenuBottomBarLiquidGlassButtons)
|
||||||
|
val ReadMenuTopBarBlurStyle = intPreferencesKey(PreferKey.readMenuTopBarBlurStyle)
|
||||||
|
val ReadMenuBottomBarBlurStyle = intPreferencesKey(PreferKey.readMenuBottomBarBlurStyle)
|
||||||
|
val ReadMenuBlurRadius = intPreferencesKey(PreferKey.readMenuBlurRadius)
|
||||||
|
val ReadMenuBlurAlpha = intPreferencesKey(PreferKey.readMenuBlurAlpha)
|
||||||
|
val ReadMenuLensRadius = floatPreferencesKey(PreferKey.readMenuLensRadius)
|
||||||
|
val ReadMenuBorderWidth = intPreferencesKey(PreferKey.readMenuBorderWidth)
|
||||||
|
val ReadMenuBorderColor = intPreferencesKey(PreferKey.readMenuBorderColor)
|
||||||
|
val ReadMenuBorderColorNight = intPreferencesKey(PreferKey.readMenuBorderColorNight)
|
||||||
|
val ReadMenuCustomIcons = stringPreferencesKey(PreferKey.readMenuCustomIcons)
|
||||||
|
val TitleBarCustomIcons = stringPreferencesKey(PreferKey.titleBarCustomIcons)
|
||||||
|
val TitleBarIconPosition = intPreferencesKey(PreferKey.titleBarIconPosition)
|
||||||
|
val ShowTitleBarIcons = booleanPreferencesKey(PreferKey.showTitleBarIcons)
|
||||||
|
val ChineseConverterType = intPreferencesKey(PreferKey.chineseConverterType)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,255 @@
|
|||||||
|
package io.legado.app.data.repository
|
||||||
|
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.help.DefaultData
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import io.legado.app.help.config.ReadStyleResolver
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.compress.ZipUtils
|
||||||
|
import io.legado.app.utils.createFolderReplace
|
||||||
|
import io.legado.app.utils.externalCache
|
||||||
|
import io.legado.app.utils.externalFiles
|
||||||
|
import io.legado.app.utils.fromJsonArray
|
||||||
|
import io.legado.app.utils.fromJsonObject
|
||||||
|
import io.legado.app.utils.getFile
|
||||||
|
import io.legado.app.utils.printOnDebug
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class ReadStyleRepository {
|
||||||
|
|
||||||
|
val configFilePath: String =
|
||||||
|
FileUtils.getPath(appCtx.filesDir, ReadBookConfig.configFileName)
|
||||||
|
val shareConfigFilePath: String =
|
||||||
|
FileUtils.getPath(appCtx.filesDir, ReadBookConfig.shareConfigFileName)
|
||||||
|
|
||||||
|
fun readConfigs(): List<ReadBookConfig.Config> {
|
||||||
|
val configFile = File(configFilePath)
|
||||||
|
if (configFile.exists()) {
|
||||||
|
try {
|
||||||
|
return GSON.fromJsonArray<ReadBookConfig.Config>(configFile.readText()).getOrThrow()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
AppLog.put("读取排版配置文件出错", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return DefaultData.readConfigs
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readShareConfig(fallbackConfig: ReadBookConfig.Config): ReadBookConfig.Config {
|
||||||
|
val configFile = File(shareConfigFilePath)
|
||||||
|
if (configFile.exists()) {
|
||||||
|
try {
|
||||||
|
return GSON.fromJsonObject<ReadBookConfig.Config>(configFile.readText()).getOrThrow()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fallbackConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(
|
||||||
|
configs: List<ReadBookConfig.Config>,
|
||||||
|
shareConfig: ReadBookConfig.Config
|
||||||
|
) {
|
||||||
|
GSON.toJson(configs).let {
|
||||||
|
FileUtils.delete(configFilePath)
|
||||||
|
FileUtils.createFileIfNotExist(configFilePath).writeText(it)
|
||||||
|
}
|
||||||
|
GSON.toJson(shareConfig).let {
|
||||||
|
FileUtils.delete(shareConfigFilePath)
|
||||||
|
FileUtils.createFileIfNotExist(shareConfigFilePath).writeText(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getAllPicBgStr(configs: List<ReadBookConfig.Config>): ArrayList<String> {
|
||||||
|
val list = arrayListOf<String>()
|
||||||
|
configs.forEach {
|
||||||
|
if (it.bgType == 2) {
|
||||||
|
list.add(it.bgStr)
|
||||||
|
}
|
||||||
|
if (it.bgTypeNight == 2) {
|
||||||
|
list.add(it.bgStrNight)
|
||||||
|
}
|
||||||
|
if (it.bgTypeEInk == 2) {
|
||||||
|
list.add(it.bgStrEInk)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
||||||
|
fun clearBgAndCache(configs: List<ReadBookConfig.Config>) {
|
||||||
|
val bgs = hashSetOf<String>()
|
||||||
|
configs.forEach { config ->
|
||||||
|
repeat(3) {
|
||||||
|
config.getBgPath(it)?.let { path ->
|
||||||
|
bgs.add(path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
appCtx.externalFiles.getFile("bg").listFiles()?.forEach {
|
||||||
|
if (!bgs.contains(it.absolutePath)) {
|
||||||
|
it.delete()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
FileUtils.delete(appCtx.externalCache.getFile("readConfig"))
|
||||||
|
FileUtils.delete(FileUtils.getPath(appCtx.externalCache, "readConfig.zip"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveBackgroundImage(inputStream: InputStream, displayName: String?): String {
|
||||||
|
val bgDir = appCtx.externalFiles.getFile("bg")
|
||||||
|
bgDir.mkdirs()
|
||||||
|
val safeName = displayName
|
||||||
|
?.let { File(it).name }
|
||||||
|
?.takeIf { it.isNotBlank() }
|
||||||
|
?: "read_bg.jpg"
|
||||||
|
val baseName = File(safeName).nameWithoutExtension.ifBlank { "read_bg" }
|
||||||
|
val extension = File(safeName).extension.ifBlank { "jpg" }
|
||||||
|
val bgFile = File(bgDir, "${baseName}_${System.currentTimeMillis()}.$extension")
|
||||||
|
if (!FileUtils.writeInputStream(bgFile, inputStream)) {
|
||||||
|
error("save read background image failed")
|
||||||
|
}
|
||||||
|
return bgFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
fun export(config: ReadBookConfig.Config): ByteArray {
|
||||||
|
val exportDir = appCtx.externalCache.getFile("readConfigExport")
|
||||||
|
exportDir.createFolderReplace()
|
||||||
|
val exportConfig = config.copy(
|
||||||
|
regexColorRules = ArrayList(config.regexColorRules.map { it.copy() })
|
||||||
|
)
|
||||||
|
val exportFiles = arrayListOf<File>()
|
||||||
|
|
||||||
|
addBackgroundFile(exportDir, exportConfig, 0, exportFiles)
|
||||||
|
addBackgroundFile(exportDir, exportConfig, 1, exportFiles)
|
||||||
|
addBackgroundFile(exportDir, exportConfig, 2, exportFiles)
|
||||||
|
exportConfig.textFont = addAssetFile(exportDir, exportConfig.textFont, exportFiles)
|
||||||
|
exportConfig.titleFont = addAssetFile(exportDir, exportConfig.titleFont, exportFiles)
|
||||||
|
|
||||||
|
val configFile = exportDir.getFile(ReadBookConfig.configFileName)
|
||||||
|
configFile.writeText(GSON.toJson(exportConfig))
|
||||||
|
exportFiles.add(configFile)
|
||||||
|
|
||||||
|
val zipFile = appCtx.externalCache.getFile("readConfig.zip")
|
||||||
|
FileUtils.delete(zipFile)
|
||||||
|
ZipUtils.zipFiles(exportFiles, zipFile)
|
||||||
|
return zipFile.readBytes()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun import(byteArray: ByteArray): ReadBookConfig.Config {
|
||||||
|
val configZipPath = FileUtils.getPath(appCtx.externalCache, "readConfig.zip")
|
||||||
|
FileUtils.delete(configZipPath)
|
||||||
|
val zipFile = FileUtils.createFileIfNotExist(configZipPath)
|
||||||
|
zipFile.writeBytes(byteArray)
|
||||||
|
val configDir = appCtx.externalCache.getFile("readConfig")
|
||||||
|
configDir.createFolderReplace()
|
||||||
|
ZipUtils.unZipToPath(zipFile, configDir)
|
||||||
|
val configFile = configDir.getFile(ReadBookConfig.configFileName)
|
||||||
|
val config: ReadBookConfig.Config =
|
||||||
|
GSON.fromJsonObject<ReadBookConfig.Config>(configFile.readText()).getOrThrow()
|
||||||
|
|
||||||
|
config.textFont = importFont(configDir, config.textFont)
|
||||||
|
config.titleFont = importFont(configDir, config.titleFont)
|
||||||
|
|
||||||
|
if (config.bgType == 2) {
|
||||||
|
val bgName = FileUtils.getName(config.bgStr)
|
||||||
|
config.bgStr = bgName
|
||||||
|
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
|
||||||
|
if (!FileUtils.exist(bgPath)) {
|
||||||
|
val bgFile = configDir.getFile(bgName)
|
||||||
|
if (bgFile.exists()) {
|
||||||
|
bgFile.copyTo(File(bgPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.bgStr = bgPath
|
||||||
|
} else if (config.bgTypeNight == 0) {
|
||||||
|
config.bgStrNight.toColorInt()
|
||||||
|
}
|
||||||
|
if (config.bgTypeNight == 2) {
|
||||||
|
val bgName = FileUtils.getName(config.bgStrNight)
|
||||||
|
config.bgStrNight = bgName
|
||||||
|
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
|
||||||
|
if (!FileUtils.exist(bgPath)) {
|
||||||
|
val bgFile = configDir.getFile(bgName)
|
||||||
|
if (bgFile.exists()) {
|
||||||
|
bgFile.copyTo(File(bgPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.bgStrNight = bgPath
|
||||||
|
}
|
||||||
|
if (config.bgTypeEInk == 2) {
|
||||||
|
val bgName = FileUtils.getName(config.bgStrEInk)
|
||||||
|
config.bgStrEInk = bgName
|
||||||
|
val bgPath = FileUtils.getPath(appCtx.externalFiles, "bg", bgName)
|
||||||
|
if (!FileUtils.exist(bgPath)) {
|
||||||
|
val bgFile = configDir.getFile(bgName)
|
||||||
|
if (bgFile.exists()) {
|
||||||
|
bgFile.copyTo(File(bgPath))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
config.bgStrEInk = bgPath
|
||||||
|
} else if (config.bgTypeEInk == 0) {
|
||||||
|
config.bgStrEInk.toColorInt()
|
||||||
|
}
|
||||||
|
config.curTextColor()
|
||||||
|
config.curTextAccentColor()
|
||||||
|
config.curTextShadowColor()
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addBackgroundFile(
|
||||||
|
exportDir: File,
|
||||||
|
config: ReadBookConfig.Config,
|
||||||
|
bgIndex: Int,
|
||||||
|
exportFiles: MutableList<File>
|
||||||
|
) {
|
||||||
|
val sourcePath = ReadStyleResolver.backgroundPath(config, bgIndex) ?: return
|
||||||
|
val exportedName = addAssetFile(exportDir, sourcePath, exportFiles)
|
||||||
|
if (exportedName.isBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
when (bgIndex) {
|
||||||
|
0 -> config.bgStr = exportedName
|
||||||
|
1 -> config.bgStrNight = exportedName
|
||||||
|
2 -> config.bgStrEInk = exportedName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addAssetFile(
|
||||||
|
exportDir: File,
|
||||||
|
sourcePath: String,
|
||||||
|
exportFiles: MutableList<File>
|
||||||
|
): String {
|
||||||
|
if (sourcePath.isBlank()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val source = File(sourcePath)
|
||||||
|
if (!source.exists() || !source.isFile) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val target = exportDir.getFile(source.name)
|
||||||
|
source.copyTo(target, overwrite = true)
|
||||||
|
if (exportFiles.none { it.absolutePath == target.absolutePath }) {
|
||||||
|
exportFiles.add(target)
|
||||||
|
}
|
||||||
|
return target.name
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importFont(configDir: File, fontName: String): String {
|
||||||
|
if (fontName.isEmpty()) {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
val fontPath = FileUtils.getPath(appCtx.externalFiles, "font", fontName)
|
||||||
|
val fontFile = configDir.getFile(fontName)
|
||||||
|
return if (fontFile.exists()) {
|
||||||
|
if (!FileUtils.exist(fontPath)) {
|
||||||
|
fontFile.copyTo(File(fontPath))
|
||||||
|
}
|
||||||
|
fontPath
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,6 +84,17 @@ class SettingsRepository(private val context: Context) {
|
|||||||
suspend fun putString(key: String, value: String) =
|
suspend fun putString(key: String, value: String) =
|
||||||
updatePreference(stringPreferencesKey(key), value)
|
updatePreference(stringPreferencesKey(key), value)
|
||||||
|
|
||||||
|
suspend fun putStrings(values: Map<String, String>) {
|
||||||
|
dataStore.edit { preferences ->
|
||||||
|
values.forEach { (key, value) ->
|
||||||
|
preferences[stringPreferencesKey(key)] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
values.forEach { (key, value) ->
|
||||||
|
syncToSharedPrefs(key, value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Int 类型的快捷访问
|
// Int 类型的快捷访问
|
||||||
fun getInt(key: String, defaultValue: Int = 0): Flow<Int> =
|
fun getInt(key: String, defaultValue: Int = 0): Flow<Int> =
|
||||||
getPreference(intPreferencesKey(key), defaultValue)
|
getPreference(intPreferencesKey(key), defaultValue)
|
||||||
|
|||||||
@@ -38,4 +38,5 @@ val appDatabaseModule = module {
|
|||||||
factory<ServerDao> { get<AppDatabase>().serverDao }
|
factory<ServerDao> { get<AppDatabase>().serverDao }
|
||||||
factory<HomepageModuleDao> { get<AppDatabase>().homepageModuleDao }
|
factory<HomepageModuleDao> { get<AppDatabase>().homepageModuleDao }
|
||||||
factory<HomepageCustomSetDao> { get<AppDatabase>().homepageCustomSetDao }
|
factory<HomepageCustomSetDao> { get<AppDatabase>().homepageCustomSetDao }
|
||||||
|
factory<HighlightRuleDao> { get<AppDatabase>().highlightRuleDao }
|
||||||
}
|
}
|
||||||
@@ -25,7 +25,11 @@ import io.legado.app.data.repository.ExploreRepositoryImpl
|
|||||||
import io.legado.app.data.repository.HomepageModulesRepository
|
import io.legado.app.data.repository.HomepageModulesRepository
|
||||||
import io.legado.app.data.repository.LlmTranslateRepositoryImpl
|
import io.legado.app.data.repository.LlmTranslateRepositoryImpl
|
||||||
import io.legado.app.data.repository.LocalBookRepository
|
import io.legado.app.data.repository.LocalBookRepository
|
||||||
|
import io.legado.app.data.repository.MangaSettingsRepository
|
||||||
|
import io.legado.app.data.repository.ReadBookStyleConfigRepository
|
||||||
|
import io.legado.app.data.repository.ReadAloudSettingsRepository
|
||||||
import io.legado.app.data.repository.ReadRecordRepository
|
import io.legado.app.data.repository.ReadRecordRepository
|
||||||
|
import io.legado.app.data.repository.ReadSettingsRepository
|
||||||
import io.legado.app.data.repository.RemoteBookRepository
|
import io.legado.app.data.repository.RemoteBookRepository
|
||||||
import io.legado.app.data.repository.RssRepository
|
import io.legado.app.data.repository.RssRepository
|
||||||
import io.legado.app.data.repository.SearchContentRepository
|
import io.legado.app.data.repository.SearchContentRepository
|
||||||
@@ -57,11 +61,13 @@ import io.legado.app.domain.usecase.AppStartupMaintenanceUseCase
|
|||||||
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
|
||||||
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
import io.legado.app.domain.usecase.CacheBookChaptersUseCase
|
||||||
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
import io.legado.app.domain.usecase.ChangeBookSourceUseCase
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceSearchUseCase
|
||||||
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
import io.legado.app.domain.usecase.ClearBookCacheUseCase
|
||||||
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
import io.legado.app.domain.usecase.DeleteBooksUseCase
|
||||||
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
||||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||||
import io.legado.app.domain.usecase.ExportBookshelfUseCase
|
import io.legado.app.domain.usecase.ExportBookshelfUseCase
|
||||||
|
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
||||||
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
||||||
import io.legado.app.domain.usecase.ImportBookshelfUseCase
|
import io.legado.app.domain.usecase.ImportBookshelfUseCase
|
||||||
import io.legado.app.domain.usecase.RefreshTocUseCase
|
import io.legado.app.domain.usecase.RefreshTocUseCase
|
||||||
@@ -85,6 +91,7 @@ import io.legado.app.ui.book.cache.manage.BookCacheManageViewModel
|
|||||||
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
import io.legado.app.ui.book.changecover.ChangeCoverViewModel
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
import io.legado.app.ui.book.changesource.ChangeBookSourceComposeViewModel
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
|
import io.legado.app.ui.book.changesource.ChangeBookSourceViewModel
|
||||||
|
import io.legado.app.ui.book.changesource.ChangeChapterSourceViewModel
|
||||||
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
import io.legado.app.ui.book.explore.ExploreShowViewModel
|
||||||
import io.legado.app.ui.book.group.GroupViewModel
|
import io.legado.app.ui.book.group.GroupViewModel
|
||||||
import io.legado.app.ui.book.import.local.ImportBookViewModel
|
import io.legado.app.ui.book.import.local.ImportBookViewModel
|
||||||
@@ -147,6 +154,10 @@ val appModule = module {
|
|||||||
singleOf(::SearchContentRepository)
|
singleOf(::SearchContentRepository)
|
||||||
singleOf(::RemoteBookRepository)
|
singleOf(::RemoteBookRepository)
|
||||||
singleOf(::SettingsRepository)
|
singleOf(::SettingsRepository)
|
||||||
|
singleOf(::ReadSettingsRepository)
|
||||||
|
singleOf(::ReadAloudSettingsRepository)
|
||||||
|
singleOf(::ReadBookStyleConfigRepository)
|
||||||
|
singleOf(::MangaSettingsRepository)
|
||||||
singleOf(::LocalPreferencesRepository)
|
singleOf(::LocalPreferencesRepository)
|
||||||
singleOf(::ExploreBooksUseCase)
|
singleOf(::ExploreBooksUseCase)
|
||||||
singleOf(::ExploreKindUiUseCase)
|
singleOf(::ExploreKindUiUseCase)
|
||||||
@@ -194,6 +205,8 @@ val appModule = module {
|
|||||||
single<SearchRepository> { get<SearchRepositoryImpl>() }
|
single<SearchRepository> { get<SearchRepositoryImpl>() }
|
||||||
single<BookSearchGateway> { get<SearchRepositoryImpl>() }
|
single<BookSearchGateway> { get<SearchRepositoryImpl>() }
|
||||||
singleOf(::SearchBooksUseCase)
|
singleOf(::SearchBooksUseCase)
|
||||||
|
singleOf(::ChangeSourceSearchUseCase)
|
||||||
|
singleOf(::GetChapterContentUseCase)
|
||||||
single<LlmGateway> { LlmTranslateRepositoryImpl() }
|
single<LlmGateway> { LlmTranslateRepositoryImpl() }
|
||||||
single<DictionaryGateway> { DictionaryRepositoryImpl() }
|
single<DictionaryGateway> { DictionaryRepositoryImpl() }
|
||||||
singleOf(::TranslateChapterUseCase)
|
singleOf(::TranslateChapterUseCase)
|
||||||
@@ -252,12 +265,16 @@ val appModule = module {
|
|||||||
application = get(),
|
application = get(),
|
||||||
getReadingProgressUseCase = get(),
|
getReadingProgressUseCase = get(),
|
||||||
uploadReadingProgressUseCase = get(),
|
uploadReadingProgressUseCase = get(),
|
||||||
translateChapterUseCase = get()
|
translateChapterUseCase = get(),
|
||||||
|
readSettingsRepository = get(),
|
||||||
|
readBookStyleConfigRepository = get(),
|
||||||
|
readAloudSettingsRepository = get()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
viewModelOf(::ChangeCoverViewModel)
|
viewModelOf(::ChangeCoverViewModel)
|
||||||
viewModelOf(::ChangeBookSourceComposeViewModel)
|
viewModelOf(::ChangeBookSourceComposeViewModel)
|
||||||
viewModelOf(::ChangeBookSourceViewModel)
|
viewModelOf(::ChangeBookSourceViewModel)
|
||||||
|
viewModelOf(::ChangeChapterSourceViewModel)
|
||||||
viewModelOf(::ExploreViewModel)
|
viewModelOf(::ExploreViewModel)
|
||||||
viewModelOf(::RssViewModel)
|
viewModelOf(::RssViewModel)
|
||||||
viewModelOf(::SearchViewModel)
|
viewModelOf(::SearchViewModel)
|
||||||
|
|||||||
@@ -0,0 +1,276 @@
|
|||||||
|
package io.legado.app.domain.usecase
|
||||||
|
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import io.legado.app.domain.gateway.BookSearchGateway
|
||||||
|
import io.legado.app.help.book.BookHelp
|
||||||
|
import io.legado.app.help.book.ContentProcessor
|
||||||
|
import io.legado.app.help.book.primaryStr
|
||||||
|
import io.legado.app.help.book.releaseHtmlData
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.source.SourceHelp
|
||||||
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
import io.legado.app.ui.book.changesource.ObservableSourceConfig
|
||||||
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
|
import io.legado.app.utils.internString
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.currentCoroutineContext
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
|
import kotlinx.coroutines.withTimeout
|
||||||
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
|
sealed interface ChangeSourceSearchEvent {
|
||||||
|
data object Started : ChangeSourceSearchEvent
|
||||||
|
data class Progress(
|
||||||
|
val processedSources: Int,
|
||||||
|
val totalSources: Int,
|
||||||
|
val resultCount: Int,
|
||||||
|
val sourceName: String,
|
||||||
|
) : ChangeSourceSearchEvent
|
||||||
|
|
||||||
|
data class Result(val searchBook: SearchBook) : ChangeSourceSearchEvent
|
||||||
|
data class Finished(val isEmpty: Boolean) : ChangeSourceSearchEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
class ChangeSourceSearchUseCase(
|
||||||
|
private val gateway: BookSearchGateway,
|
||||||
|
) {
|
||||||
|
private val threadCount = OtherConfig.threadCount
|
||||||
|
private val contentProcessor by lazy {
|
||||||
|
// ContentProcessor needs the old book - will be set before search
|
||||||
|
null as ContentProcessor?
|
||||||
|
}
|
||||||
|
|
||||||
|
// Shared state for TOC cache
|
||||||
|
private val tocMap = ConcurrentHashMap<String, List<BookChapter>>()
|
||||||
|
private val bookMap = ConcurrentHashMap<String, Book>()
|
||||||
|
private var tocMapChapterCount = 0
|
||||||
|
|
||||||
|
fun search(
|
||||||
|
name: String,
|
||||||
|
author: String,
|
||||||
|
scope: io.legado.app.ui.book.search.SearchScope,
|
||||||
|
oldBook: Book,
|
||||||
|
fromReadBookActivity: Boolean,
|
||||||
|
): Flow<ChangeSourceSearchEvent> = flow {
|
||||||
|
val contentProcessor = ContentProcessor.get(oldBook)
|
||||||
|
val bookSourceParts = scope.getBookSourceParts()
|
||||||
|
if (bookSourceParts.isEmpty()) {
|
||||||
|
throw io.legado.app.exception.NoStackTraceException("启用书源为空")
|
||||||
|
}
|
||||||
|
|
||||||
|
tocMap.clear()
|
||||||
|
bookMap.clear()
|
||||||
|
tocMapChapterCount = 0
|
||||||
|
|
||||||
|
emit(ChangeSourceSearchEvent.Started)
|
||||||
|
|
||||||
|
var processedSources = 0
|
||||||
|
val totalSources = bookSourceParts.size
|
||||||
|
|
||||||
|
for (bs in bookSourceParts) {
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
val source = bs.getBookSource() ?: continue
|
||||||
|
try {
|
||||||
|
withTimeout(60000L) {
|
||||||
|
searchSource(
|
||||||
|
source, name, author, oldBook, fromReadBookActivity,
|
||||||
|
contentProcessor
|
||||||
|
)
|
||||||
|
}.forEach { searchBook ->
|
||||||
|
emit(ChangeSourceSearchEvent.Result(searchBook))
|
||||||
|
}
|
||||||
|
} catch (_: Throwable) {
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
}
|
||||||
|
processedSources++
|
||||||
|
emit(
|
||||||
|
ChangeSourceSearchEvent.Progress(
|
||||||
|
processedSources = processedSources,
|
||||||
|
totalSources = totalSources,
|
||||||
|
resultCount = 0,
|
||||||
|
sourceName = source.bookSourceName,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
emit(ChangeSourceSearchEvent.Finished(isEmpty = true))
|
||||||
|
}.flowOn(Dispatchers.IO)
|
||||||
|
|
||||||
|
private suspend fun searchSource(
|
||||||
|
source: BookSource,
|
||||||
|
name: String,
|
||||||
|
author: String,
|
||||||
|
oldBook: Book,
|
||||||
|
fromReadBookActivity: Boolean,
|
||||||
|
contentProcessor: ContentProcessor,
|
||||||
|
): List<SearchBook> {
|
||||||
|
val checkAuthor = AppConfig.changeSourceCheckAuthor
|
||||||
|
val loadInfo = AppConfig.changeSourceLoadInfo
|
||||||
|
val loadToc = AppConfig.changeSourceLoadToc
|
||||||
|
val loadWordCount = AppConfig.changeSourceLoadWordCount
|
||||||
|
|
||||||
|
val resultBooks = WebBook.searchBookAwait(
|
||||||
|
source, name,
|
||||||
|
filter = { fName, fAuthor, _ ->
|
||||||
|
fName == name && (!checkAuthor || fAuthor.contains(author))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val processedBooks = mutableListOf<SearchBook>()
|
||||||
|
for (searchBook in resultBooks) {
|
||||||
|
currentCoroutineContext().ensureActive()
|
||||||
|
when {
|
||||||
|
loadInfo || loadToc || loadWordCount -> {
|
||||||
|
val book = searchBook.toBook()
|
||||||
|
try {
|
||||||
|
loadBookInfo(
|
||||||
|
source,
|
||||||
|
book,
|
||||||
|
loadToc,
|
||||||
|
loadWordCount,
|
||||||
|
oldBook,
|
||||||
|
fromReadBookActivity,
|
||||||
|
contentProcessor
|
||||||
|
)
|
||||||
|
val processedSearchBook = book.toSearchBook()
|
||||||
|
processedBooks.add(processedSearchBook)
|
||||||
|
} catch (e: Throwable) {
|
||||||
|
if (e is CancellationException) throw e
|
||||||
|
processedBooks.add(searchBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
processedBooks.add(searchBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return processedBooks
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBookInfo(
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
loadToc: Boolean,
|
||||||
|
loadWordCount: Boolean,
|
||||||
|
oldBook: Book,
|
||||||
|
fromReadBookActivity: Boolean,
|
||||||
|
contentProcessor: ContentProcessor,
|
||||||
|
) {
|
||||||
|
if (book.tocUrl.isEmpty()) {
|
||||||
|
WebBook.getBookInfoAwait(source, book)
|
||||||
|
}
|
||||||
|
if (loadToc || loadWordCount) {
|
||||||
|
loadBookToc(
|
||||||
|
source,
|
||||||
|
book,
|
||||||
|
loadWordCount,
|
||||||
|
oldBook,
|
||||||
|
fromReadBookActivity,
|
||||||
|
contentProcessor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBookToc(
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
loadWordCount: Boolean,
|
||||||
|
oldBook: Book,
|
||||||
|
fromReadBookActivity: Boolean,
|
||||||
|
contentProcessor: ContentProcessor,
|
||||||
|
) {
|
||||||
|
val chapters = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||||
|
for (chapter in chapters) {
|
||||||
|
chapter.internString()
|
||||||
|
}
|
||||||
|
if (tocMapChapterCount < 30000) {
|
||||||
|
tocMapChapterCount += chapters.size
|
||||||
|
tocMap[book.primaryStr()] = chapters
|
||||||
|
}
|
||||||
|
bookMap[book.primaryStr()] = book
|
||||||
|
book.releaseHtmlData()
|
||||||
|
if (loadWordCount) {
|
||||||
|
loadBookWordCount(
|
||||||
|
source,
|
||||||
|
book,
|
||||||
|
chapters,
|
||||||
|
oldBook,
|
||||||
|
fromReadBookActivity,
|
||||||
|
contentProcessor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun loadBookWordCount(
|
||||||
|
source: BookSource,
|
||||||
|
book: Book,
|
||||||
|
chapters: List<BookChapter>,
|
||||||
|
oldBook: Book,
|
||||||
|
fromReadBookActivity: Boolean,
|
||||||
|
contentProcessor: ContentProcessor,
|
||||||
|
) {
|
||||||
|
if (chapters.isEmpty()) return
|
||||||
|
val chapterIndex = if (fromReadBookActivity) {
|
||||||
|
BookHelp.getDurChapter(oldBook, chapters)
|
||||||
|
} else {
|
||||||
|
chapters.lastIndex
|
||||||
|
}
|
||||||
|
if (chapterIndex !in chapters.indices) return
|
||||||
|
val bookChapter = chapters[chapterIndex]
|
||||||
|
var title = bookChapter.title.trim()
|
||||||
|
if (title.length > 20) {
|
||||||
|
title = title.substring(0, 20) + "…"
|
||||||
|
}
|
||||||
|
val startTime = System.currentTimeMillis()
|
||||||
|
try {
|
||||||
|
val nextChapterUrl = chapters.getOrNull(chapterIndex + 1)?.url
|
||||||
|
var content = WebBook.getContentAwait(source, book, bookChapter, nextChapterUrl, false)
|
||||||
|
content = contentProcessor.getContent(oldBook, bookChapter, content, false).toString()
|
||||||
|
val len = content.length
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
book.toSearchBook().apply {
|
||||||
|
chapterWordCountText = "[${chapterIndex + 1}] ${title}\n字数:${len}"
|
||||||
|
chapterWordCount = len
|
||||||
|
respondTime = (endTime - startTime).toInt()
|
||||||
|
}
|
||||||
|
} catch (t: Throwable) {
|
||||||
|
if (t is CancellationException) throw t
|
||||||
|
val endTime = System.currentTimeMillis()
|
||||||
|
book.toSearchBook().apply {
|
||||||
|
chapterWordCountText =
|
||||||
|
"[${chapterIndex + 1}] ${title}\n获取字数失败:${t.localizedMessage}"
|
||||||
|
chapterWordCount = -1
|
||||||
|
respondTime = (endTime - startTime).toInt()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Source management
|
||||||
|
fun topSource(searchBook: SearchBook) {
|
||||||
|
ObservableSourceConfig.setBookScore(searchBook, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bottomSource(searchBook: SearchBook) {
|
||||||
|
ObservableSourceConfig.setBookScore(searchBook, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableSource(searchBook: SearchBook) {
|
||||||
|
io.legado.app.data.appDb.bookSourceDao.getBookSource(searchBook.origin)?.let { source ->
|
||||||
|
source.enabled = false
|
||||||
|
io.legado.app.data.appDb.bookSourceDao.update(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deleteSource(searchBook: SearchBook) {
|
||||||
|
SourceHelp.deleteBookSource(searchBook.origin)
|
||||||
|
io.legado.app.data.appDb.searchBookDao.delete(searchBook)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package io.legado.app.domain.usecase
|
||||||
|
|
||||||
|
import io.legado.app.data.dao.BookChapterDao
|
||||||
|
import io.legado.app.data.dao.BookSourceDao
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.model.webBook.WebBook
|
||||||
|
|
||||||
|
class GetChapterContentUseCase(
|
||||||
|
private val bookSourceDao: BookSourceDao,
|
||||||
|
private val bookChapterDao: BookChapterDao,
|
||||||
|
) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get TOC for a book. If tocUrl is empty, fetches book info first.
|
||||||
|
*/
|
||||||
|
suspend fun getToc(book: Book): Pair<List<BookChapter>, BookSource> {
|
||||||
|
val source = bookSourceDao.getBookSource(book.origin)
|
||||||
|
?: throw NoStackTraceException("书源不存在")
|
||||||
|
if (book.tocUrl.isEmpty()) {
|
||||||
|
WebBook.getBookInfoAwait(source, book)
|
||||||
|
}
|
||||||
|
val toc = WebBook.getChapterListAwait(source, book).getOrThrow()
|
||||||
|
return Pair(toc, source)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get content for a specific chapter.
|
||||||
|
*/
|
||||||
|
suspend fun getContent(
|
||||||
|
book: Book,
|
||||||
|
chapter: BookChapter,
|
||||||
|
nextChapterUrl: String?,
|
||||||
|
): String {
|
||||||
|
val bookSource = bookSourceDao.getBookSource(book.origin)
|
||||||
|
?: throw NoStackTraceException("书源不存在")
|
||||||
|
return WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Find the chapter index in a new TOC matching the current chapter.
|
||||||
|
*/
|
||||||
|
fun getDurChapterIndex(chapterIndex: Int, chapterTitle: String, toc: List<BookChapter>): Int {
|
||||||
|
return io.legado.app.help.book.BookHelp.getDurChapter(chapterIndex, chapterTitle, toc)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,7 +8,7 @@ import io.legado.app.data.entities.KeyboardAssist
|
|||||||
import io.legado.app.data.entities.RssSource
|
import io.legado.app.data.entities.RssSource
|
||||||
import io.legado.app.data.entities.TxtTocRule
|
import io.legado.app.data.entities.TxtTocRule
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
@@ -70,12 +70,12 @@ object DefaultData {
|
|||||||
GSON.fromJsonArray<TxtTocRule>(json).getOrNull() ?: emptyList()
|
GSON.fromJsonArray<TxtTocRule>(json).getOrNull() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val themeConfigs: List<OldThemeConfig.Config> by lazy {
|
val themeConfigs: List<ThemeConfigStore.Config> by lazy {
|
||||||
val json = String(
|
val json = String(
|
||||||
appCtx.assets.open("defaultData${File.separator}${OldThemeConfig.configFileName}")
|
appCtx.assets.open("defaultData${File.separator}${ThemeConfigStore.configFileName}")
|
||||||
.readBytes()
|
.readBytes()
|
||||||
)
|
)
|
||||||
GSON.fromJsonArray<OldThemeConfig.Config>(json).getOrNull() ?: emptyList()
|
GSON.fromJsonArray<ThemeConfigStore.Config>(json).getOrNull() ?: emptyList()
|
||||||
}
|
}
|
||||||
|
|
||||||
val rssSources: List<RssSource> by lazy {
|
val rssSources: List<RssSource> by lazy {
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import io.legado.app.constant.AppPattern
|
|||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.http.BackstageWebView
|
import io.legado.app.help.http.BackstageWebView
|
||||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||||
@@ -1204,12 +1204,12 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
*/
|
*/
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun getThemeConfig(): String {
|
fun getThemeConfig(): String {
|
||||||
val themeConfig = OldThemeConfig.getDurConfig(appCtx)
|
val themeConfig = ThemeConfigStore.getDurConfig(appCtx)
|
||||||
return GSON.toJson(themeConfig)
|
return GSON.toJson(themeConfig)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getThemeConfigMap(): Map<String, Any?> {
|
fun getThemeConfigMap(): Map<String, Any?> {
|
||||||
return OldThemeConfig.getDurConfig(appCtx).toMap()
|
return ThemeConfigStore.getDurConfig(appCtx).toMap()
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -5,6 +5,7 @@ import android.os.Build
|
|||||||
import io.legado.app.BuildConfig
|
import io.legado.app.BuildConfig
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.repository.ReadPreferences
|
||||||
import io.legado.app.ui.book.manga.config.MangaScrollMode
|
import io.legado.app.ui.book.manga.config.MangaScrollMode
|
||||||
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
@@ -64,8 +65,134 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
var adaptSpecialStyle = appCtx.getPrefBoolean(PreferKey.adaptSpecialStyle, true)
|
var adaptSpecialStyle = appCtx.getPrefBoolean(PreferKey.adaptSpecialStyle, true)
|
||||||
var useUnderline = appCtx.getPrefBoolean(PreferKey.useUnderline, false)
|
var useUnderline = appCtx.getPrefBoolean(PreferKey.useUnderline, false)
|
||||||
|
|
||||||
|
private var screenOrientationValue = appCtx.getPrefString(PreferKey.screenOrientation) ?: "0"
|
||||||
|
private var noAnimScrollPageValue = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
|
||||||
|
private var tocUiUseReplaceValue = appCtx.getPrefBoolean(PreferKey.tocUiUseReplace)
|
||||||
|
private var tocCountWordsValue = appCtx.getPrefBoolean(PreferKey.tocCountWords, true)
|
||||||
|
private var autoChangeSourceValue = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
|
||||||
|
private var clickImgWayValue = appCtx.getPrefString(PreferKey.clickImgWay, "2") ?: "2"
|
||||||
|
private var doublePageHorizontalValue = appCtx.getPrefString(PreferKey.doublePageHorizontal, "0") ?: "0"
|
||||||
|
private var progressBarBehaviorValue = appCtx.getPrefString(PreferKey.progressBarBehavior, "page") ?: "page"
|
||||||
|
private var keyPageOnLongPressValue = appCtx.getPrefBoolean(PreferKey.keyPageOnLongPress, false)
|
||||||
|
private var volumeKeyPageValue = appCtx.getPrefBoolean(PreferKey.volumeKeyPage, true)
|
||||||
|
private var volumeKeyPageOnPlayValue = appCtx.getPrefBoolean(PreferKey.volumeKeyPageOnPlay, true)
|
||||||
|
private var mouseWheelPageValue = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
|
||||||
|
private var paddingDisplayCutoutsValue = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
|
||||||
|
private var pageTouchSlopValue = appCtx.getPrefInt(PreferKey.pageTouchSlop, 0)
|
||||||
|
private var showReadTitleBarAdditionValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.showReadTitleAddition, true)
|
||||||
|
private var titleBarModeValue = appCtx.getPrefString(PreferKey.titleBarMode, "1") ?: "1"
|
||||||
|
private var menuAlphaValue = appCtx.getPrefInt(PreferKey.menuAlpha, 100)
|
||||||
|
private var readSliderModeValue = appCtx.getPrefString(PreferKey.readSliderMode, "0") ?: "0"
|
||||||
|
private var readBarStyleFollowPageValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.readBarStyleFollowPage, false)
|
||||||
|
private var readBarStyleValue = appCtx.getPrefInt(PreferKey.readBarStyle, 0)
|
||||||
|
private var defaultSourceChangeAllValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.defaultSourceChangeAll, true)
|
||||||
|
private var sliderVibratorValue = appCtx.getPrefBoolean(PreferKey.sliderVibrator, false)
|
||||||
|
private var selectVibratorValue = appCtx.getPrefBoolean(PreferKey.selectVibrator, false)
|
||||||
|
|
||||||
|
fun syncReadPreferences(preferences: ReadPreferences) {
|
||||||
|
optimizeRender = CanvasRecorderFactory.isSupport && preferences.optimizeRender
|
||||||
|
adaptSpecialStyle = preferences.adaptSpecialStyle
|
||||||
|
useUnderline = preferences.useUnderline
|
||||||
|
clickActionTL = preferences.clickActionTL
|
||||||
|
clickActionTC = preferences.clickActionTC
|
||||||
|
clickActionTR = preferences.clickActionTR
|
||||||
|
clickActionML = preferences.clickActionML
|
||||||
|
clickActionMC = preferences.clickActionMC
|
||||||
|
clickActionMR = preferences.clickActionMR
|
||||||
|
clickActionBL = preferences.clickActionBL
|
||||||
|
clickActionBC = preferences.clickActionBC
|
||||||
|
clickActionBR = preferences.clickActionBR
|
||||||
|
screenOrientationValue = preferences.screenOrientation
|
||||||
|
noAnimScrollPageValue = preferences.noAnimScrollPage
|
||||||
|
tocUiUseReplaceValue = preferences.tocUiUseReplace
|
||||||
|
tocCountWordsValue = preferences.tocCountWords
|
||||||
|
autoChangeSourceValue = preferences.autoChangeSource
|
||||||
|
clickImgWayValue = preferences.clickImgWay
|
||||||
|
doublePageHorizontalValue = preferences.doubleHorizontalPage
|
||||||
|
progressBarBehaviorValue = preferences.progressBarBehavior
|
||||||
|
keyPageOnLongPressValue = preferences.keyPageOnLongPress
|
||||||
|
volumeKeyPageValue = preferences.volumeKeyPage
|
||||||
|
volumeKeyPageOnPlayValue = preferences.volumeKeyPageOnPlay
|
||||||
|
mouseWheelPageValue = preferences.mouseWheelPage
|
||||||
|
paddingDisplayCutoutsValue = preferences.paddingDisplayCutouts
|
||||||
|
pageTouchSlopValue = preferences.pageTouchSlop
|
||||||
|
showReadTitleBarAdditionValue = preferences.showReadTitleAddition
|
||||||
|
titleBarModeValue = preferences.titleBarMode
|
||||||
|
menuAlphaValue = preferences.menuAlpha
|
||||||
|
readSliderModeValue = preferences.readSliderMode
|
||||||
|
readBarStyleFollowPageValue = preferences.readBarStyleFollowPage
|
||||||
|
readBarStyleValue = preferences.readBarStyle
|
||||||
|
defaultSourceChangeAllValue = preferences.defaultSourceChangeAll
|
||||||
|
sliderVibratorValue = preferences.sliderVibrator
|
||||||
|
selectVibratorValue = preferences.selectVibrator
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateReadBarStyleCache(value: Int) {
|
||||||
|
readBarStyleValue = value.coerceIn(0, 2)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun syncReadPreferenceFromSharedPreferences(key: String?) {
|
||||||
|
when (key) {
|
||||||
|
PreferKey.optimizeRender -> optimizeRender = CanvasRecorderFactory.isSupport &&
|
||||||
|
appCtx.getPrefBoolean(PreferKey.optimizeRender, false)
|
||||||
|
PreferKey.adaptSpecialStyle -> adaptSpecialStyle =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.adaptSpecialStyle, true)
|
||||||
|
PreferKey.useUnderline -> useUnderline =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.useUnderline, false)
|
||||||
|
PreferKey.screenOrientation -> screenOrientationValue =
|
||||||
|
appCtx.getPrefString(PreferKey.screenOrientation) ?: "0"
|
||||||
|
PreferKey.noAnimScrollPage -> noAnimScrollPageValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
|
||||||
|
PreferKey.tocUiUseReplace -> tocUiUseReplaceValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.tocUiUseReplace)
|
||||||
|
PreferKey.tocCountWords -> tocCountWordsValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.tocCountWords, true)
|
||||||
|
PreferKey.autoChangeSource -> autoChangeSourceValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
|
||||||
|
PreferKey.clickImgWay -> clickImgWayValue =
|
||||||
|
appCtx.getPrefString(PreferKey.clickImgWay, "2") ?: "2"
|
||||||
|
PreferKey.doublePageHorizontal -> doublePageHorizontalValue =
|
||||||
|
appCtx.getPrefString(PreferKey.doublePageHorizontal, "0") ?: "0"
|
||||||
|
PreferKey.progressBarBehavior -> progressBarBehaviorValue =
|
||||||
|
appCtx.getPrefString(PreferKey.progressBarBehavior, "page") ?: "page"
|
||||||
|
PreferKey.keyPageOnLongPress -> keyPageOnLongPressValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.keyPageOnLongPress, false)
|
||||||
|
PreferKey.volumeKeyPage -> volumeKeyPageValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.volumeKeyPage, true)
|
||||||
|
PreferKey.volumeKeyPageOnPlay -> volumeKeyPageOnPlayValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.volumeKeyPageOnPlay, true)
|
||||||
|
PreferKey.mouseWheelPage -> mouseWheelPageValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
|
||||||
|
PreferKey.paddingDisplayCutouts -> paddingDisplayCutoutsValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
|
||||||
|
PreferKey.pageTouchSlop -> pageTouchSlopValue =
|
||||||
|
appCtx.getPrefInt(PreferKey.pageTouchSlop, 0)
|
||||||
|
PreferKey.showReadTitleAddition -> showReadTitleBarAdditionValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.showReadTitleAddition, true)
|
||||||
|
PreferKey.titleBarMode -> titleBarModeValue =
|
||||||
|
appCtx.getPrefString(PreferKey.titleBarMode, "1") ?: "1"
|
||||||
|
PreferKey.menuAlpha -> menuAlphaValue = appCtx.getPrefInt(PreferKey.menuAlpha, 100)
|
||||||
|
PreferKey.readSliderMode -> readSliderModeValue =
|
||||||
|
appCtx.getPrefString(PreferKey.readSliderMode, "0") ?: "0"
|
||||||
|
PreferKey.readBarStyleFollowPage -> readBarStyleFollowPageValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.readBarStyleFollowPage, false)
|
||||||
|
PreferKey.readBarStyle -> readBarStyleValue =
|
||||||
|
appCtx.getPrefInt(PreferKey.readBarStyle, 0)
|
||||||
|
PreferKey.defaultSourceChangeAll -> defaultSourceChangeAllValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.defaultSourceChangeAll, true)
|
||||||
|
PreferKey.sliderVibrator -> sliderVibratorValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.sliderVibrator, false)
|
||||||
|
PreferKey.selectVibrator -> selectVibratorValue =
|
||||||
|
appCtx.getPrefBoolean(PreferKey.selectVibrator, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||||
|
syncReadPreferenceFromSharedPreferences(key)
|
||||||
when (key) {
|
when (key) {
|
||||||
|
|
||||||
PreferKey.adaptSpecialStyle -> adaptSpecialStyle =
|
PreferKey.adaptSpecialStyle -> adaptSpecialStyle =
|
||||||
@@ -230,7 +357,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
// get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
|
// get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
|
||||||
|
|
||||||
val screenOrientation: String?
|
val screenOrientation: String?
|
||||||
get() = appCtx.getPrefString(PreferKey.screenOrientation)
|
get() = screenOrientationValue
|
||||||
|
|
||||||
var bookGroupStyle: Int
|
var bookGroupStyle: Int
|
||||||
get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0)
|
get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0)
|
||||||
@@ -351,7 +478,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val noAnimScrollPage: Boolean
|
val noAnimScrollPage: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
|
get() = noAnimScrollPageValue
|
||||||
|
|
||||||
const val defaultSpeechRate = 5
|
const val defaultSpeechRate = 5
|
||||||
|
|
||||||
@@ -468,14 +595,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tocUiUseReplace: Boolean
|
var tocUiUseReplace: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.tocUiUseReplace)
|
get() = tocUiUseReplaceValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
tocUiUseReplaceValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.tocUiUseReplace, value)
|
appCtx.putPrefBoolean(PreferKey.tocUiUseReplace, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var tocCountWords: Boolean
|
var tocCountWords: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.tocCountWords, true)
|
get() = tocCountWordsValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
tocCountWordsValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.tocCountWords, value)
|
appCtx.putPrefBoolean(PreferKey.tocCountWords, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -486,7 +615,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val autoChangeSource: Boolean
|
val autoChangeSource: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
|
get() = autoChangeSourceValue
|
||||||
|
|
||||||
var changeSourceLoadInfo: Boolean
|
var changeSourceLoadInfo: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo)
|
get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo)
|
||||||
@@ -545,7 +674,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
val clickImgWay: String?
|
val clickImgWay: String?
|
||||||
get() = appCtx.getPrefString(PreferKey.clickImgWay)
|
get() = clickImgWayValue
|
||||||
var preDownloadNum
|
var preDownloadNum
|
||||||
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -590,25 +719,25 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false)
|
val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false)
|
||||||
|
|
||||||
val doublePageHorizontal: String?
|
val doublePageHorizontal: String?
|
||||||
get() = appCtx.getPrefString(PreferKey.doublePageHorizontal)
|
get() = doublePageHorizontalValue
|
||||||
|
|
||||||
val progressBarBehavior: String?
|
val progressBarBehavior: String?
|
||||||
get() = appCtx.getPrefString(PreferKey.progressBarBehavior, "page")
|
get() = progressBarBehaviorValue
|
||||||
|
|
||||||
val keyPageOnLongPress
|
val keyPageOnLongPress
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.keyPageOnLongPress, false)
|
get() = keyPageOnLongPressValue
|
||||||
|
|
||||||
val volumeKeyPage
|
val volumeKeyPage
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPage, true)
|
get() = volumeKeyPageValue
|
||||||
|
|
||||||
val volumeKeyPageOnPlay
|
val volumeKeyPageOnPlay
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPageOnPlay, true)
|
get() = volumeKeyPageOnPlayValue
|
||||||
|
|
||||||
val mouseWheelPage
|
val mouseWheelPage
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
|
get() = mouseWheelPageValue
|
||||||
|
|
||||||
val paddingDisplayCutouts
|
val paddingDisplayCutouts
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
|
get() = paddingDisplayCutoutsValue
|
||||||
|
|
||||||
var searchScope: String
|
var searchScope: String
|
||||||
get() = appCtx.getPrefString("searchScope") ?: ""
|
get() = appCtx.getPrefString("searchScope") ?: ""
|
||||||
@@ -623,8 +752,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var pageTouchSlop: Int
|
var pageTouchSlop: Int
|
||||||
get() = appCtx.getPrefInt(PreferKey.pageTouchSlop, 0)
|
get() = pageTouchSlopValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
pageTouchSlopValue = value
|
||||||
appCtx.putPrefInt(PreferKey.pageTouchSlop, value)
|
appCtx.putPrefInt(PreferKey.pageTouchSlop, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,21 +796,24 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showReadTitleBarAddition: Boolean
|
var showReadTitleBarAddition: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.showReadTitleAddition, true)
|
get() = showReadTitleBarAdditionValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
showReadTitleBarAdditionValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.showReadTitleAddition, value)
|
appCtx.putPrefBoolean(PreferKey.showReadTitleAddition, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var readBarStyleFollowPage: Boolean
|
var readBarStyleFollowPage: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.readBarStyleFollowPage, false)
|
get() = readBarStyleFollowPageValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
readBarStyleFollowPageValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.readBarStyleFollowPage, value)
|
appCtx.putPrefBoolean(PreferKey.readBarStyleFollowPage, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var readBarStyle: Int
|
var readBarStyle: Int
|
||||||
get() = appCtx.getPrefInt(PreferKey.readBarStyle, 0)
|
get() = readBarStyleValue
|
||||||
set(value) {
|
set(value) {
|
||||||
appCtx.putPrefInt(PreferKey.readBarStyle, value)
|
readBarStyleValue = value.coerceIn(0, 2)
|
||||||
|
appCtx.putPrefInt(PreferKey.readBarStyle, readBarStyleValue)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sourceEditMaxLine: Int
|
var sourceEditMaxLine: Int
|
||||||
@@ -757,8 +890,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var titleBarMode
|
var titleBarMode
|
||||||
get() = appCtx.getPrefString(PreferKey.titleBarMode, "1")
|
get() = titleBarModeValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
titleBarModeValue = value
|
||||||
appCtx.putPrefString(PreferKey.titleBarMode, value)
|
appCtx.putPrefString(PreferKey.titleBarMode, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -903,14 +1037,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
appCtx.putPrefBoolean(PreferKey.enableBlur, value)
|
appCtx.putPrefBoolean(PreferKey.enableBlur, value)
|
||||||
}
|
}
|
||||||
var menuAlpha: Int
|
var menuAlpha: Int
|
||||||
get() = appCtx.getPrefInt(PreferKey.menuAlpha, 100)
|
get() = menuAlphaValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
menuAlphaValue = value
|
||||||
appCtx.putPrefInt(PreferKey.menuAlpha, value)
|
appCtx.putPrefInt(PreferKey.menuAlpha, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var readSliderMode
|
var readSliderMode
|
||||||
get() = appCtx.getPrefString(PreferKey.readSliderMode, "0")
|
get() = readSliderModeValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
readSliderModeValue = value
|
||||||
appCtx.putPrefString(PreferKey.readSliderMode, value)
|
appCtx.putPrefString(PreferKey.readSliderMode, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -945,20 +1081,23 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var defaultSourceChangeAll: Boolean
|
var defaultSourceChangeAll: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.defaultSourceChangeAll, true)
|
get() = defaultSourceChangeAllValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
defaultSourceChangeAllValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.defaultSourceChangeAll, value)
|
appCtx.putPrefBoolean(PreferKey.defaultSourceChangeAll, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sliderVibrator: Boolean
|
var sliderVibrator: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.sliderVibrator, false)
|
get() = sliderVibratorValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
sliderVibratorValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.sliderVibrator, value)
|
appCtx.putPrefBoolean(PreferKey.sliderVibrator, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
var selectVibrator: Boolean
|
var selectVibrator: Boolean
|
||||||
get() = appCtx.getPrefBoolean(PreferKey.selectVibrator, false)
|
get() = selectVibratorValue
|
||||||
set(value) {
|
set(value) {
|
||||||
|
selectVibratorValue = value
|
||||||
appCtx.putPrefBoolean(PreferKey.selectVibrator, value)
|
appCtx.putPrefBoolean(PreferKey.selectVibrator, value)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,141 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import android.graphics.drawable.Drawable
|
||||||
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import com.google.android.material.color.MaterialColors
|
||||||
|
import io.legado.app.utils.BitmapUtils
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.externalFiles
|
||||||
|
import io.legado.app.utils.printOnDebug
|
||||||
|
import io.legado.app.utils.resizeAndRecycle
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object ReadStyleResolver {
|
||||||
|
|
||||||
|
enum class ReadStyleMode {
|
||||||
|
Day,
|
||||||
|
Night,
|
||||||
|
EInk
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ReadBackground(
|
||||||
|
val type: Int,
|
||||||
|
val value: String
|
||||||
|
)
|
||||||
|
|
||||||
|
fun currentMode(): ReadStyleMode {
|
||||||
|
return when {
|
||||||
|
AppConfig.isEInkMode -> ReadStyleMode.EInk
|
||||||
|
AppConfig.isNightTheme -> ReadStyleMode.Night
|
||||||
|
else -> ReadStyleMode.Day
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun isNightTheme(): Boolean {
|
||||||
|
return AppConfig.isNightTheme
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setCurrentBackground(
|
||||||
|
config: ReadBookConfig.Config,
|
||||||
|
bgType: Int,
|
||||||
|
bg: String
|
||||||
|
) {
|
||||||
|
when (currentMode()) {
|
||||||
|
ReadStyleMode.EInk -> {
|
||||||
|
config.bgTypeEInk = bgType
|
||||||
|
config.bgStrEInk = bg
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadStyleMode.Night -> {
|
||||||
|
config.bgTypeNight = bgType
|
||||||
|
config.bgStrNight = bg
|
||||||
|
}
|
||||||
|
|
||||||
|
ReadStyleMode.Day -> {
|
||||||
|
config.bgType = bgType
|
||||||
|
config.bgStr = bg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentBackground(config: ReadBookConfig.Config): ReadBackground {
|
||||||
|
return when (currentMode()) {
|
||||||
|
ReadStyleMode.EInk -> ReadBackground(config.bgTypeEInk, config.bgStrEInk)
|
||||||
|
ReadStyleMode.Night -> ReadBackground(config.bgTypeNight, config.bgStrNight)
|
||||||
|
ReadStyleMode.Day -> ReadBackground(config.bgType, config.bgStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun backgroundPath(config: ReadBookConfig.Config, bgIndex: Int): String? {
|
||||||
|
val bgType = when (bgIndex) {
|
||||||
|
0 -> config.bgType
|
||||||
|
1 -> config.bgTypeNight
|
||||||
|
2 -> config.bgTypeEInk
|
||||||
|
else -> error("unknown bgIndex: $bgIndex")
|
||||||
|
}
|
||||||
|
if (bgType != 2) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
val bgStr = when (bgIndex) {
|
||||||
|
0 -> config.bgStr
|
||||||
|
1 -> config.bgStrNight
|
||||||
|
2 -> config.bgStrEInk
|
||||||
|
else -> error("unknown bgIndex: $bgIndex")
|
||||||
|
}
|
||||||
|
return if (bgStr.contains(File.separator)) {
|
||||||
|
bgStr
|
||||||
|
} else {
|
||||||
|
FileUtils.getPath(appCtx.externalFiles, "bg", bgStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun currentBackgroundDrawable(
|
||||||
|
config: ReadBookConfig.Config,
|
||||||
|
width: Int,
|
||||||
|
height: Int
|
||||||
|
): Drawable {
|
||||||
|
if (width == 0 || height == 0) {
|
||||||
|
return fallbackBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
var bgDrawable: Drawable? = null
|
||||||
|
val resources = appCtx.resources
|
||||||
|
val background = currentBackground(config)
|
||||||
|
try {
|
||||||
|
bgDrawable = when (background.type) {
|
||||||
|
0 -> background.value.toColorInt().toDrawable()
|
||||||
|
1 -> {
|
||||||
|
val path = "bg" + File.separator + background.value
|
||||||
|
val bitmap = BitmapUtils.decodeAssetsBitmap(appCtx, path, width, height)
|
||||||
|
bitmap?.resizeAndRecycle(width, height)?.toDrawable(resources)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val path = background.value.let {
|
||||||
|
if (it.contains(File.separator)) it
|
||||||
|
else FileUtils.getPath(appCtx.externalFiles, "bg", background.value)
|
||||||
|
}
|
||||||
|
val bitmap = BitmapUtils.decodeBitmap(path, width, height)
|
||||||
|
bitmap?.resizeAndRecycle(width, height)?.toDrawable(resources)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: OutOfMemoryError) {
|
||||||
|
e.printOnDebug()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printOnDebug()
|
||||||
|
}
|
||||||
|
|
||||||
|
return bgDrawable ?: fallbackBackground()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun fallbackBackground(): Drawable {
|
||||||
|
val fallbackColor = MaterialColors.getColor(
|
||||||
|
appCtx,
|
||||||
|
com.google.android.material.R.attr.colorSurface,
|
||||||
|
Color.WHITE
|
||||||
|
)
|
||||||
|
return fallbackColor.toDrawable()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,120 +0,0 @@
|
|||||||
package io.legado.app.help.config
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import io.legado.app.R
|
|
||||||
import splitties.init.appCtx
|
|
||||||
|
|
||||||
@Suppress("ConstPropertyName")
|
|
||||||
object ReadTipConfig {
|
|
||||||
|
|
||||||
const val none = 0
|
|
||||||
const val chapterTitle = 1
|
|
||||||
const val time = 2
|
|
||||||
const val battery = 3
|
|
||||||
const val batteryPercentage = 10
|
|
||||||
const val page = 4
|
|
||||||
const val totalProgress = 5
|
|
||||||
const val pageAndTotal = 6
|
|
||||||
const val bookName = 7
|
|
||||||
const val timeBattery = 8
|
|
||||||
const val timeBatteryPercentage = 9
|
|
||||||
const val totalProgress1 = 11
|
|
||||||
const val chapterTitleArrow = 12
|
|
||||||
const val batteryInside = 13
|
|
||||||
const val batteryIcon = 14
|
|
||||||
const val batteryClassic = 15
|
|
||||||
const val timeBatteryClassic = 16
|
|
||||||
const val chapterTitleArrowClassic = 17
|
|
||||||
|
|
||||||
|
|
||||||
val tipValues = arrayOf(
|
|
||||||
none, bookName, chapterTitle, chapterTitleArrow, chapterTitleArrowClassic, time, battery, batteryClassic, batteryInside, batteryIcon, batteryPercentage, page,
|
|
||||||
totalProgress, totalProgress1, pageAndTotal, timeBattery, timeBatteryClassic, timeBatteryPercentage
|
|
||||||
)
|
|
||||||
val tipNames get() = appCtx.resources.getStringArray(R.array.read_tip).toList()
|
|
||||||
|
|
||||||
val tipColorNames get() = appCtx.resources.getStringArray(R.array.tip_color).toList()
|
|
||||||
val tipDividerColorNames
|
|
||||||
get() = appCtx.resources.getStringArray(R.array.tip_divider_color).toList()
|
|
||||||
|
|
||||||
var tipHeaderLeft: Int
|
|
||||||
get() = ReadBookConfig.config.tipHeaderLeft
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipHeaderLeft = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipHeaderMiddle: Int
|
|
||||||
get() = ReadBookConfig.config.tipHeaderMiddle
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipHeaderMiddle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipHeaderRight: Int
|
|
||||||
get() = ReadBookConfig.config.tipHeaderRight
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipHeaderRight = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipFooterLeft: Int
|
|
||||||
get() = ReadBookConfig.config.tipFooterLeft
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipFooterLeft = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipFooterMiddle: Int
|
|
||||||
get() = ReadBookConfig.config.tipFooterMiddle
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipFooterMiddle = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipFooterRight: Int
|
|
||||||
get() = ReadBookConfig.config.tipFooterRight
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipFooterRight = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var headerMode: Int
|
|
||||||
get() = ReadBookConfig.config.headerMode
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.headerMode = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var footerMode: Int
|
|
||||||
get() = ReadBookConfig.config.footerMode
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.footerMode = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipHeaderColor: Int
|
|
||||||
get() = ReadBookConfig.config.tipHeaderColor
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipHeaderColor = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipFooterColor: Int
|
|
||||||
get() = ReadBookConfig.config.tipFooterColor
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipFooterColor = value
|
|
||||||
}
|
|
||||||
|
|
||||||
var tipDividerColor: Int
|
|
||||||
get() = ReadBookConfig.config.tipDividerColor
|
|
||||||
set(value) {
|
|
||||||
ReadBookConfig.config.tipDividerColor = value
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getHeaderModes(context: Context): LinkedHashMap<Int, String> {
|
|
||||||
return linkedMapOf(
|
|
||||||
Pair(0, context.getString(R.string.hide_when_status_bar_show)),
|
|
||||||
Pair(1, context.getString(R.string.show)),
|
|
||||||
Pair(2, context.getString(R.string.hide))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun getFooterModes(context: Context): LinkedHashMap<Int, String> {
|
|
||||||
return linkedMapOf(
|
|
||||||
Pair(0, context.getString(R.string.show)),
|
|
||||||
Pair(1, context.getString(R.string.hide))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+4
-52
@@ -4,15 +4,13 @@ import android.content.Context
|
|||||||
import android.graphics.Bitmap
|
import android.graphics.Bitmap
|
||||||
import android.util.DisplayMetrics
|
import android.util.DisplayMetrics
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
import androidx.appcompat.app.AppCompatDelegate
|
|
||||||
import androidx.core.graphics.toColorInt
|
import androidx.core.graphics.toColorInt
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.constant.Theme
|
import io.legado.app.constant.Theme
|
||||||
import io.legado.app.help.DefaultData
|
import io.legado.app.help.DefaultData
|
||||||
import io.legado.app.model.BookCover
|
|
||||||
import io.legado.app.utils.BitmapUtils
|
import io.legado.app.utils.BitmapUtils
|
||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
@@ -26,13 +24,12 @@ import io.legado.app.utils.getPrefString
|
|||||||
import io.legado.app.utils.hexString
|
import io.legado.app.utils.hexString
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
import io.legado.app.utils.putPrefInt
|
|
||||||
import io.legado.app.utils.stackBlur
|
import io.legado.app.utils.stackBlur
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
|
|
||||||
@Keep
|
@Keep
|
||||||
object OldThemeConfig {
|
object ThemeConfigStore {
|
||||||
const val configFileName = "themeConfig.json"
|
const val configFileName = "themeConfig.json"
|
||||||
val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
|
val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
|
||||||
|
|
||||||
@@ -46,13 +43,8 @@ object OldThemeConfig {
|
|||||||
else -> Theme.Light
|
else -> Theme.Light
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isDarkTheme(): Boolean {
|
|
||||||
return getTheme() == Theme.Dark
|
|
||||||
}
|
|
||||||
|
|
||||||
fun applyDayNight(context: Context) {
|
fun applyDayNight(context: Context) {
|
||||||
initNightMode()
|
initNightMode()
|
||||||
BookCover.upDefaultCover()
|
|
||||||
postEvent(EventBus.RECREATE, "")
|
postEvent(EventBus.RECREATE, "")
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
}
|
}
|
||||||
@@ -62,17 +54,7 @@ object OldThemeConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initNightMode() {
|
private fun initNightMode() {
|
||||||
when (appCtx.getPrefString(PreferKey.themeMode, "0")) {
|
ThemeConfig.initNightMode()
|
||||||
"1" -> {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
|
||||||
}
|
|
||||||
"2" -> {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? {
|
fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? {
|
||||||
@@ -110,11 +92,6 @@ object OldThemeConfig {
|
|||||||
FileUtils.createFileIfNotExist(configFilePath).writeText(json)
|
FileUtils.createFileIfNotExist(configFilePath).writeText(json)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun delConfig(index: Int) {
|
|
||||||
configList.removeAt(index)
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun addConfig(json: String): Boolean {
|
fun addConfig(json: String): Boolean {
|
||||||
GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull()
|
GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull()
|
||||||
?.let {
|
?.let {
|
||||||
@@ -165,33 +142,8 @@ object OldThemeConfig {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
fun applyConfig(context: Context, config: Config) {
|
|
||||||
try {
|
|
||||||
val primary = config.primaryColor.toColorInt()
|
|
||||||
if (config.isNightTheme) {
|
|
||||||
context.putPrefInt(PreferKey.cNPrimary, primary)
|
|
||||||
} else {
|
|
||||||
context.putPrefInt(PreferKey.cPrimary, primary)
|
|
||||||
}
|
|
||||||
AppConfig.isNightTheme = config.isNightTheme
|
|
||||||
applyDayNight(context)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
AppLog.put("设置主题出错\n$e", e, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveDayTheme(context: Context, name: String) {
|
|
||||||
val config = getDayTheme(context, name)
|
|
||||||
addConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun saveNightTheme(context: Context, name: String) {
|
|
||||||
val config = getNightTheme(context, name)
|
|
||||||
addConfig(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 更新主题
|
* 清理无用背景图片
|
||||||
*/
|
*/
|
||||||
fun clearBg() {
|
fun clearBg() {
|
||||||
val bgImagePath = appCtx.getPrefString(PreferKey.bgImage)
|
val bgImagePath = appCtx.getPrefString(PreferKey.bgImage)
|
||||||
@@ -13,7 +13,7 @@ import io.legado.app.help.AppWebDav
|
|||||||
import io.legado.app.help.DirectLinkUpload
|
import io.legado.app.help.DirectLinkUpload
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
@@ -77,7 +77,7 @@ object Backup {
|
|||||||
DirectLinkUpload.ruleFileName,
|
DirectLinkUpload.ruleFileName,
|
||||||
ReadBookConfig.configFileName,
|
ReadBookConfig.configFileName,
|
||||||
ReadBookConfig.shareConfigFileName,
|
ReadBookConfig.shareConfigFileName,
|
||||||
OldThemeConfig.configFileName,
|
ThemeConfigStore.configFileName,
|
||||||
BookCover.configFileName,
|
BookCover.configFileName,
|
||||||
"config.xml"
|
"config.xml"
|
||||||
)
|
)
|
||||||
@@ -164,8 +164,8 @@ object Backup {
|
|||||||
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
|
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
|
||||||
.writeText(it)
|
.writeText(it)
|
||||||
}
|
}
|
||||||
GSON.toJson(OldThemeConfig.configList).let {
|
GSON.toJson(ThemeConfigStore.configList).let {
|
||||||
FileUtils.createFileIfNotExist(backupPath + File.separator + OldThemeConfig.configFileName)
|
FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfigStore.configFileName)
|
||||||
.writeText(it)
|
.writeText(it)
|
||||||
}
|
}
|
||||||
DirectLinkUpload.getConfig()?.let {
|
DirectLinkUpload.getConfig()?.let {
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ import io.legado.app.help.LauncherIconHelp
|
|||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
import io.legado.app.help.book.upType
|
import io.legado.app.help.book.upType
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
@@ -275,12 +275,12 @@ object Restore : KoinComponent {
|
|||||||
}
|
}
|
||||||
//恢复主题配置
|
//恢复主题配置
|
||||||
if (!BackupConfig.ignoreThemeConfig) {
|
if (!BackupConfig.ignoreThemeConfig) {
|
||||||
File(path, OldThemeConfig.configFileName).takeIf {
|
File(path, ThemeConfigStore.configFileName).takeIf {
|
||||||
it.exists()
|
it.exists()
|
||||||
}?.runCatching {
|
}?.runCatching {
|
||||||
FileUtils.delete(OldThemeConfig.configFilePath)
|
FileUtils.delete(ThemeConfigStore.configFilePath)
|
||||||
copyTo(File(OldThemeConfig.configFilePath))
|
copyTo(File(ThemeConfigStore.configFilePath))
|
||||||
OldThemeConfig.upConfig()
|
ThemeConfigStore.upConfig()
|
||||||
}?.onFailure {
|
}?.onFailure {
|
||||||
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
|
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
@@ -341,7 +341,7 @@ object Restore : KoinComponent {
|
|||||||
if (!BuildConfig.DEBUG) {
|
if (!BuildConfig.DEBUG) {
|
||||||
LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon))
|
LauncherIconHelp.changeIcon(appCtx.getPrefString(PreferKey.launcherIcon))
|
||||||
}
|
}
|
||||||
OldThemeConfig.applyDayNight(appCtx)
|
ThemeConfigStore.applyDayNight(appCtx)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import android.util.AttributeSet
|
|||||||
import androidx.preference.PreferenceViewHolder
|
import androidx.preference.PreferenceViewHolder
|
||||||
import com.google.android.material.button.MaterialButtonToggleGroup
|
import com.google.android.material.button.MaterialButtonToggleGroup
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
|
|
||||||
|
|
||||||
class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(context, attrs) {
|
class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(context, attrs) {
|
||||||
@@ -59,7 +59,7 @@ class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(co
|
|||||||
persistString(newValue)
|
persistString(newValue)
|
||||||
callChangeListener(newValue)
|
callChangeListener(newValue)
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
Handler(Looper.getMainLooper()).postDelayed({
|
||||||
OldThemeConfig.applyDayNight(context)
|
ThemeConfigStore.applyDayNight(context)
|
||||||
}, 300)
|
}, 300)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,9 +63,6 @@ object BookCover {
|
|||||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 兼容旧代码,空实现
|
|
||||||
fun upDefaultCover() {}
|
|
||||||
|
|
||||||
fun getRandomDefaultPath(
|
fun getRandomDefaultPath(
|
||||||
seed: Any? = null,
|
seed: Any? = null,
|
||||||
isNight: Boolean = AppConfig.isNightTheme
|
isNight: Boolean = AppConfig.isNightTheme
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.service.AudioPlayService
|
import io.legado.app.service.AudioPlayService
|
||||||
import io.legado.app.service.BaseReadAloudService
|
import io.legado.app.service.BaseReadAloudService
|
||||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
import io.legado.app.ui.main.MainActivity
|
||||||
import io.legado.app.utils.LogUtils
|
import io.legado.app.utils.LogUtils
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
@@ -94,7 +94,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
|||||||
// break
|
// break
|
||||||
}
|
}
|
||||||
|
|
||||||
LifecycleHelp.isExistActivity(ReadBookActivity::class.java) ->
|
MainActivity.hasActiveReadBookRoute ->
|
||||||
postEvent(EventBus.MEDIA_BUTTON, true)
|
postEvent(EventBus.MEDIA_BUTTON, true)
|
||||||
|
|
||||||
LifecycleHelp.isExistActivity(AudioPlayActivity::class.java) ->
|
LifecycleHelp.isExistActivity(AudioPlayActivity::class.java) ->
|
||||||
|
|||||||
@@ -17,12 +17,10 @@ import splitties.systemservices.connectivityManager
|
|||||||
class NetworkChangedListener(private val context: Context) {
|
class NetworkChangedListener(private val context: Context) {
|
||||||
|
|
||||||
var onNetworkChanged: (() -> Unit)? = null
|
var onNetworkChanged: (() -> Unit)? = null
|
||||||
|
private var registered = false
|
||||||
|
|
||||||
private val receiver: NetworkChangedReceiver? by lazy {
|
private val receiver: NetworkChangedReceiver? by lazy {
|
||||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) NetworkChangedReceiver() else null
|
||||||
NetworkChangedReceiver()
|
|
||||||
}
|
|
||||||
return@lazy null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val networkCallback: ConnectivityManager.NetworkCallback? by lazy {
|
private val networkCallback: ConnectivityManager.NetworkCallback? by lazy {
|
||||||
@@ -38,25 +36,31 @@ class NetworkChangedListener(private val context: Context) {
|
|||||||
|
|
||||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||||
fun register() {
|
fun register() {
|
||||||
|
if (registered) return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
networkCallback?.let {
|
networkCallback?.let {
|
||||||
connectivityManager.registerDefaultNetworkCallback(it)
|
connectivityManager.registerDefaultNetworkCallback(it)
|
||||||
|
registered = true
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
receiver?.let {
|
receiver?.let {
|
||||||
context.registerReceiver(it, it.filter)
|
context.registerReceiver(it, it.filter)
|
||||||
|
registered = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unRegister() {
|
fun unRegister() {
|
||||||
|
if (!registered) return
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
networkCallback?.let {
|
networkCallback?.let {
|
||||||
connectivityManager.unregisterNetworkCallback(it)
|
connectivityManager.unregisterNetworkCallback(it)
|
||||||
|
registered = false
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
receiver?.let {
|
receiver?.let {
|
||||||
context.unregisterReceiver(it)
|
context.unregisterReceiver(it)
|
||||||
|
registered = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -74,4 +78,4 @@ class NetworkChangedListener(private val context: Context) {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -43,8 +43,8 @@ import io.legado.app.lib.permission.PermissionsCompat
|
|||||||
import io.legado.app.model.ReadAloud
|
import io.legado.app.model.ReadAloud
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.receiver.MediaButtonReceiver
|
import io.legado.app.receiver.MediaButtonReceiver
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.read.page.entities.TextChapter
|
import io.legado.app.ui.book.read.page.entities.TextChapter
|
||||||
|
import io.legado.app.ui.main.MainActivity
|
||||||
import io.legado.app.utils.LogUtils
|
import io.legado.app.utils.LogUtils
|
||||||
import io.legado.app.utils.activityPendingIntent
|
import io.legado.app.utils.activityPendingIntent
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
@@ -639,7 +639,10 @@ abstract class BaseReadAloudService : BaseService(),
|
|||||||
.setContentTitle(nTitle)
|
.setContentTitle(nTitle)
|
||||||
.setContentText(nSubtitle)
|
.setContentText(nSubtitle)
|
||||||
.setContentIntent(
|
.setContentIntent(
|
||||||
activityPendingIntent<ReadBookActivity>("activity")
|
activityPendingIntent(
|
||||||
|
MainActivity.createReadBookIntent(this, readAloud = true),
|
||||||
|
"activity"
|
||||||
|
)
|
||||||
)
|
)
|
||||||
.setVibrate(null)
|
.setVibrate(null)
|
||||||
.setSound(null)
|
.setSound(null)
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ class InteractiveHighlight(
|
|||||||
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
|
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
|
||||||
|
|
||||||
private var startPosition = Offset.Zero
|
private var startPosition = Offset.Zero
|
||||||
|
val pressProgress: Float get() = pressProgressAnimation.value
|
||||||
|
val offset: Offset get() = positionAnimation.value
|
||||||
|
|
||||||
@Language("AGSL")
|
@Language("AGSL")
|
||||||
private val shader = RuntimeShader(
|
private val shader = RuntimeShader(
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ import io.legado.app.base.adapter.ItemViewHolder
|
|||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
import io.legado.app.base.adapter.RecyclerAdapter
|
||||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
import io.legado.app.databinding.DialogRecyclerViewBinding
|
||||||
import io.legado.app.databinding.ItemSourceImportBinding
|
import io.legado.app.databinding.ItemSourceImportBinding
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
//import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.ui.widget.dialog.CodeDialog
|
import io.legado.app.ui.widget.dialog.CodeDialog
|
||||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||||
@@ -126,7 +126,7 @@ class ImportThemeDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
|
|||||||
}
|
}
|
||||||
|
|
||||||
inner class SourcesAdapter(context: Context) :
|
inner class SourcesAdapter(context: Context) :
|
||||||
RecyclerAdapter<OldThemeConfig.Config, ItemSourceImportBinding>(context) {
|
RecyclerAdapter<ThemeConfigStore.Config, ItemSourceImportBinding>(context) {
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
|
override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
|
||||||
return ItemSourceImportBinding.inflate(inflater, parent, false)
|
return ItemSourceImportBinding.inflate(inflater, parent, false)
|
||||||
@@ -135,7 +135,7 @@ class ImportThemeDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
|
|||||||
override fun convert(
|
override fun convert(
|
||||||
holder: ItemViewHolder,
|
holder: ItemViewHolder,
|
||||||
binding: ItemSourceImportBinding,
|
binding: ItemSourceImportBinding,
|
||||||
item: OldThemeConfig.Config,
|
item: ThemeConfigStore.Config,
|
||||||
payloads: MutableList<Any>
|
payloads: MutableList<Any>
|
||||||
) {
|
) {
|
||||||
binding.apply {
|
binding.apply {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel
|
|||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.ThemeConfigStore
|
||||||
import io.legado.app.help.http.decompressed
|
import io.legado.app.help.http.decompressed
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
@@ -28,8 +28,8 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
val errorLiveData = MutableLiveData<String>()
|
val errorLiveData = MutableLiveData<String>()
|
||||||
val successLiveData = MutableLiveData<Int>()
|
val successLiveData = MutableLiveData<Int>()
|
||||||
|
|
||||||
val allSources = arrayListOf<OldThemeConfig.Config>()
|
val allSources = arrayListOf<ThemeConfigStore.Config>()
|
||||||
val checkSources = arrayListOf<OldThemeConfig.Config?>()
|
val checkSources = arrayListOf<ThemeConfigStore.Config?>()
|
||||||
val selectStatus = arrayListOf<Boolean>()
|
val selectStatus = arrayListOf<Boolean>()
|
||||||
|
|
||||||
val isSelectAll: Boolean
|
val isSelectAll: Boolean
|
||||||
@@ -57,7 +57,7 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
execute {
|
execute {
|
||||||
selectStatus.forEachIndexed { index, b ->
|
selectStatus.forEachIndexed { index, b ->
|
||||||
if (b) {
|
if (b) {
|
||||||
OldThemeConfig.addConfig(allSources[index])
|
ThemeConfigStore.addConfig(allSources[index])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
@@ -79,12 +79,12 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
private suspend fun importSourceAwait(text: String) {
|
private suspend fun importSourceAwait(text: String) {
|
||||||
when {
|
when {
|
||||||
text.isJsonObject() -> {
|
text.isJsonObject() -> {
|
||||||
GSON.fromJsonObject<OldThemeConfig.Config>(text).getOrThrow().let {
|
GSON.fromJsonObject<ThemeConfigStore.Config>(text).getOrThrow().let {
|
||||||
allSources.add(it)
|
allSources.add(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
text.isJsonArray() -> GSON.fromJsonArray<OldThemeConfig.Config>(text).getOrThrow()
|
text.isJsonArray() -> GSON.fromJsonArray<ThemeConfigStore.Config>(text).getOrThrow()
|
||||||
.let { items ->
|
.let { items ->
|
||||||
allSources.addAll(items)
|
allSources.addAll(items)
|
||||||
}
|
}
|
||||||
@@ -114,7 +114,7 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
private fun comparisonSource() {
|
private fun comparisonSource() {
|
||||||
execute {
|
execute {
|
||||||
allSources.forEach { config ->
|
allSources.forEach { config ->
|
||||||
val source = OldThemeConfig.configList.find {
|
val source = ThemeConfigStore.configList.find {
|
||||||
it.themeName == config.themeName
|
it.themeName == config.themeName
|
||||||
}
|
}
|
||||||
checkSources.add(source)
|
checkSources.add(source)
|
||||||
|
|||||||
+227
-29
@@ -1,25 +1,35 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import io.legado.app.data.appDb
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.data.entities.BookSourcePart
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
import io.legado.app.data.entities.SearchBook
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.data.repository.SearchRepository
|
import io.legado.app.data.repository.SearchRepository
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceSearchEvent
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceSearchUseCase
|
||||||
|
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
||||||
|
import io.legado.app.help.book.primaryStr
|
||||||
import io.legado.app.ui.book.search.SearchScope
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.launch
|
||||||
|
|
||||||
class ChangeBookSourceComposeViewModel(
|
class ChangeBookSourceComposeViewModel(
|
||||||
application: Application,
|
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
||||||
|
private val getChapterContentUseCase: GetChapterContentUseCase,
|
||||||
private val searchRepository: SearchRepository,
|
private val searchRepository: SearchRepository,
|
||||||
) : ChangeBookSourceViewModel(application) {
|
) : ViewModel() {
|
||||||
|
|
||||||
|
// Public state for the sheet
|
||||||
val enabledGroups = searchRepository.enabledGroups
|
val enabledGroups = searchRepository.enabledGroups
|
||||||
val enabledSources = searchRepository.enabledSources
|
val enabledSources = searchRepository.enabledSources
|
||||||
|
|
||||||
val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
private val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
||||||
|
|
||||||
data class ScopeUiState(
|
data class ScopeUiState(
|
||||||
val isAll: Boolean,
|
val isAll: Boolean,
|
||||||
@@ -38,18 +48,164 @@ class ChangeBookSourceComposeViewModel(
|
|||||||
)
|
)
|
||||||
val scopeUiState = _scopeUiState.asStateFlow()
|
val scopeUiState = _scopeUiState.asStateFlow()
|
||||||
|
|
||||||
val checkAuthor: Boolean
|
private val _isSearching = MutableStateFlow(false)
|
||||||
get() = ChangeSourceConfig.checkAuthor
|
val isSearching = _isSearching.asStateFlow()
|
||||||
|
|
||||||
val loadInfo: Boolean
|
private val _changeSourceProgress = MutableStateFlow(0 to "")
|
||||||
get() = ChangeSourceConfig.loadInfo
|
val changeSourceProgress = _changeSourceProgress.asStateFlow()
|
||||||
|
|
||||||
val loadToc: Boolean
|
private val _searchDataFlow = MutableStateFlow<List<SearchBook>>(emptyList())
|
||||||
get() = ChangeSourceConfig.loadToc
|
val searchDataFlow: StateFlow<List<SearchBook>> = _searchDataFlow.asStateFlow()
|
||||||
|
|
||||||
val loadWordCount: Boolean
|
val totalSourceCount: Int
|
||||||
get() = ChangeSourceConfig.loadWordCount
|
get() = searchResults.size
|
||||||
|
|
||||||
|
fun getBookFromMap(key: String): Book? = bookMap[key]?.toBook()
|
||||||
|
|
||||||
|
// Options
|
||||||
|
val checkAuthor: Boolean get() = ChangeSourceConfig.checkAuthor
|
||||||
|
val loadInfo: Boolean get() = ChangeSourceConfig.loadInfo
|
||||||
|
val loadToc: Boolean get() = ChangeSourceConfig.loadToc
|
||||||
|
val loadWordCount: Boolean get() = ChangeSourceConfig.loadWordCount
|
||||||
|
|
||||||
|
// Internal state
|
||||||
|
private var searchJob: Job? = null
|
||||||
|
private var oldBook: Book? = null
|
||||||
|
private var screenKey: String = ""
|
||||||
|
private val searchResults = mutableListOf<SearchBook>()
|
||||||
|
private val bookMap = mutableMapOf<String, SearchBook>()
|
||||||
|
private val tocMap = mutableMapOf<String, List<BookChapter>>()
|
||||||
|
|
||||||
|
init {
|
||||||
|
viewModelScope.launch {
|
||||||
|
searchRepository.enabledGroups.collect { /* handled by sheet */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun initData(name: String, author: String, book: Book, fromReadBookActivity: Boolean) {
|
||||||
|
this.oldBook = book
|
||||||
|
if (searchJob?.isActive != true) {
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startSearch() {
|
||||||
|
val book = oldBook ?: return
|
||||||
|
stopSearch()
|
||||||
|
searchResults.clear()
|
||||||
|
bookMap.clear()
|
||||||
|
tocMap.clear()
|
||||||
|
_searchDataFlow.value = emptyList()
|
||||||
|
|
||||||
|
searchJob = viewModelScope.launch {
|
||||||
|
changeSourceSearchUseCase.search(
|
||||||
|
name = book.name,
|
||||||
|
author = book.author,
|
||||||
|
scope = SearchScope(ChangeSourceConfig.searchScope),
|
||||||
|
oldBook = book,
|
||||||
|
fromReadBookActivity = false,
|
||||||
|
).collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is ChangeSourceSearchEvent.Started -> {
|
||||||
|
_isSearching.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Progress -> {
|
||||||
|
_changeSourceProgress.value = event.processedSources to event.sourceName
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Result -> {
|
||||||
|
searchResults.add(event.searchBook)
|
||||||
|
bookMap[event.searchBook.primaryStr()] = event.searchBook
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Finished -> {
|
||||||
|
_isSearching.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startSearch(origin: String) {
|
||||||
|
// Reload a single source
|
||||||
|
viewModelScope.launch {
|
||||||
|
changeSourceSearchUseCase.topSource(
|
||||||
|
searchResults.find { it.origin == origin } ?: return@launch
|
||||||
|
)
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopSearch() {
|
||||||
|
searchJob?.cancel()
|
||||||
|
searchJob = null
|
||||||
|
_isSearching.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun screen(key: String?) {
|
||||||
|
screenKey = key?.trim() ?: ""
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun startOrStopSearch() {
|
||||||
|
if (searchJob?.isActive == true) {
|
||||||
|
stopSearch()
|
||||||
|
} else {
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun pause() {
|
||||||
|
// No-op for now
|
||||||
|
}
|
||||||
|
|
||||||
|
fun resume() {
|
||||||
|
// No-op for now
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterResults() {
|
||||||
|
val filtered = if (screenKey.isEmpty()) {
|
||||||
|
searchResults.toList()
|
||||||
|
} else {
|
||||||
|
searchResults.filter {
|
||||||
|
it.name.contains(screenKey) || it.originName.contains(screenKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val sorted = filtered.sortedWith(
|
||||||
|
compareByDescending<SearchBook> { ObservableSourceConfig.getBookScore(it) }
|
||||||
|
.thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) }
|
||||||
|
.thenBy { it.originOrder }
|
||||||
|
)
|
||||||
|
_searchDataFlow.value = sorted
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getToc(
|
||||||
|
book: Book,
|
||||||
|
onSuccess: (toc: List<BookChapter>, source: BookSource) -> Unit,
|
||||||
|
onError: (e: Throwable) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val cachedToc = tocMap[book.primaryStr()]
|
||||||
|
if (cachedToc != null) {
|
||||||
|
val source = io.legado.app.data.appDb.bookSourceDao.getBookSource(book.origin)
|
||||||
|
if (source != null) {
|
||||||
|
onSuccess(cachedToc, source)
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val (toc, source) = getChapterContentUseCase.getToc(book)
|
||||||
|
tocMap[book.primaryStr()] = toc
|
||||||
|
onSuccess(toc, source)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
onError(e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options
|
||||||
fun onCheckAuthorChange(enabled: Boolean) {
|
fun onCheckAuthorChange(enabled: Boolean) {
|
||||||
if (ChangeSourceConfig.checkAuthor == enabled) return
|
if (ChangeSourceConfig.checkAuthor == enabled) return
|
||||||
ChangeSourceConfig.checkAuthor = enabled
|
ChangeSourceConfig.checkAuthor = enabled
|
||||||
@@ -70,21 +226,67 @@ class ChangeBookSourceComposeViewModel(
|
|||||||
if (ChangeSourceConfig.loadWordCount == enabled) return
|
if (ChangeSourceConfig.loadWordCount == enabled) return
|
||||||
ChangeSourceConfig.loadWordCount = enabled
|
ChangeSourceConfig.loadWordCount = enabled
|
||||||
if (enabled) {
|
if (enabled) {
|
||||||
onLoadWordCountChecked(true)
|
startSearch()
|
||||||
} else {
|
} else {
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun bookScoreFlow(searchBook: SearchBook): StateFlow<Int> {
|
fun refresh() {
|
||||||
return ObservableSourceConfig.bookScoreFlow(searchBook)
|
searchResults.clear()
|
||||||
|
bookMap.clear()
|
||||||
|
startSearch()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Source actions
|
||||||
|
fun topSource(searchBook: SearchBook) {
|
||||||
|
changeSourceSearchUseCase.topSource(searchBook)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bottomSource(searchBook: SearchBook) {
|
||||||
|
changeSourceSearchUseCase.bottomSource(searchBook)
|
||||||
|
refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun disableSource(searchBook: SearchBook) {
|
||||||
|
changeSourceSearchUseCase.disableSource(searchBook)
|
||||||
|
searchResults.remove(searchBook)
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun del(searchBook: SearchBook) {
|
||||||
|
changeSourceSearchUseCase.deleteSource(searchBook)
|
||||||
|
searchResults.remove(searchBook)
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun autoChangeSource(
|
||||||
|
bookType: Int?,
|
||||||
|
onSuccess: (book: Book, toc: List<BookChapter>, source: BookSource) -> Unit,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
val found = searchResults.firstOrNull { it.type == bookType }
|
||||||
|
if (found != null) {
|
||||||
|
try {
|
||||||
|
val (toc, source) = getChapterContentUseCase.getToc(found.toBook())
|
||||||
|
onSuccess(found.toBook(), toc, source)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Score
|
||||||
|
fun bookScoreFlow(searchBook: SearchBook) = ObservableSourceConfig.bookScoreFlow(searchBook)
|
||||||
|
|
||||||
fun onBookScoreClick(searchBook: SearchBook) {
|
fun onBookScoreClick(searchBook: SearchBook) {
|
||||||
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
|
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
|
||||||
setBookScore(searchBook, if (currentScore > 0) 0 else 1)
|
changeSourceSearchUseCase.topSource(searchBook)
|
||||||
|
ObservableSourceConfig.setBookScore(searchBook, if (currentScore > 0) 0 else 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scope
|
||||||
fun selectAllScope() {
|
fun selectAllScope() {
|
||||||
searchScope.update("")
|
searchScope.update("")
|
||||||
saveScope()
|
saveScope()
|
||||||
@@ -110,17 +312,15 @@ class ChangeBookSourceComposeViewModel(
|
|||||||
} else {
|
} else {
|
||||||
mutableSetOf()
|
mutableSetOf()
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedUrls.contains(source.bookSourceUrl)) {
|
if (selectedUrls.contains(source.bookSourceUrl)) {
|
||||||
selectedUrls.remove(source.bookSourceUrl)
|
selectedUrls.remove(source.bookSourceUrl)
|
||||||
} else {
|
} else {
|
||||||
selectedUrls.add(source.bookSourceUrl)
|
selectedUrls.add(source.bookSourceUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedUrls.isEmpty()) {
|
if (selectedUrls.isEmpty()) {
|
||||||
searchScope.update("")
|
searchScope.update("")
|
||||||
} else {
|
} else {
|
||||||
val selectedSources = appDb.bookSourceDao.allEnabledPart.filter {
|
val selectedSources = io.legado.app.data.appDb.bookSourceDao.allEnabledPart.filter {
|
||||||
selectedUrls.contains(it.bookSourceUrl)
|
selectedUrls.contains(it.bookSourceUrl)
|
||||||
}
|
}
|
||||||
searchScope.updateSources(selectedSources)
|
searchScope.updateSources(selectedSources)
|
||||||
@@ -130,14 +330,12 @@ class ChangeBookSourceComposeViewModel(
|
|||||||
|
|
||||||
private fun saveScope() {
|
private fun saveScope() {
|
||||||
ChangeSourceConfig.searchScope = searchScope.toString()
|
ChangeSourceConfig.searchScope = searchScope.toString()
|
||||||
_scopeUiState.update {
|
_scopeUiState.value = ScopeUiState(
|
||||||
ScopeUiState(
|
isAll = searchScope.isAll(),
|
||||||
isAll = searchScope.isAll(),
|
isSource = searchScope.isSource(),
|
||||||
isSource = searchScope.isSource(),
|
displayNames = searchScope.displayNames,
|
||||||
displayNames = searchScope.displayNames,
|
sourceUrls = searchScope.sourceUrls
|
||||||
sourceUrls = searchScope.sourceUrls
|
)
|
||||||
)
|
|
||||||
}
|
|
||||||
refresh()
|
refresh()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -32,10 +32,10 @@ import io.legado.app.databinding.DialogBookChangeSourceBinding
|
|||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.search.SearchScope
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
||||||
|
import io.legado.app.ui.main.MainActivity
|
||||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
import io.legado.app.ui.widget.recycler.VerticalDivider
|
||||||
import io.legado.app.utils.StartActivityContract
|
import io.legado.app.utils.StartActivityContract
|
||||||
@@ -103,7 +103,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
|
|||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
//binding.toolBar.setBackgroundColor(primaryColor)
|
||||||
viewModel.initData(arguments, callBack?.oldBook, activity is ReadBookActivity)
|
viewModel.initData(arguments, callBack?.oldBook, MainActivity.hasActiveReadBookRoute)
|
||||||
showTitle()
|
showTitle()
|
||||||
initMenu()
|
initMenu()
|
||||||
initRecyclerView()
|
initRecyclerView()
|
||||||
@@ -547,4 +547,4 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
|
|||||||
fun addToBookshelf(book: Book, toc: List<BookChapter>)
|
fun addToBookshelf(book: Book, toc: List<BookChapter>)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.recyclerview.widget.DiffUtil
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.DiffRecyclerAdapter
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
|
||||||
import io.legado.app.databinding.ItemChangeSourceBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.invisible
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onLongClick
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeChapterSourceAdapter(
|
|
||||||
context: Context,
|
|
||||||
val viewModel: ChangeChapterSourceViewModel,
|
|
||||||
val callBack: CallBack
|
|
||||||
) : DiffRecyclerAdapter<SearchBook, ItemChangeSourceBinding>(context) {
|
|
||||||
|
|
||||||
override val diffItemCallback = object : DiffUtil.ItemCallback<SearchBook>() {
|
|
||||||
override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean {
|
|
||||||
return oldItem.bookUrl == newItem.bookUrl
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean {
|
|
||||||
return oldItem.originName == newItem.originName
|
|
||||||
&& oldItem.getDisplayLastChapterTitle() == newItem.getDisplayLastChapterTitle()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemChangeSourceBinding {
|
|
||||||
return ItemChangeSourceBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemChangeSourceBinding,
|
|
||||||
item: SearchBook,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.apply {
|
|
||||||
if (payloads.isEmpty()) {
|
|
||||||
tvOrigin.text = item.originName
|
|
||||||
tvAuthor.text = item.author
|
|
||||||
tvLast.text = item.getDisplayLastChapterTitle()
|
|
||||||
tvCurrentChapterWordCount.text = item.chapterWordCountText
|
|
||||||
tvRespondTime.text = context.getString(R.string.respondTime, item.respondTime)
|
|
||||||
if (callBack.oldBookUrl == item.bookUrl) {
|
|
||||||
ivChecked.visible()
|
|
||||||
} else {
|
|
||||||
ivChecked.invisible()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
for (i in payloads.indices) {
|
|
||||||
val bundle = payloads[i] as Bundle
|
|
||||||
bundle.keySet().forEach {
|
|
||||||
when (it) {
|
|
||||||
"name" -> tvOrigin.text = item.originName
|
|
||||||
"latest" -> tvLast.text = item.getDisplayLastChapterTitle()
|
|
||||||
"upCurSource" -> if (callBack.oldBookUrl == item.bookUrl) {
|
|
||||||
ivChecked.visible()
|
|
||||||
} else {
|
|
||||||
ivChecked.invisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val score = callBack.getBookScore(item)
|
|
||||||
if (score > 0) {
|
|
||||||
// 已置顶
|
|
||||||
binding.ivGood.setImageResource(R.drawable.ic_praise_filled)
|
|
||||||
} else {
|
|
||||||
// 未置顶
|
|
||||||
binding.ivGood.setImageResource(R.drawable.ic_praise)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
if (AppConfig.changeSourceLoadWordCount && !item.chapterWordCountText.isNullOrBlank()) {
|
|
||||||
tvCurrentChapterWordCount.visible()
|
|
||||||
} else {
|
|
||||||
tvCurrentChapterWordCount.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AppConfig.changeSourceLoadWordCount && item.respondTime >= 0) {
|
|
||||||
tvRespondTime.visible()
|
|
||||||
} else {
|
|
||||||
tvRespondTime.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemChangeSourceBinding) {
|
|
||||||
binding.ivGood.setOnClickListener {
|
|
||||||
val item = getItem(holder.layoutPosition) ?: return@setOnClickListener
|
|
||||||
val score = callBack.getBookScore(item)
|
|
||||||
if (score > 0) {
|
|
||||||
// 已置顶 -> 取消置顶
|
|
||||||
binding.ivGood.setImageResource(R.drawable.ic_praise)
|
|
||||||
callBack.setBookScore(item, 0)
|
|
||||||
} else {
|
|
||||||
// 未置顶 -> 设置置顶
|
|
||||||
binding.ivGood.setImageResource(R.drawable.ic_praise_filled)
|
|
||||||
callBack.setBookScore(item, 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callBack.openToc(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
holder.itemView.onLongClick {
|
|
||||||
showMenu(holder.itemView, getItem(holder.layoutPosition))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showMenu(view: View, searchBook: SearchBook?) {
|
|
||||||
searchBook ?: return
|
|
||||||
val popupMenu = PopupMenu(context, view)
|
|
||||||
popupMenu.inflate(R.menu.change_source_item)
|
|
||||||
popupMenu.setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.menu_top_source -> {
|
|
||||||
callBack.topSource(searchBook)
|
|
||||||
}
|
|
||||||
R.id.menu_bottom_source -> {
|
|
||||||
callBack.bottomSource(searchBook)
|
|
||||||
}
|
|
||||||
R.id.menu_edit_source -> {
|
|
||||||
callBack.editSource(searchBook)
|
|
||||||
}
|
|
||||||
R.id.menu_disable_source -> {
|
|
||||||
callBack.disableSource(searchBook)
|
|
||||||
}
|
|
||||||
R.id.menu_delete_source -> {
|
|
||||||
callBack.deleteSource(searchBook)
|
|
||||||
updateItems(0, itemCount, listOf<Int>())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
popupMenu.show()
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
val oldBookUrl: String?
|
|
||||||
fun openToc(searchBook: SearchBook)
|
|
||||||
fun topSource(searchBook: SearchBook)
|
|
||||||
fun bottomSource(searchBook: SearchBook)
|
|
||||||
fun editSource(searchBook: SearchBook)
|
|
||||||
fun disableSource(searchBook: SearchBook)
|
|
||||||
fun deleteSource(searchBook: SearchBook)
|
|
||||||
fun setBookScore(searchBook: SearchBook, score: Int)
|
|
||||||
fun getBookScore(searchBook: SearchBook): Int
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.data.entities.BookSourcePart
|
||||||
|
import io.legado.app.data.entities.SearchBook
|
||||||
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ChangeChapterSourceUiState(
|
||||||
|
val searchQuery: String = "",
|
||||||
|
val isSearching: Boolean = false,
|
||||||
|
val searchProgress: Pair<Int, String> = 0 to "",
|
||||||
|
val totalSourceCount: Int = 0,
|
||||||
|
val searchResults: ImmutableList<SearchBook> = persistentListOf(),
|
||||||
|
// Options
|
||||||
|
val checkAuthor: Boolean = ChangeSourceConfig.checkAuthor,
|
||||||
|
val loadInfo: Boolean = ChangeSourceConfig.loadInfo,
|
||||||
|
val loadToc: Boolean = ChangeSourceConfig.loadToc,
|
||||||
|
val loadWordCount: Boolean = ChangeSourceConfig.loadWordCount,
|
||||||
|
// TOC view
|
||||||
|
val showToc: Boolean = false,
|
||||||
|
val selectedSourceName: String = "",
|
||||||
|
val tocItems: ImmutableList<BookChapter> = persistentListOf(),
|
||||||
|
val isLoadingToc: Boolean = false,
|
||||||
|
// Scope filter
|
||||||
|
val scopeState: ScopeUiState = ScopeUiState(
|
||||||
|
isAll = true,
|
||||||
|
isSource = false,
|
||||||
|
displayNames = emptyList(),
|
||||||
|
sourceUrls = emptyList()
|
||||||
|
),
|
||||||
|
val enabledGroups: ImmutableList<String> = persistentListOf(),
|
||||||
|
val enabledSources: ImmutableList<BookSourcePart> = persistentListOf(),
|
||||||
|
// Source book map (for score flow)
|
||||||
|
val bookMap: Map<String, SearchBook> = emptyMap(),
|
||||||
|
)
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class ScopeUiState(
|
||||||
|
val isAll: Boolean,
|
||||||
|
val isSource: Boolean,
|
||||||
|
val displayNames: List<String>,
|
||||||
|
val sourceUrls: List<String>
|
||||||
|
)
|
||||||
|
|
||||||
|
sealed interface ChangeChapterSourceIntent {
|
||||||
|
// Search
|
||||||
|
data class UpdateQuery(val query: String) : ChangeChapterSourceIntent
|
||||||
|
data object StartStopSearch : ChangeChapterSourceIntent
|
||||||
|
data object Refresh : ChangeChapterSourceIntent
|
||||||
|
|
||||||
|
// Source selection
|
||||||
|
data class SelectSource(val searchBook: SearchBook) : ChangeChapterSourceIntent
|
||||||
|
data object BackFromToc : ChangeChapterSourceIntent
|
||||||
|
|
||||||
|
// Chapter selection
|
||||||
|
data class SelectChapter(val chapter: BookChapter) : ChangeChapterSourceIntent
|
||||||
|
|
||||||
|
// Options menu
|
||||||
|
data class SetCheckAuthor(val enabled: Boolean) : ChangeChapterSourceIntent
|
||||||
|
data class SetLoadInfo(val enabled: Boolean) : ChangeChapterSourceIntent
|
||||||
|
data class SetLoadToc(val enabled: Boolean) : ChangeChapterSourceIntent
|
||||||
|
data class SetLoadWordCount(val enabled: Boolean) : ChangeChapterSourceIntent
|
||||||
|
|
||||||
|
// Source actions
|
||||||
|
data class TopSource(val searchBook: SearchBook) : ChangeChapterSourceIntent
|
||||||
|
data class BottomSource(val searchBook: SearchBook) : ChangeChapterSourceIntent
|
||||||
|
data class DisableSource(val searchBook: SearchBook) : ChangeChapterSourceIntent
|
||||||
|
data class DeleteSource(val searchBook: SearchBook) : ChangeChapterSourceIntent
|
||||||
|
|
||||||
|
// Scope filter
|
||||||
|
data object ShowFilterSheet : ChangeChapterSourceIntent
|
||||||
|
data object DismissFilterSheet : ChangeChapterSourceIntent
|
||||||
|
data object SelectAllScope : ChangeChapterSourceIntent
|
||||||
|
data class ToggleScopeGroup(val groupName: String) : ChangeChapterSourceIntent
|
||||||
|
data class ToggleScopeSource(val source: BookSourcePart) : ChangeChapterSourceIntent
|
||||||
|
data object ApplyScope : ChangeChapterSourceIntent
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed interface ChangeChapterSourceEffect {
|
||||||
|
data class ReplaceContent(val content: String) : ChangeChapterSourceEffect
|
||||||
|
data class ShowToast(val message: String) : ChangeChapterSourceEffect
|
||||||
|
data object Dismiss : ChangeChapterSourceEffect
|
||||||
|
}
|
||||||
@@ -1,420 +0,0 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Menu
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.addCallback
|
|
||||||
import androidx.appcompat.widget.SearchView
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.Lifecycle.State.STARTED
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseDialogFragment
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
|
||||||
import io.legado.app.data.entities.BookChapter
|
|
||||||
import io.legado.app.data.entities.BookSource
|
|
||||||
import io.legado.app.data.entities.SearchBook
|
|
||||||
import io.legado.app.databinding.DialogChapterChangeSourceBinding
|
|
||||||
import io.legado.app.help.book.BookHelp
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.search.SearchScope
|
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
|
||||||
import io.legado.app.ui.book.source.manage.BookSourceActivity
|
|
||||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
|
||||||
import io.legado.app.utils.StartActivityContract
|
|
||||||
import io.legado.app.utils.applyTint
|
|
||||||
import io.legado.app.utils.dpToPx
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.setLayout
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.transaction
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.delay
|
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.flow.first
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
|
|
||||||
class ChangeChapterSourceDialog() : BaseDialogFragment(R.layout.dialog_chapter_change_source),
|
|
||||||
Toolbar.OnMenuItemClickListener,
|
|
||||||
ChangeChapterSourceAdapter.CallBack,
|
|
||||||
ChangeChapterTocAdapter.Callback {
|
|
||||||
|
|
||||||
constructor(name: String, author: String, chapterIndex: Int, chapterTitle: String) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putString("name", name)
|
|
||||||
putString("author", author)
|
|
||||||
putInt("chapterIndex", chapterIndex)
|
|
||||||
putString("chapterTitle", chapterTitle)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogChapterChangeSourceBinding::bind)
|
|
||||||
private val groups = linkedSetOf<String>()
|
|
||||||
private val callBack: CallBack? get() = activity as? CallBack
|
|
||||||
private val viewModel: ChangeChapterSourceViewModel by viewModels()
|
|
||||||
private val editSourceResult =
|
|
||||||
registerForActivityResult(StartActivityContract(BookSourceEditActivity::class.java)) {
|
|
||||||
viewModel.startSearch()
|
|
||||||
}
|
|
||||||
private val searchBookAdapter by lazy {
|
|
||||||
ChangeChapterSourceAdapter(requireContext(), viewModel, this)
|
|
||||||
}
|
|
||||||
private val tocAdapter by lazy {
|
|
||||||
ChangeChapterTocAdapter(requireContext(), this)
|
|
||||||
}
|
|
||||||
private val contentSuccess: (content: String) -> Unit = {
|
|
||||||
binding.loadingToc.gone()
|
|
||||||
callBack?.replaceContent(it)
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
private var searchBook: SearchBook? = null
|
|
||||||
private val searchFinishCallback: (isEmpty: Boolean) -> Unit = {
|
|
||||||
if (it) {
|
|
||||||
val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
|
||||||
val group = searchScope.display
|
|
||||||
if (!searchScope.isAll()) {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
context?.alert("搜索结果为空") {
|
|
||||||
setMessage("${group}分组搜索结果为空,是否切换到全部分组")
|
|
||||||
noButton()
|
|
||||||
yesButton {
|
|
||||||
ChangeSourceConfig.searchScope = ""
|
|
||||||
upGroupMenu()
|
|
||||||
viewModel.startSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
setLayout(1f, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
|
||||||
viewModel.initData(arguments, callBack?.oldBook, activity is ReadBookActivity)
|
|
||||||
showTitle()
|
|
||||||
initMenu()
|
|
||||||
initView()
|
|
||||||
initRecyclerView()
|
|
||||||
initSearchView()
|
|
||||||
initBottomBar()
|
|
||||||
initLiveData()
|
|
||||||
viewModel.searchFinishCallback = searchFinishCallback
|
|
||||||
activity?.onBackPressedDispatcher?.addCallback(this) {
|
|
||||||
if (binding.clToc.isVisible) {
|
|
||||||
binding.clToc.gone()
|
|
||||||
binding.recyclerView.visible()
|
|
||||||
return@addCallback
|
|
||||||
}
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
viewModel.searchFinishCallback = null
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showTitle() {
|
|
||||||
binding.toolBar.title = viewModel.chapterTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initMenu() {
|
|
||||||
binding.toolBar.inflateMenu(R.menu.change_source)
|
|
||||||
//binding.toolBar.menu.applyTint(requireContext())
|
|
||||||
binding.toolBar.setOnMenuItemClickListener(this)
|
|
||||||
binding.toolBar.menu.findItem(R.id.menu_check_author)
|
|
||||||
?.isChecked = AppConfig.changeSourceCheckAuthor
|
|
||||||
binding.toolBar.menu.findItem(R.id.menu_load_info)
|
|
||||||
?.isChecked = AppConfig.changeSourceLoadInfo
|
|
||||||
binding.toolBar.menu.findItem(R.id.menu_load_toc)
|
|
||||||
?.isChecked = AppConfig.changeSourceLoadToc
|
|
||||||
binding.toolBar.menu.findItem(R.id.menu_load_word_count)
|
|
||||||
?.isChecked = AppConfig.changeSourceLoadWordCount
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
binding.ivHideToc.setOnClickListener {
|
|
||||||
binding.clToc.gone()
|
|
||||||
binding.recyclerView.visible()
|
|
||||||
}
|
|
||||||
//binding.flHideToc.elevation = requireContext().elevation
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
|
||||||
binding.recyclerView.addItemDecoration(VerticalDivider(requireContext()))
|
|
||||||
binding.recyclerView.adapter = searchBookAdapter
|
|
||||||
searchBookAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() {
|
|
||||||
override fun onItemRangeInserted(positionStart: Int, itemCount: Int) {
|
|
||||||
if (positionStart == 0) {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) {
|
|
||||||
if (toPosition == 0) {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
binding.recyclerViewToc.adapter = tocAdapter
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSearchView() {
|
|
||||||
val searchView = binding.toolBar.menu.findItem(R.id.menu_screen).actionView as SearchView
|
|
||||||
searchView.setOnCloseListener {
|
|
||||||
showTitle()
|
|
||||||
false
|
|
||||||
}
|
|
||||||
searchView.setOnSearchClickListener {
|
|
||||||
binding.toolBar.title = ""
|
|
||||||
binding.toolBar.subtitle = ""
|
|
||||||
}
|
|
||||||
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
|
||||||
override fun onQueryTextSubmit(query: String?): Boolean {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String?): Boolean {
|
|
||||||
viewModel.screen(newText)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initBottomBar() {
|
|
||||||
binding.tvDur.text = callBack?.oldBook?.originName
|
|
||||||
binding.tvDur.setOnClickListener {
|
|
||||||
scrollToDurSource()
|
|
||||||
}
|
|
||||||
binding.ivTop.setOnClickListener {
|
|
||||||
binding.recyclerView.scrollToPosition(0)
|
|
||||||
}
|
|
||||||
binding.ivBottom.setOnClickListener {
|
|
||||||
binding.recyclerView.scrollToPosition(searchBookAdapter.itemCount - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initLiveData() {
|
|
||||||
viewModel.searchStateData.observe(viewLifecycleOwner) {
|
|
||||||
binding.refreshProgressBar.isVisible = it
|
|
||||||
if (it) {
|
|
||||||
startStopMenuItem?.let { item ->
|
|
||||||
item.setIcon(R.drawable.ic_stop_black_24dp)
|
|
||||||
item.setTitle(R.string.stop)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
startStopMenuItem?.let { item ->
|
|
||||||
item.setIcon(R.drawable.ic_refresh)
|
|
||||||
item.setTitle(R.string.refresh)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.toolBar.menu.applyTint(requireContext())
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
lifecycle.currentStateFlow.first { it.isAtLeast(STARTED) }
|
|
||||||
viewModel.searchDataFlow.conflate().collect {
|
|
||||||
searchBookAdapter.setItems(it)
|
|
||||||
delay(1000)
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
appDb.bookSourceDao.flowEnabledGroups().conflate().collect {
|
|
||||||
groups.clear()
|
|
||||||
groups.addAll(it)
|
|
||||||
upGroupMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val startStopMenuItem: MenuItem?
|
|
||||||
get() = binding.toolBar.menu.findItem(R.id.menu_start_stop)
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
when (item?.itemId) {
|
|
||||||
R.id.menu_check_author -> {
|
|
||||||
AppConfig.changeSourceCheckAuthor = !item.isChecked
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
viewModel.refresh()
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_load_info -> {
|
|
||||||
AppConfig.changeSourceLoadInfo = !item.isChecked
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_load_toc -> {
|
|
||||||
AppConfig.changeSourceLoadToc = !item.isChecked
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_load_word_count -> {
|
|
||||||
AppConfig.changeSourceLoadWordCount = !item.isChecked
|
|
||||||
item.isChecked = !item.isChecked
|
|
||||||
viewModel.onLoadWordCountChecked(item.isChecked)
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_start_stop -> viewModel.startOrStopSearch()
|
|
||||||
R.id.menu_source_manage -> startActivity<BookSourceActivity>()
|
|
||||||
else -> if (item?.groupId == R.id.source_group && !item.isChecked) {
|
|
||||||
item.isChecked = true
|
|
||||||
if (item.title.toString() == getString(R.string.all_source)) {
|
|
||||||
ChangeSourceConfig.searchScope = ""
|
|
||||||
} else {
|
|
||||||
ChangeSourceConfig.searchScope = item.title.toString()
|
|
||||||
}
|
|
||||||
lifecycleScope.launch(IO) {
|
|
||||||
viewModel.stopSearch()
|
|
||||||
if (viewModel.refresh()) {
|
|
||||||
viewModel.startSearch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun scrollToDurSource() {
|
|
||||||
searchBookAdapter.getItems().forEachIndexed { index, searchBook ->
|
|
||||||
if (searchBook.bookUrl == oldBookUrl) {
|
|
||||||
(binding.recyclerView.layoutManager as LinearLayoutManager)
|
|
||||||
.scrollToPositionWithOffset(index, 60.dpToPx())
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun openToc(searchBook: SearchBook) {
|
|
||||||
this.searchBook = searchBook
|
|
||||||
tocAdapter.setItems(null)
|
|
||||||
binding.recyclerView.gone()
|
|
||||||
binding.clToc.visible()
|
|
||||||
binding.loadingToc.visible()
|
|
||||||
val book = searchBook.toBook()
|
|
||||||
viewModel.getToc(book, { toc: List<BookChapter>, _: BookSource ->
|
|
||||||
tocAdapter.durChapterIndex =
|
|
||||||
BookHelp.getDurChapter(viewModel.chapterIndex, viewModel.chapterTitle, toc)
|
|
||||||
binding.loadingToc.gone()
|
|
||||||
tocAdapter.setItems(toc)
|
|
||||||
binding.recyclerViewToc.scrollToPosition(tocAdapter.durChapterIndex - 5)
|
|
||||||
}, {
|
|
||||||
binding.clToc.gone()
|
|
||||||
AppLog.put("单章换源获取目录出错\n$it", it, true)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
override val oldBookUrl: String?
|
|
||||||
get() = callBack?.oldBook?.bookUrl
|
|
||||||
|
|
||||||
override fun topSource(searchBook: SearchBook) {
|
|
||||||
viewModel.topSource(searchBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun bottomSource(searchBook: SearchBook) {
|
|
||||||
viewModel.bottomSource(searchBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun editSource(searchBook: SearchBook) {
|
|
||||||
editSourceResult.launch {
|
|
||||||
putExtra("sourceUrl", searchBook.origin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun disableSource(searchBook: SearchBook) {
|
|
||||||
viewModel.disableSource(searchBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun deleteSource(searchBook: SearchBook) {
|
|
||||||
viewModel.del(searchBook)
|
|
||||||
if (oldBookUrl == searchBook.bookUrl) {
|
|
||||||
viewModel.autoChangeSource(callBack?.oldBook?.type) { book, toc, source ->
|
|
||||||
callBack?.changeTo(source, book, toc)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun setBookScore(searchBook: SearchBook, score: Int) {
|
|
||||||
viewModel.setBookScore(searchBook, score)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getBookScore(searchBook: SearchBook): Int {
|
|
||||||
return viewModel.getBookScore(searchBook)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?) {
|
|
||||||
searchBook?.let {
|
|
||||||
binding.loadingToc.visible()
|
|
||||||
viewModel.getContent(it.toBook(), bookChapter, nextChapterUrl, contentSuccess) { msg ->
|
|
||||||
binding.loadingToc.gone()
|
|
||||||
binding.clToc.gone()
|
|
||||||
toastOnUi(msg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新分组菜单
|
|
||||||
*/
|
|
||||||
private fun upGroupMenu() {
|
|
||||||
binding.toolBar.menu.findItem(R.id.menu_group)?.subMenu?.transaction { menu ->
|
|
||||||
val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
|
||||||
val selectedGroup = searchScope.displayNames.firstOrNull() ?: ""
|
|
||||||
menu.removeGroup(R.id.source_group)
|
|
||||||
val allItem = menu.add(R.id.source_group, Menu.NONE, Menu.NONE, R.string.all_source)
|
|
||||||
var hasSelectedGroup = false
|
|
||||||
groups.forEach { group ->
|
|
||||||
menu.add(R.id.source_group, Menu.NONE, Menu.NONE, group)?.let {
|
|
||||||
if (group == selectedGroup) {
|
|
||||||
it.isChecked = true
|
|
||||||
hasSelectedGroup = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
menu.setGroupCheckable(R.id.source_group, true, true)
|
|
||||||
if (!hasSelectedGroup) {
|
|
||||||
allItem.isChecked = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
observeEvent<String>(EventBus.SOURCE_CHANGED) {
|
|
||||||
searchBookAdapter.notifyItemRangeChanged(
|
|
||||||
0,
|
|
||||||
searchBookAdapter.itemCount,
|
|
||||||
bundleOf(Pair("upCurSource", oldBookUrl))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
val oldBook: Book?
|
|
||||||
fun changeTo(source: BookSource, book: Book, toc: List<BookChapter>)
|
|
||||||
fun replaceContent(content: String)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
+375
-32
@@ -1,46 +1,389 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
package io.legado.app.ui.book.changesource
|
||||||
|
|
||||||
import android.app.Application
|
import androidx.lifecycle.ViewModel
|
||||||
import android.os.Bundle
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.data.entities.SearchBook
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.data.repository.SearchRepository
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceSearchEvent
|
||||||
|
import io.legado.app.domain.usecase.ChangeSourceSearchUseCase
|
||||||
|
import io.legado.app.domain.usecase.GetChapterContentUseCase
|
||||||
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
|
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
|
||||||
|
|
||||||
@Suppress("MemberVisibilityCanBePrivate")
|
class ChangeChapterSourceViewModel(
|
||||||
class ChangeChapterSourceViewModel(application: Application) :
|
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
||||||
ChangeBookSourceViewModel(application) {
|
private val getChapterContentUseCase: GetChapterContentUseCase,
|
||||||
|
private val searchRepository: SearchRepository,
|
||||||
|
) : ViewModel() {
|
||||||
|
|
||||||
var chapterIndex: Int = 0
|
private val _uiState = MutableStateFlow(ChangeChapterSourceUiState())
|
||||||
var chapterTitle: String = ""
|
val uiState = _uiState.asStateFlow()
|
||||||
|
|
||||||
override fun initData(arguments: Bundle?, book: Book?, fromReadBookActivity: Boolean) {
|
private val _effects = MutableSharedFlow<ChangeChapterSourceEffect>(extraBufferCapacity = 16)
|
||||||
super.initData(arguments, book, fromReadBookActivity)
|
val effects = _effects.asSharedFlow()
|
||||||
arguments?.let { bundle ->
|
|
||||||
bundle.getString("chapterTitle")?.let {
|
// Internal state
|
||||||
chapterTitle = it
|
private var searchJob: Job? = null
|
||||||
|
private var oldBook: Book? = null
|
||||||
|
private var chapterIndex: Int = 0
|
||||||
|
private var chapterTitle: String = ""
|
||||||
|
private var screenKey: String = ""
|
||||||
|
private val searchResults = mutableListOf<SearchBook>()
|
||||||
|
private val bookMap = mutableMapOf<String, SearchBook>()
|
||||||
|
|
||||||
|
// Scope state
|
||||||
|
private val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
||||||
|
|
||||||
|
init {
|
||||||
|
// Load initial scope state
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
scopeState = ScopeUiState(
|
||||||
|
isAll = searchScope.isAll(),
|
||||||
|
isSource = searchScope.isSource(),
|
||||||
|
displayNames = searchScope.displayNames,
|
||||||
|
sourceUrls = searchScope.sourceUrls
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
// Collect enabled groups and sources
|
||||||
|
viewModelScope.launch {
|
||||||
|
searchRepository.enabledGroups.collect { groups ->
|
||||||
|
_uiState.update { it.copy(enabledGroups = groups.toImmutableList()) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
searchRepository.enabledSources.collect { sources ->
|
||||||
|
_uiState.update { it.copy(enabledSources = sources.toImmutableList()) }
|
||||||
}
|
}
|
||||||
chapterIndex = bundle.getInt("chapterIndex")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getContent(
|
fun initData(book: Book, chapterIndex: Int, chapterTitle: String) {
|
||||||
book: Book,
|
this.oldBook = book
|
||||||
chapter: BookChapter,
|
this.chapterIndex = chapterIndex
|
||||||
nextChapterUrl: String?,
|
this.chapterTitle = chapterTitle
|
||||||
success: (content: String) -> Unit,
|
_uiState.update {
|
||||||
error: (msg: String) -> Unit
|
it.copy(
|
||||||
) {
|
showToc = false,
|
||||||
execute {
|
tocItems = persistentListOf(),
|
||||||
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
isLoadingToc = false,
|
||||||
?: throw NoStackTraceException("书源不存在")
|
selectedSourceName = "",
|
||||||
WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false)
|
)
|
||||||
}.onSuccess {
|
}
|
||||||
success.invoke(it)
|
startSearch()
|
||||||
}.onError {
|
}
|
||||||
error.invoke(it.localizedMessage ?: "获取正文出错")
|
|
||||||
|
fun onIntent(intent: ChangeChapterSourceIntent) {
|
||||||
|
when (intent) {
|
||||||
|
is ChangeChapterSourceIntent.UpdateQuery -> {
|
||||||
|
screenKey = intent.query.trim()
|
||||||
|
_uiState.update { it.copy(searchQuery = intent.query) }
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.StartStopSearch -> {
|
||||||
|
if (searchJob?.isActive == true) {
|
||||||
|
stopSearch()
|
||||||
|
} else {
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.Refresh -> {
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SelectSource -> {
|
||||||
|
selectSource(intent.searchBook)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.BackFromToc -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
showToc = false,
|
||||||
|
tocItems = persistentListOf(),
|
||||||
|
isLoadingToc = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SelectChapter -> {
|
||||||
|
selectChapter(intent.chapter)
|
||||||
|
}
|
||||||
|
// Options
|
||||||
|
is ChangeChapterSourceIntent.SetCheckAuthor -> {
|
||||||
|
ChangeSourceConfig.checkAuthor = intent.enabled
|
||||||
|
_uiState.update { it.copy(checkAuthor = intent.enabled) }
|
||||||
|
refreshResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SetLoadInfo -> {
|
||||||
|
ChangeSourceConfig.loadInfo = intent.enabled
|
||||||
|
_uiState.update { it.copy(loadInfo = intent.enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SetLoadToc -> {
|
||||||
|
ChangeSourceConfig.loadToc = intent.enabled
|
||||||
|
_uiState.update { it.copy(loadToc = intent.enabled) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SetLoadWordCount -> {
|
||||||
|
ChangeSourceConfig.loadWordCount = intent.enabled
|
||||||
|
_uiState.update { it.copy(loadWordCount = intent.enabled) }
|
||||||
|
if (intent.enabled) {
|
||||||
|
startSearch()
|
||||||
|
} else {
|
||||||
|
refreshResults()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Source actions
|
||||||
|
is ChangeChapterSourceIntent.TopSource -> {
|
||||||
|
changeSourceSearchUseCase.topSource(intent.searchBook)
|
||||||
|
refreshResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.BottomSource -> {
|
||||||
|
changeSourceSearchUseCase.bottomSource(intent.searchBook)
|
||||||
|
refreshResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.DisableSource -> {
|
||||||
|
changeSourceSearchUseCase.disableSource(intent.searchBook)
|
||||||
|
searchResults.remove(intent.searchBook)
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.DeleteSource -> {
|
||||||
|
changeSourceSearchUseCase.deleteSource(intent.searchBook)
|
||||||
|
searchResults.remove(intent.searchBook)
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
// Scope
|
||||||
|
is ChangeChapterSourceIntent.ShowFilterSheet -> {
|
||||||
|
// Handled by UI
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.DismissFilterSheet -> {
|
||||||
|
// Handled by UI
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.SelectAllScope -> {
|
||||||
|
searchScope.update("")
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.ToggleScopeGroup -> {
|
||||||
|
if (searchScope.isSource()) {
|
||||||
|
searchScope.update("")
|
||||||
|
}
|
||||||
|
val selected = searchScope.displayNames.toMutableSet()
|
||||||
|
if (selected.contains(intent.groupName)) {
|
||||||
|
selected.remove(intent.groupName)
|
||||||
|
} else {
|
||||||
|
selected.add(intent.groupName)
|
||||||
|
}
|
||||||
|
searchScope.update(selected.toList())
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.ToggleScopeSource -> {
|
||||||
|
val selectedUrls = if (searchScope.isSource()) {
|
||||||
|
searchScope.sourceUrls.toMutableSet()
|
||||||
|
} else {
|
||||||
|
mutableSetOf()
|
||||||
|
}
|
||||||
|
if (selectedUrls.contains(intent.source.bookSourceUrl)) {
|
||||||
|
selectedUrls.remove(intent.source.bookSourceUrl)
|
||||||
|
} else {
|
||||||
|
selectedUrls.add(intent.source.bookSourceUrl)
|
||||||
|
}
|
||||||
|
if (selectedUrls.isEmpty()) {
|
||||||
|
searchScope.update("")
|
||||||
|
} else {
|
||||||
|
val selectedSources =
|
||||||
|
io.legado.app.data.appDb.bookSourceDao.allEnabledPart.filter {
|
||||||
|
selectedUrls.contains(it.bookSourceUrl)
|
||||||
|
}
|
||||||
|
searchScope.updateSources(selectedSources)
|
||||||
|
}
|
||||||
|
saveScope()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeChapterSourceIntent.ApplyScope -> {
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
private fun startSearch() {
|
||||||
|
val book = oldBook ?: return
|
||||||
|
stopSearch()
|
||||||
|
searchResults.clear()
|
||||||
|
bookMap.clear()
|
||||||
|
filterResults()
|
||||||
|
|
||||||
|
searchJob = viewModelScope.launch {
|
||||||
|
changeSourceSearchUseCase.search(
|
||||||
|
name = book.name,
|
||||||
|
author = book.author,
|
||||||
|
scope = SearchScope(ChangeSourceConfig.searchScope),
|
||||||
|
oldBook = book,
|
||||||
|
fromReadBookActivity = true,
|
||||||
|
).collect { event ->
|
||||||
|
when (event) {
|
||||||
|
is ChangeSourceSearchEvent.Started -> {
|
||||||
|
_uiState.update { it.copy(isSearching = true) }
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Progress -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
searchProgress = event.processedSources to event.sourceName,
|
||||||
|
totalSourceCount = event.totalSources,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Result -> {
|
||||||
|
searchResults.add(event.searchBook)
|
||||||
|
bookMap[event.searchBook.primaryStr()] = event.searchBook
|
||||||
|
filterResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
is ChangeSourceSearchEvent.Finished -> {
|
||||||
|
_uiState.update { it.copy(isSearching = false) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun stopSearch() {
|
||||||
|
searchJob?.cancel()
|
||||||
|
searchJob = null
|
||||||
|
_uiState.update { it.copy(isSearching = false) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun dispose() {
|
||||||
|
stopSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun refreshResults() {
|
||||||
|
searchResults.clear()
|
||||||
|
bookMap.clear()
|
||||||
|
startSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterResults() {
|
||||||
|
val filtered = if (screenKey.isEmpty()) {
|
||||||
|
searchResults.toList()
|
||||||
|
} else {
|
||||||
|
searchResults.filter {
|
||||||
|
it.name.contains(screenKey) || it.originName.contains(screenKey)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sort by score
|
||||||
|
val sorted = filtered.sortedWith(
|
||||||
|
compareByDescending<SearchBook> { ObservableSourceConfig.getBookScore(it) }
|
||||||
|
.thenByDescending { io.legado.app.help.config.SourceConfig.getSourceScore(it.origin) }
|
||||||
|
.thenBy { it.originOrder }
|
||||||
|
)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
searchResults = sorted.toImmutableList(),
|
||||||
|
bookMap = bookMap.toMap()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectSource(searchBook: SearchBook) {
|
||||||
|
val book = searchBook.toBook()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
showToc = true,
|
||||||
|
selectedSourceName = searchBook.originName,
|
||||||
|
isLoadingToc = true
|
||||||
|
)
|
||||||
|
}
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val (toc, _) = getChapterContentUseCase.getToc(book)
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
tocItems = toc.toImmutableList(),
|
||||||
|
isLoadingToc = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
showToc = false,
|
||||||
|
isLoadingToc = false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
_effects.tryEmit(ChangeChapterSourceEffect.ShowToast("获取目录失败"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun selectChapter(chapter: BookChapter) {
|
||||||
|
val book = oldBook ?: return
|
||||||
|
val selectedSearchBook = _uiState.value.searchResults.find {
|
||||||
|
it.originName == _uiState.value.selectedSourceName
|
||||||
|
} ?: return
|
||||||
|
|
||||||
|
_uiState.update { it.copy(isLoadingToc = true) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
try {
|
||||||
|
val searchBook = selectedSearchBook.toBook()
|
||||||
|
val toc = _uiState.value.tocItems
|
||||||
|
val nextChapterUrl = toc.getOrNull(chapter.index + 1)?.url
|
||||||
|
val content =
|
||||||
|
getChapterContentUseCase.getContent(searchBook, chapter, nextChapterUrl)
|
||||||
|
_uiState.update { it.copy(isLoadingToc = false) }
|
||||||
|
_effects.tryEmit(ChangeChapterSourceEffect.ReplaceContent(content))
|
||||||
|
_effects.tryEmit(ChangeChapterSourceEffect.Dismiss)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
_uiState.update { it.copy(isLoadingToc = false) }
|
||||||
|
_effects.tryEmit(
|
||||||
|
ChangeChapterSourceEffect.ShowToast(
|
||||||
|
e.localizedMessage ?: "获取正文出错"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveScope() {
|
||||||
|
ChangeSourceConfig.searchScope = searchScope.toString()
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
scopeState = ScopeUiState(
|
||||||
|
isAll = searchScope.isAll(),
|
||||||
|
isSource = searchScope.isSource(),
|
||||||
|
displayNames = searchScope.displayNames,
|
||||||
|
sourceUrls = searchScope.sourceUrls
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
refreshResults()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun bookScoreFlow(searchBook: SearchBook) = ObservableSourceConfig.bookScoreFlow(searchBook)
|
||||||
|
|
||||||
|
fun onBookScoreClick(searchBook: SearchBook) {
|
||||||
|
val currentScore = ObservableSourceConfig.getBookScore(searchBook)
|
||||||
|
ObservableSourceConfig.setBookScore(searchBook, if (currentScore > 0) 0 else 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
package io.legado.app.ui.book.changesource
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.BookChapter
|
|
||||||
import io.legado.app.databinding.ItemChapterListBinding
|
|
||||||
import io.legado.app.lib.theme.ThemeUtils
|
|
||||||
//import io.legado.app.lib.theme.accentColor
|
|
||||||
import io.legado.app.utils.getCompatColor
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
class ChangeChapterTocAdapter(context: Context, val callback: Callback) :
|
|
||||||
RecyclerAdapter<BookChapter, ItemChapterListBinding>(context) {
|
|
||||||
|
|
||||||
var durChapterIndex = 0
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
|
|
||||||
return ItemChapterListBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemChapterListBinding,
|
|
||||||
item: BookChapter,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.run {
|
|
||||||
val isDur = durChapterIndex == item.index
|
|
||||||
ivVolume.gone()
|
|
||||||
ivLocked.gone()
|
|
||||||
if (isDur) {
|
|
||||||
tvChapterName.setTextColor(context.themeColor(androidx.appcompat.R.attr.colorPrimary))
|
|
||||||
} else {
|
|
||||||
tvChapterName.setTextColor(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
|
|
||||||
}
|
|
||||||
|
|
||||||
tvChapterName.text = item.title
|
|
||||||
if (item.isVolume) {
|
|
||||||
//卷名,如第一卷 突出显示
|
|
||||||
tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
|
|
||||||
} else {
|
|
||||||
//普通章节 保持不变
|
|
||||||
tvChapterItem.background =
|
|
||||||
ThemeUtils.resolveDrawable(context, android.R.attr.selectableItemBackground)
|
|
||||||
}
|
|
||||||
if (!item.tag.isNullOrEmpty() && !item.isVolume) {
|
|
||||||
//卷名不显示tag(更新时间规则)
|
|
||||||
tvTag.text = item.tag
|
|
||||||
tvTag.visible()
|
|
||||||
} else {
|
|
||||||
tvTag.gone()
|
|
||||||
}
|
|
||||||
ivChecked.setImageResource(R.drawable.ic_check)
|
|
||||||
ivChecked.visible(isDur)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemChapterListBinding) {
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
callback.clickChapter(it, getItem(holder.layoutPosition + 1)?.url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface Callback {
|
|
||||||
fun clickChapter(bookChapter: BookChapter, nextChapterUrl: String?)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -29,6 +29,16 @@ class BookInfoActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
onOpenSearch = { keyword ->
|
onOpenSearch = { keyword ->
|
||||||
startActivity(MainActivity.createSearchIntent(this, key = keyword))
|
startActivity(MainActivity.createSearchIntent(this, key = keyword))
|
||||||
},
|
},
|
||||||
|
onOpenReader = { bookUrl, inBookshelf, chapterChanged ->
|
||||||
|
startActivity(
|
||||||
|
MainActivity.createReadBookIntent(
|
||||||
|
context = this,
|
||||||
|
bookUrl = bookUrl,
|
||||||
|
inBookshelf = inBookshelf,
|
||||||
|
chapterChanged = chapterChanged,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath ->
|
onNavigateToBookInfo = { name, author, bookUrl, origin, coverPath ->
|
||||||
startActivity<BookInfoActivity> {
|
startActivity<BookInfoActivity> {
|
||||||
putExtra("bookUrl", bookUrl)
|
putExtra("bookUrl", bookUrl)
|
||||||
|
|||||||
@@ -10,6 +10,8 @@ import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
|||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
import kotlinx.collections.immutable.persistentListOf
|
import kotlinx.collections.immutable.persistentListOf
|
||||||
|
|
||||||
|
const val READER_RESULT_DELETED = 100
|
||||||
|
|
||||||
data class BookInfoUiState(
|
data class BookInfoUiState(
|
||||||
val book: Book? = null,
|
val book: Book? = null,
|
||||||
val chapterList: List<BookChapter> = emptyList(),
|
val chapterList: List<BookChapter> = emptyList(),
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import io.legado.app.model.SourceCallBack
|
|||||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||||
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
|
import io.legado.app.ui.book.info.edit.BookInfoEditActivity
|
||||||
import io.legado.app.ui.book.manga.ReadMangaActivity
|
import io.legado.app.ui.book.manga.ReadMangaActivity
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.book.toc.TocActivityResult
|
import io.legado.app.ui.book.toc.TocActivityResult
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||||
@@ -48,6 +47,7 @@ fun BookInfoRouteScreen(
|
|||||||
onBack: () -> Unit,
|
onBack: () -> Unit,
|
||||||
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
||||||
onOpenSearch: (String) -> Unit,
|
onOpenSearch: (String) -> Unit,
|
||||||
|
onOpenReader: (bookUrl: String, inBookshelf: Boolean, chapterChanged: Boolean) -> Unit = { _, _, _ -> },
|
||||||
onNavigateToBookInfo: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?) -> Unit = { _, _, _, _, _ -> },
|
onNavigateToBookInfo: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?) -> Unit = { _, _, _, _, _ -> },
|
||||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit = { _, _, _ -> },
|
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit = { _, _, _ -> },
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
@@ -123,7 +123,15 @@ fun BookInfoRouteScreen(
|
|||||||
ReadMangaActivity::class.java
|
ReadMangaActivity::class.java
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> ReadBookActivity::class.java
|
else -> null
|
||||||
|
}
|
||||||
|
if (cls == null) {
|
||||||
|
onOpenReader(
|
||||||
|
effect.book.bookUrl,
|
||||||
|
effect.inBookshelf,
|
||||||
|
effect.chapterChanged,
|
||||||
|
)
|
||||||
|
return@collectLatest
|
||||||
}
|
}
|
||||||
readBookResult.launch(
|
readBookResult.launch(
|
||||||
Intent(activity, cls).apply {
|
Intent(activity, cls).apply {
|
||||||
|
|||||||
@@ -576,7 +576,7 @@ fun ChangeSourceSheet(
|
|||||||
|
|
||||||
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
||||||
loadingAction = true
|
loadingAction = true
|
||||||
val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook()
|
val book = viewModel.getBookFromMap(searchBook.primaryStr()) ?: searchBook.toBook()
|
||||||
viewModel.getToc(
|
viewModel.getToc(
|
||||||
book,
|
book,
|
||||||
onSuccess = { toc, source ->
|
onSuccess = { toc, source ->
|
||||||
|
|||||||
@@ -11,7 +11,6 @@ import coil.ImageLoader
|
|||||||
import coil.request.SuccessResult
|
import coil.request.SuccessResult
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
|
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
@@ -56,12 +55,13 @@ import io.legado.app.ui.main.MainIntent
|
|||||||
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
import io.legado.app.ui.widget.components.image.cover.buildCoverImageRequest
|
||||||
import io.legado.app.utils.ArchiveUtils
|
import io.legado.app.utils.ArchiveUtils
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.fromJsonArray
|
|
||||||
import io.legado.app.utils.ImageSaveUtils
|
import io.legado.app.utils.ImageSaveUtils
|
||||||
import io.legado.app.utils.UrlUtil
|
import io.legado.app.utils.UrlUtil
|
||||||
|
import io.legado.app.utils.fromJsonArray
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.collections.immutable.toImmutableList
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.Job
|
import kotlinx.coroutines.Job
|
||||||
@@ -76,7 +76,6 @@ import kotlinx.coroutines.flow.collectLatest
|
|||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.update
|
import kotlinx.coroutines.flow.update
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import kotlinx.collections.immutable.toImmutableList
|
|
||||||
import java.io.ByteArrayOutputStream
|
import java.io.ByteArrayOutputStream
|
||||||
|
|
||||||
class BookInfoViewModel(
|
class BookInfoViewModel(
|
||||||
@@ -346,7 +345,7 @@ class BookInfoViewModel(
|
|||||||
syncUiState()
|
syncUiState()
|
||||||
}
|
}
|
||||||
|
|
||||||
io.legado.app.ui.book.read.ReadBookActivity.RESULT_DELETED -> {
|
READER_RESULT_DELETED -> {
|
||||||
emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
|
emitEffect(BookInfoEffect.Finish(resultCode = RESULT_OK))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
|||||||
import io.legado.app.receiver.NetworkChangedListener
|
import io.legado.app.receiver.NetworkChangedListener
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||||
import io.legado.app.ui.book.info.BookInfoActivity
|
import io.legado.app.ui.book.info.BookInfoActivity
|
||||||
|
import io.legado.app.ui.book.info.READER_RESULT_DELETED
|
||||||
import io.legado.app.ui.book.manga.config.MangaAutoReadDialog
|
import io.legado.app.ui.book.manga.config.MangaAutoReadDialog
|
||||||
import io.legado.app.ui.book.manga.config.MangaClickActionConfigDialog
|
import io.legado.app.ui.book.manga.config.MangaClickActionConfigDialog
|
||||||
import io.legado.app.ui.book.manga.config.MangaColorFilterConfig
|
import io.legado.app.ui.book.manga.config.MangaColorFilterConfig
|
||||||
@@ -73,7 +74,6 @@ import io.legado.app.ui.book.manga.recyclerview.WebtoonFrame
|
|||||||
import io.legado.app.ui.book.read.EyeProtectionRefreshScheduler
|
import io.legado.app.ui.book.read.EyeProtectionRefreshScheduler
|
||||||
import io.legado.app.ui.book.read.MangaMenu
|
import io.legado.app.ui.book.read.MangaMenu
|
||||||
import io.legado.app.ui.book.read.observeEyeProtectionEvents
|
import io.legado.app.ui.book.read.observeEyeProtectionEvents
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity.Companion.RESULT_DELETED
|
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.book.toc.TocActivityResult
|
import io.legado.app.ui.book.toc.TocActivityResult
|
||||||
import io.legado.app.ui.browser.WebViewActivity
|
import io.legado.app.ui.browser.WebViewActivity
|
||||||
@@ -185,7 +185,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
private val bookInfoActivity =
|
private val bookInfoActivity =
|
||||||
registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) {
|
registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) {
|
||||||
if (it.resultCode == RESULT_OK) {
|
if (it.resultCode == RESULT_OK) {
|
||||||
setResult(RESULT_DELETED)
|
setResult(READER_RESULT_DELETED)
|
||||||
super.finish()
|
super.finish()
|
||||||
} else {
|
} else {
|
||||||
ReadManga.loadOrUpContent()
|
ReadManga.loadOrUpContent()
|
||||||
@@ -498,7 +498,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
override fun onColorSelected(dialogId: Int, color: Int){
|
override fun onColorSelected(dialogId: Int, color: Int){
|
||||||
if (dialogId == MANGA_B)
|
if (dialogId == MANGA_B)
|
||||||
{
|
{
|
||||||
AppConfig.mangaBackground = color
|
viewModel.setMangaBackground(color)
|
||||||
setBackground()
|
setBackground()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -703,7 +703,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
getString(R.string.pre_download),
|
getString(R.string.pre_download),
|
||||||
AppConfig.mangaPreDownloadNum
|
AppConfig.mangaPreDownloadNum
|
||||||
) {
|
) {
|
||||||
AppConfig.mangaPreDownloadNum = it
|
viewModel.setMangaPreDownloadNum(it)
|
||||||
item.title = getString(R.string.pre_download_m, it)
|
item.title = getString(R.string.pre_download_m, it)
|
||||||
setRecyclerViewPreloader(it)
|
setRecyclerViewPreloader(it)
|
||||||
}
|
}
|
||||||
@@ -757,20 +757,20 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
|
|
||||||
//点击滑动
|
//点击滑动
|
||||||
override fun onClickScrollDisabledChanged(disabled: Boolean) {
|
override fun onClickScrollDisabledChanged(disabled: Boolean) {
|
||||||
AppConfig.disableClickScroll = disabled
|
viewModel.setDisableClickScroll(disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onScrollAniDisabledChanged(disabled: Boolean) {
|
override fun onScrollAniDisabledChanged(disabled: Boolean) {
|
||||||
AppConfig.disableMangaScrollAnimation = disabled
|
viewModel.setDisableMangaScrollAnimation(disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCrossFadeDisabledChanged(disabled: Boolean) {
|
override fun onCrossFadeDisabledChanged(disabled: Boolean) {
|
||||||
AppConfig.disableMangaCrossFade = disabled
|
viewModel.setDisableMangaCrossFade(disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
//双击缩放
|
//双击缩放
|
||||||
override fun onMangaScaleDisabledChanged(disabled: Boolean) {
|
override fun onMangaScaleDisabledChanged(disabled: Boolean) {
|
||||||
AppConfig.disableMangaScale = disabled
|
viewModel.setDisableMangaScale(disabled)
|
||||||
setDisableMangaScale(disabled)
|
setDisableMangaScale(disabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -784,16 +784,13 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
|
|
||||||
//墨水屏
|
//墨水屏
|
||||||
override fun updateEpaperMode(enabled: Boolean, threshold: Int) {
|
override fun updateEpaperMode(enabled: Boolean, threshold: Int) {
|
||||||
AppConfig.enableMangaEInk = enabled
|
viewModel.setEInkMode(enabled, threshold)
|
||||||
AppConfig.enableMangaGray = false
|
|
||||||
AppConfig.mangaEInkThreshold = threshold
|
|
||||||
mAdapter.enableMangaEInk(enabled, threshold)
|
mAdapter.enableMangaEInk(enabled, threshold)
|
||||||
}
|
}
|
||||||
|
|
||||||
//灰度
|
//灰度
|
||||||
override fun updateGrayMode(enabled: Boolean) {
|
override fun updateGrayMode(enabled: Boolean) {
|
||||||
AppConfig.enableMangaGray = enabled
|
viewModel.setGrayMode(enabled)
|
||||||
AppConfig.enableMangaEInk = false
|
|
||||||
mAdapter.enableGray(enabled)
|
mAdapter.enableGray(enabled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -811,7 +808,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
//自动翻页速度
|
//自动翻页速度
|
||||||
override fun onAutoPageSpeedChanged(speed: Int) {
|
override fun onAutoPageSpeedChanged(speed: Int) {
|
||||||
setAutoReadEnabled(false)
|
setAutoReadEnabled(false)
|
||||||
AppConfig.mangaAutoPageSpeed = speed
|
viewModel.setMangaAutoPageSpeed(speed)
|
||||||
mScrollTimer.setSpeed(speed)
|
mScrollTimer.setSpeed(speed)
|
||||||
setAutoReadEnabled(enableScroll)
|
setAutoReadEnabled(enableScroll)
|
||||||
// if (enableAutoScrollPage) {
|
// if (enableAutoScrollPage) {
|
||||||
@@ -820,19 +817,19 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onMangaLongClickChanged(checked: Boolean) {
|
override fun onMangaLongClickChanged(checked: Boolean) {
|
||||||
AppConfig.mangaLongClick = checked
|
viewModel.setMangaLongClick(checked)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onVolumeKeyPageChanged(enable: Boolean) {
|
override fun onVolumeKeyPageChanged(enable: Boolean) {
|
||||||
AppConfig.MangaVolumeKeyPage = enable
|
viewModel.setMangaVolumeKeyPage(enable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onReverseVolumeKeyPageChanged(enable: Boolean) {
|
override fun onReverseVolumeKeyPageChanged(enable: Boolean) {
|
||||||
AppConfig.reverseVolumeKeyPage = enable
|
viewModel.setReverseVolumeKeyPage(enable)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onHideMangaTitleChanged(hide: Boolean) {
|
override fun onHideMangaTitleChanged(hide: Boolean) {
|
||||||
AppConfig.hideMangaTitle = hide
|
viewModel.setHideMangaTitle(hide)
|
||||||
ReadManga.loadContent()
|
ReadManga.loadContent()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import io.legado.app.data.entities.Book
|
|||||||
import io.legado.app.data.entities.Book.ReadConfig
|
import io.legado.app.data.entities.Book.ReadConfig
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.data.entities.BookProgress
|
import io.legado.app.data.entities.BookProgress
|
||||||
|
import io.legado.app.data.repository.MangaSettingsRepository
|
||||||
import io.legado.app.domain.model.ReadingProgress
|
import io.legado.app.domain.model.ReadingProgress
|
||||||
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
import io.legado.app.domain.usecase.GetReadingProgressUseCase
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
@@ -50,7 +51,8 @@ import java.io.ByteArrayOutputStream
|
|||||||
|
|
||||||
class ReadMangaViewModel(
|
class ReadMangaViewModel(
|
||||||
application: Application,
|
application: Application,
|
||||||
private val getReadingProgressUseCase: GetReadingProgressUseCase
|
private val getReadingProgressUseCase: GetReadingProgressUseCase,
|
||||||
|
private val mangaSettingsRepository: MangaSettingsRepository
|
||||||
) : BaseViewModel(application) {
|
) : BaseViewModel(application) {
|
||||||
|
|
||||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||||
@@ -378,6 +380,91 @@ class ReadMangaViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun setMangaPreDownloadNum(value: Int) {
|
||||||
|
AppConfig.mangaPreDownloadNum = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaPreDownloadNum(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaBackground(value: Int) {
|
||||||
|
AppConfig.mangaBackground = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaBackground(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisableClickScroll(value: Boolean) {
|
||||||
|
AppConfig.disableClickScroll = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setDisableClickScroll(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisableMangaScrollAnimation(value: Boolean) {
|
||||||
|
AppConfig.disableMangaScrollAnimation = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setDisableMangaScrollAnimation(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisableMangaCrossFade(value: Boolean) {
|
||||||
|
AppConfig.disableMangaCrossFade = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setDisableMangaCrossFade(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDisableMangaScale(value: Boolean) {
|
||||||
|
AppConfig.disableMangaScale = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setDisableMangaScale(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setEInkMode(enabled: Boolean, threshold: Int) {
|
||||||
|
AppConfig.enableMangaEInk = enabled
|
||||||
|
AppConfig.enableMangaGray = false
|
||||||
|
AppConfig.mangaEInkThreshold = threshold
|
||||||
|
viewModelScope.launch {
|
||||||
|
mangaSettingsRepository.setEnableMangaEInk(enabled)
|
||||||
|
mangaSettingsRepository.setEnableMangaGray(false)
|
||||||
|
mangaSettingsRepository.setMangaEInkThreshold(threshold)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setGrayMode(enabled: Boolean) {
|
||||||
|
AppConfig.enableMangaGray = enabled
|
||||||
|
AppConfig.enableMangaEInk = false
|
||||||
|
viewModelScope.launch {
|
||||||
|
mangaSettingsRepository.setEnableMangaGray(enabled)
|
||||||
|
mangaSettingsRepository.setEnableMangaEInk(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaAutoPageSpeed(value: Int) {
|
||||||
|
AppConfig.mangaAutoPageSpeed = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaAutoPageSpeed(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaLongClick(value: Boolean) {
|
||||||
|
AppConfig.mangaLongClick = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaLongClick(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaVolumeKeyPage(value: Boolean) {
|
||||||
|
AppConfig.MangaVolumeKeyPage = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaVolumeKeyPage(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setReverseVolumeKeyPage(value: Boolean) {
|
||||||
|
AppConfig.reverseVolumeKeyPage = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setReverseVolumeKeyPage(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setHideMangaTitle(value: Boolean) {
|
||||||
|
AppConfig.hideMangaTitle = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setHideMangaTitle(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaColorFilter(value: String) {
|
||||||
|
AppConfig.mangaColorFilter = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaAutoColorFilter(value) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setMangaFooterConfig(value: String) {
|
||||||
|
AppConfig.mangaFooterConfig = value
|
||||||
|
viewModelScope.launch { mangaSettingsRepository.setMangaFooterConfig(value) }
|
||||||
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
super.onCleared()
|
super.onCleared()
|
||||||
changeSourceCoroutine?.cancel()
|
changeSourceCoroutine?.cancel()
|
||||||
|
|||||||
+43
-11
@@ -8,15 +8,17 @@ import android.widget.TextView
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseOverlayDialogFragment
|
import io.legado.app.base.BaseOverlayDialogFragment
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.data.repository.MangaSettingsRepository
|
||||||
import io.legado.app.databinding.DialogClickActionConfigBinding
|
import io.legado.app.databinding.DialogClickActionConfigBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
import io.legado.app.utils.getCompatColor
|
import io.legado.app.utils.getCompatColor
|
||||||
import io.legado.app.utils.putPrefInt
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_click_action_config) {
|
class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_click_action_config) {
|
||||||
private val binding by viewBinding(DialogClickActionConfigBinding::bind)
|
private val binding by viewBinding(DialogClickActionConfigBinding::bind)
|
||||||
|
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||||
|
|
||||||
private val actions by lazy {
|
private val actions by lazy {
|
||||||
linkedMapOf(
|
linkedMapOf(
|
||||||
@@ -66,68 +68,85 @@ class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_c
|
|||||||
|
|
||||||
binding.tvTopLeft.setOnClickListener {
|
binding.tvTopLeft.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionTL, action)
|
setClickAction(PreferKey.mangaClickActionTL, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvTopCenter.setOnClickListener {
|
binding.tvTopCenter.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionTC, action)
|
setClickAction(PreferKey.mangaClickActionTC, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvTopRight.setOnClickListener {
|
binding.tvTopRight.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionTR, action)
|
setClickAction(PreferKey.mangaClickActionTR, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvMiddleLeft.setOnClickListener {
|
binding.tvMiddleLeft.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionML, action)
|
setClickAction(PreferKey.mangaClickActionML, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvMiddleCenter.setOnClickListener {
|
binding.tvMiddleCenter.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionMC, action)
|
setClickAction(PreferKey.mangaClickActionMC, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvMiddleRight.setOnClickListener {
|
binding.tvMiddleRight.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionMR, action)
|
setClickAction(PreferKey.mangaClickActionMR, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvBottomLeft.setOnClickListener {
|
binding.tvBottomLeft.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionBL, action)
|
setClickAction(PreferKey.mangaClickActionBL, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvBottomCenter.setOnClickListener {
|
binding.tvBottomCenter.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionBC, action)
|
setClickAction(PreferKey.mangaClickActionBC, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
binding.tvBottomRight.setOnClickListener {
|
binding.tvBottomRight.setOnClickListener {
|
||||||
selectAction { action ->
|
selectAction { action ->
|
||||||
putPrefInt(PreferKey.mangaClickActionBR, action)
|
setClickAction(PreferKey.mangaClickActionBR, action)
|
||||||
(it as? TextView)?.text = actions[action]
|
(it as? TextView)?.text = actions[action]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun setClickAction(key: String, action: Int) {
|
||||||
|
when (key) {
|
||||||
|
PreferKey.mangaClickActionTL -> AppConfig.mangaClickActionTL = action
|
||||||
|
PreferKey.mangaClickActionTC -> AppConfig.mangaClickActionTC = action
|
||||||
|
PreferKey.mangaClickActionTR -> AppConfig.mangaClickActionTR = action
|
||||||
|
PreferKey.mangaClickActionML -> AppConfig.mangaClickActionML = action
|
||||||
|
PreferKey.mangaClickActionMC -> AppConfig.mangaClickActionMC = action
|
||||||
|
PreferKey.mangaClickActionMR -> AppConfig.mangaClickActionMR = action
|
||||||
|
PreferKey.mangaClickActionBL -> AppConfig.mangaClickActionBL = action
|
||||||
|
PreferKey.mangaClickActionBC -> AppConfig.mangaClickActionBC = action
|
||||||
|
PreferKey.mangaClickActionBR -> AppConfig.mangaClickActionBR = action
|
||||||
|
}
|
||||||
|
execute {
|
||||||
|
mangaSettingsRepository.setMangaClickAction(key, action)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun selectAction(success: (action: Int) -> Unit) {
|
private fun selectAction(success: (action: Int) -> Unit) {
|
||||||
context?.selector(
|
context?.selector(
|
||||||
getString(R.string.select_action),
|
getString(R.string.select_action),
|
||||||
@@ -138,7 +157,20 @@ class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_c
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onDestroy() {
|
override fun onDestroy() {
|
||||||
|
if (!hasMenuClickArea()) {
|
||||||
|
AppConfig.detectMangaClickArea()
|
||||||
|
execute {
|
||||||
|
mangaSettingsRepository.setMangaClickAction(PreferKey.mangaClickActionMC, 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
super.onDestroy()
|
super.onDestroy()
|
||||||
AppConfig.detectMangaClickArea()
|
}
|
||||||
|
|
||||||
|
private fun hasMenuClickArea(): Boolean {
|
||||||
|
return AppConfig.mangaClickActionTL * AppConfig.mangaClickActionTC *
|
||||||
|
AppConfig.mangaClickActionTR * AppConfig.mangaClickActionML *
|
||||||
|
AppConfig.mangaClickActionMC * AppConfig.mangaClickActionMR *
|
||||||
|
AppConfig.mangaClickActionBL * AppConfig.mangaClickActionBC *
|
||||||
|
AppConfig.mangaClickActionBR == 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -7,6 +7,7 @@ import android.view.WindowManager
|
|||||||
import androidx.core.view.isVisible
|
import androidx.core.view.isVisible
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
import io.legado.app.base.BaseBottomSheetDialogFragment
|
||||||
|
import io.legado.app.data.repository.MangaSettingsRepository
|
||||||
import io.legado.app.databinding.DialogMangaColorFilterBinding
|
import io.legado.app.databinding.DialogMangaColorFilterBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
@@ -14,9 +15,11 @@ import io.legado.app.utils.fromJsonObject
|
|||||||
import io.legado.app.utils.invisible
|
import io.legado.app.utils.invisible
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_color_filter) {
|
class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_color_filter) {
|
||||||
private val binding by viewBinding(DialogMangaColorFilterBinding::bind)
|
private val binding by viewBinding(DialogMangaColorFilterBinding::bind)
|
||||||
|
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||||
private val mConfig =
|
private val mConfig =
|
||||||
GSON.fromJsonObject<MangaColorFilterConfig>(AppConfig.mangaColorFilter).getOrNull()
|
GSON.fromJsonObject<MangaColorFilterConfig>(AppConfig.mangaColorFilter).getOrNull()
|
||||||
?: MangaColorFilterConfig()
|
?: MangaColorFilterConfig()
|
||||||
@@ -114,8 +117,13 @@ class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_man
|
|||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
AppConfig.mangaColorFilter = mConfig.toJson()
|
val colorFilter = mConfig.toJson()
|
||||||
|
AppConfig.mangaColorFilter = colorFilter
|
||||||
AppConfig.mangaEInkThreshold = mMangaEInkThreshold
|
AppConfig.mangaEInkThreshold = mMangaEInkThreshold
|
||||||
|
execute {
|
||||||
|
mangaSettingsRepository.setMangaAutoColorFilter(colorFilter)
|
||||||
|
mangaSettingsRepository.setMangaEInkThreshold(mMangaEInkThreshold)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
@@ -124,4 +132,4 @@ class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_man
|
|||||||
fun updateGrayMode(enabled: Boolean)
|
fun updateGrayMode(enabled: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -11,6 +11,7 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
import io.legado.app.base.BaseBottomSheetDialogFragment
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.data.repository.MangaSettingsRepository
|
||||||
import io.legado.app.databinding.DialogMangaFooterSettingBinding
|
import io.legado.app.databinding.DialogMangaFooterSettingBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.ui.book.manga.entities.MangaFooterConfig
|
import io.legado.app.ui.book.manga.entities.MangaFooterConfig
|
||||||
@@ -20,6 +21,7 @@ import io.legado.app.utils.fromJsonObject
|
|||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
import org.koin.android.ext.android.inject
|
||||||
|
|
||||||
class MangaFooterSettingDialog :
|
class MangaFooterSettingDialog :
|
||||||
BaseBottomSheetDialogFragment(R.layout.dialog_manga_footer_setting) {
|
BaseBottomSheetDialogFragment(R.layout.dialog_manga_footer_setting) {
|
||||||
@@ -37,6 +39,7 @@ class MangaFooterSettingDialog :
|
|||||||
var callback: Callback? = null
|
var callback: Callback? = null
|
||||||
|
|
||||||
private val binding by viewBinding(DialogMangaFooterSettingBinding::bind)
|
private val binding by viewBinding(DialogMangaFooterSettingBinding::bind)
|
||||||
|
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
@@ -255,7 +258,11 @@ class MangaFooterSettingDialog :
|
|||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
override fun onDismiss(dialog: DialogInterface) {
|
||||||
super.onDismiss(dialog)
|
super.onDismiss(dialog)
|
||||||
AppConfig.mangaFooterConfig = GSON.toJson(config)
|
val footerConfig = GSON.toJson(config)
|
||||||
|
AppConfig.mangaFooterConfig = footerConfig
|
||||||
|
execute {
|
||||||
|
mangaSettingsRepository.setMangaFooterConfig(footerConfig)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun updateChapterText() {
|
private fun updateChapterText() {
|
||||||
@@ -311,4 +318,4 @@ class MangaFooterSettingDialog :
|
|||||||
fun onMangaLongClickChanged(checked: Boolean)
|
fun onMangaLongClickChanged(checked: Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,432 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.pm.ActivityInfo
|
|
||||||
import android.os.Build
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.View
|
|
||||||
import android.view.WindowInsets
|
|
||||||
import android.view.WindowManager
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.core.view.WindowInsetsCompat
|
|
||||||
import androidx.core.view.doOnAttach
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import androidx.core.view.updateLayoutParams
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import com.google.android.material.datepicker.MaterialDatePicker
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.VMBaseActivity
|
|
||||||
import io.legado.app.constant.AppConst.charsets
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.ActivityBookReadBinding
|
|
||||||
import io.legado.app.databinding.DialogDownloadChoiceBinding
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
|
||||||
import io.legado.app.databinding.DialogSimulatedReadingBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.LocalConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.lib.dialogs.selector
|
|
||||||
import io.legado.app.model.CacheBook
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.book.read.config.BgTextConfigDialog
|
|
||||||
import io.legado.app.ui.book.read.config.ClickActionConfigDialog
|
|
||||||
import io.legado.app.ui.book.read.config.FontConfigDialog
|
|
||||||
import io.legado.app.ui.book.read.config.FontSelectDialog
|
|
||||||
import io.legado.app.ui.book.read.config.InfoConfigDialog
|
|
||||||
import io.legado.app.ui.book.read.config.PaddingConfigDialog
|
|
||||||
import io.legado.app.ui.book.read.config.PageKeyDialog
|
|
||||||
import io.legado.app.ui.book.read.config.ShadowSetDialog
|
|
||||||
import io.legado.app.ui.book.read.config.UnderlineConfigDialog
|
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
|
||||||
import io.legado.app.utils.ColorUtils
|
|
||||||
import io.legado.app.utils.FileDoc
|
|
||||||
import io.legado.app.utils.find
|
|
||||||
import io.legado.app.utils.getPrefString
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.isTv
|
|
||||||
import io.legado.app.utils.setLightStatusBar
|
|
||||||
import io.legado.app.utils.setNavigationBarColorAuto
|
|
||||||
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import org.koin.androidx.viewmodel.ext.android.viewModel
|
|
||||||
import java.time.Instant
|
|
||||||
import java.time.LocalDate
|
|
||||||
import java.time.ZoneId
|
|
||||||
import java.time.format.DateTimeFormatter
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阅读界面
|
|
||||||
*/
|
|
||||||
abstract class BaseReadBookActivity :
|
|
||||||
VMBaseActivity<ActivityBookReadBinding, ReadBookViewModel>(imageBg = false) {
|
|
||||||
|
|
||||||
override val binding by viewBinding(ActivityBookReadBinding::inflate)
|
|
||||||
override val viewModel by viewModel<ReadBookViewModel>()
|
|
||||||
protected val menuLayoutIsVisible
|
|
||||||
get() = bottomDialog > 0 || binding.readMenu.isVisible || binding.searchMenu.bottomMenuVisible
|
|
||||||
|
|
||||||
var bottomDialog = 0
|
|
||||||
set(value) {
|
|
||||||
if (field != value) {
|
|
||||||
field = value
|
|
||||||
onBottomDialogChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val selectBookFolderResult = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
ReadBook.book?.let { book ->
|
|
||||||
FileDoc.fromUri(uri, true).find(book.originName)?.let { doc ->
|
|
||||||
book.bookUrl = doc.uri.toString()
|
|
||||||
book.save()
|
|
||||||
viewModel.loadChapterList(book)
|
|
||||||
} ?: ReadBook.upMsg("找不到文件")
|
|
||||||
}
|
|
||||||
} ?: ReadBook.upMsg("没有权限访问")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
|
||||||
ReadBook.msg = null
|
|
||||||
setOrientation()
|
|
||||||
upLayoutInDisplayCutoutMode()
|
|
||||||
super.onCreate(savedInstanceState)
|
|
||||||
binding.navigationBar.doOnAttach {
|
|
||||||
binding.navigationBar.setOnApplyWindowInsetsListenerCompat { view, windowInsets ->
|
|
||||||
val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars())
|
|
||||||
view.updateLayoutParams {
|
|
||||||
height = insets.bottom
|
|
||||||
}
|
|
||||||
windowInsets
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.permissionDenialLiveData.observe(this) {
|
|
||||||
selectBookFolderResult.launch {
|
|
||||||
mode = HandleFileContract.DIR_SYS
|
|
||||||
title = "选择书籍所在文件夹"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!LocalConfig.readHelpVersionIsLast) {
|
|
||||||
if (isTv) {
|
|
||||||
showCustomPageKeyConfig()
|
|
||||||
} else {
|
|
||||||
showClickRegionalConfig()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onBottomDialogChange() {
|
|
||||||
when (bottomDialog) {
|
|
||||||
0 -> onMenuHide()
|
|
||||||
1 -> onMenuShow()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onMenuShow() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
open fun onMenuHide() {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showInfoConfig() {
|
|
||||||
showDialogFragment<InfoConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showFont() {
|
|
||||||
showDialogFragment<FontConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showPaddingConfig() {
|
|
||||||
showDialogFragment<PaddingConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showShadowSet() {
|
|
||||||
showDialogFragment<ShadowSetDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showFontSelect() {
|
|
||||||
showDialogFragment<FontSelectDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showUnderlineConfig() {
|
|
||||||
showDialogFragment<UnderlineConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showBgTextConfig() {
|
|
||||||
showDialogFragment<BgTextConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showClickRegionalConfig() {
|
|
||||||
showDialogFragment<ClickActionConfigDialog>()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCustomPageKeyConfig() {
|
|
||||||
PageKeyDialog(this).show()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 屏幕方向
|
|
||||||
*/
|
|
||||||
@SuppressLint("SourceLockedOrientationActivity")
|
|
||||||
fun setOrientation() {
|
|
||||||
when (AppConfig.screenOrientation) {
|
|
||||||
"0" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
|
|
||||||
"1" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
|
|
||||||
"2" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE
|
|
||||||
"3" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_SENSOR
|
|
||||||
"4" -> requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 更新状态栏,导航栏
|
|
||||||
*/
|
|
||||||
fun upSystemUiVisibility(
|
|
||||||
isInMultiWindow: Boolean,
|
|
||||||
toolBarHide: Boolean = true,
|
|
||||||
) {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
|
||||||
window.insetsController?.run {
|
|
||||||
if (toolBarHide && ReadBookConfig.hideNavigationBar) {
|
|
||||||
hide(WindowInsets.Type.navigationBars())
|
|
||||||
} else {
|
|
||||||
show(WindowInsets.Type.navigationBars())
|
|
||||||
}
|
|
||||||
if (toolBarHide && ReadBookConfig.hideStatusBar) {
|
|
||||||
hide(WindowInsets.Type.statusBars())
|
|
||||||
} else {
|
|
||||||
show(WindowInsets.Type.statusBars())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
upSystemUiVisibilityO(isInMultiWindow, toolBarHide)
|
|
||||||
if (toolBarHide) {
|
|
||||||
setLightStatusBar(ReadBookConfig.durConfig.curStatusIconDark())
|
|
||||||
} else {
|
|
||||||
val statusBarColor =
|
|
||||||
if (AppConfig.readBarStyleFollowPage
|
|
||||||
&& ReadBookConfig.durConfig.curBgType() == 0
|
|
||||||
) {
|
|
||||||
ReadBookConfig.bgMeanColor
|
|
||||||
} else {
|
|
||||||
ReadBookConfig.bgMeanColor
|
|
||||||
}
|
|
||||||
setLightStatusBar(ColorUtils.isColorLight(statusBarColor))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
private fun upSystemUiVisibilityO(
|
|
||||||
isInMultiWindow: Boolean,
|
|
||||||
toolBarHide: Boolean = true
|
|
||||||
) {
|
|
||||||
var flag = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
|
|
||||||
or View.SYSTEM_UI_FLAG_IMMERSIVE
|
|
||||||
or View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY)
|
|
||||||
if (!isInMultiWindow) {
|
|
||||||
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
|
|
||||||
}
|
|
||||||
if (ReadBookConfig.hideNavigationBar) {
|
|
||||||
flag = flag or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
|
|
||||||
if (toolBarHide) {
|
|
||||||
flag = flag or View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ReadBookConfig.hideStatusBar && toolBarHide) {
|
|
||||||
flag = flag or View.SYSTEM_UI_FLAG_FULLSCREEN
|
|
||||||
}
|
|
||||||
window.decorView.systemUiVisibility = flag
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upNavigationBarColor() {
|
|
||||||
upNavigationBar()
|
|
||||||
val navColor = when {
|
|
||||||
binding.readMenu.isVisible -> themeColor(com.google.android.material.R.attr.colorSurfaceContainer)
|
|
||||||
binding.searchMenu.bottomMenuVisible -> themeColor(com.google.android.material.R.attr.colorSurface)
|
|
||||||
bottomDialog > 0 -> themeColor(com.google.android.material.R.attr.colorSurface)
|
|
||||||
else -> ReadBookConfig.bgMeanColor
|
|
||||||
}
|
|
||||||
window.setNavigationBarColorAuto(navColor)
|
|
||||||
binding.navigationBar.setBackgroundColor(navColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
private fun upNavigationBar() {
|
|
||||||
binding.navigationBar.gone(!menuLayoutIsVisible)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 保持亮屏
|
|
||||||
*/
|
|
||||||
fun keepScreenOn(on: Boolean) {
|
|
||||||
val isScreenOn =
|
|
||||||
(window.attributes.flags and WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) != 0
|
|
||||||
if (on == isScreenOn) return
|
|
||||||
if (on) {
|
|
||||||
window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
} else {
|
|
||||||
window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 适配刘海
|
|
||||||
*/
|
|
||||||
private fun upLayoutInDisplayCutoutMode() {
|
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
|
||||||
window.attributes = window.attributes.apply {
|
|
||||||
layoutInDisplayCutoutMode = if (ReadBookConfig.readBodyToLh) {
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES
|
|
||||||
} else {
|
|
||||||
WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_NEVER
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams", "SetTextI18n")
|
|
||||||
fun showDownloadDialog() {
|
|
||||||
ReadBook.book?.let { book ->
|
|
||||||
alert(titleResource = R.string.offline_cache) {
|
|
||||||
val alertBinding = DialogDownloadChoiceBinding.inflate(layoutInflater).apply {
|
|
||||||
editStart.setText((book.durChapterIndex + 1).toString())
|
|
||||||
editEnd.setText(book.totalChapterNum.toString())
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.run {
|
|
||||||
val start = editStart.text!!.toString().let {
|
|
||||||
if (it.isEmpty()) 0 else it.toInt()
|
|
||||||
}
|
|
||||||
val end = editEnd.text!!.toString().let {
|
|
||||||
if (it.isEmpty()) book.totalChapterNum else it.toInt()
|
|
||||||
}
|
|
||||||
lifecycleScope.launch {
|
|
||||||
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showSimulatedReading() {
|
|
||||||
val book = ReadBook.book ?: return
|
|
||||||
val dateFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
|
|
||||||
|
|
||||||
val alertBinding = DialogSimulatedReadingBinding.inflate(layoutInflater).apply {
|
|
||||||
srEnabled.isChecked = book.getReadSimulating()
|
|
||||||
editStart.setText(book.getStartChapter().toString())
|
|
||||||
editNum.setText(book.getDailyChapters().toString())
|
|
||||||
|
|
||||||
// 安全地设置初始日期
|
|
||||||
val safeDate = book.getStartDate() ?: LocalDate.now()
|
|
||||||
startDate.setText(safeDate.format(dateFormatter))
|
|
||||||
|
|
||||||
// 让 EditText 不可直接编辑,只能通过选择器
|
|
||||||
startDate.isFocusable = false
|
|
||||||
startDate.isCursorVisible = false
|
|
||||||
|
|
||||||
startDate.setOnClickListener {
|
|
||||||
val currentDate = try {
|
|
||||||
LocalDate.parse(startDate.text.toString(), dateFormatter)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LocalDate.now()
|
|
||||||
}
|
|
||||||
val initialSelection = currentDate
|
|
||||||
.atStartOfDay(ZoneId.systemDefault())
|
|
||||||
.toInstant()
|
|
||||||
.toEpochMilli()
|
|
||||||
val picker = MaterialDatePicker.Builder.datePicker()
|
|
||||||
.setTitleText("选择开始日期")
|
|
||||||
.setSelection(initialSelection)
|
|
||||||
.build()
|
|
||||||
picker.addOnPositiveButtonClickListener { selection ->
|
|
||||||
val date = Instant.ofEpochMilli(selection)
|
|
||||||
.atZone(ZoneId.systemDefault())
|
|
||||||
.toLocalDate()
|
|
||||||
startDate.setText(date.format(dateFormatter))
|
|
||||||
}
|
|
||||||
picker.show((root.context as AppCompatActivity).supportFragmentManager, "md3_date_picker")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
alert(titleResource = R.string.simulated_reading) {
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.run {
|
|
||||||
val start = editStart.text.toString().toIntOrNull() ?: 0
|
|
||||||
val num = editNum.text.toString().toIntOrNull() ?: book.totalChapterNum
|
|
||||||
val enabled = srEnabled.isChecked
|
|
||||||
|
|
||||||
val date = try {
|
|
||||||
LocalDate.parse(startDate.text.toString(), dateFormatter)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
LocalDate.now()
|
|
||||||
}
|
|
||||||
|
|
||||||
book.setStartDate(date)
|
|
||||||
book.setDailyChapters(num)
|
|
||||||
book.setStartChapter(start)
|
|
||||||
book.setReadSimulating(enabled)
|
|
||||||
book.save()
|
|
||||||
ReadBook.clearTextChapter()
|
|
||||||
viewModel.initData(intent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showCharsetConfig() {
|
|
||||||
alert(R.string.set_charset) {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = "charset"
|
|
||||||
editView.setFilterValues(charsets)
|
|
||||||
editView.setText(ReadBook.book?.charset)
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let {
|
|
||||||
ReadBook.setCharset(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun showPageAnimConfig(success: () -> Unit) {
|
|
||||||
val items = arrayListOf<String>()
|
|
||||||
items.add(getString(R.string.btn_default_s))
|
|
||||||
items.add(getString(R.string.page_anim_cover))
|
|
||||||
items.add(getString(R.string.page_anim_slide))
|
|
||||||
items.add(getString(R.string.page_anim_simulation))
|
|
||||||
items.add(getString(R.string.page_anim_scroll))
|
|
||||||
items.add(getString(R.string.page_anim_none))
|
|
||||||
selector(R.string.page_anim, items) { _, i ->
|
|
||||||
ReadBook.book?.setPageAnim(i - 1)
|
|
||||||
success()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isPrevKey(keyCode: Int): Boolean {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val prevKeysStr = getPrefString(PreferKey.prevKeys)
|
|
||||||
return prevKeysStr?.split(",")?.contains(keyCode.toString()) ?: false
|
|
||||||
}
|
|
||||||
|
|
||||||
fun isNextKey(keyCode: Int): Boolean {
|
|
||||||
if (keyCode == KeyEvent.KEYCODE_UNKNOWN) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
val nextKeysStr = getPrefString(PreferKey.nextKeys)
|
|
||||||
return nextKeysStr?.split(",")?.contains(keyCode.toString()) ?: false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.MutableLiveData
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.base.BaseViewModel
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.BookChapter
|
|
||||||
import io.legado.app.databinding.DialogContentEditBinding
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
|
||||||
import io.legado.app.help.book.BookHelp
|
|
||||||
import io.legado.app.help.book.ContentProcessor
|
|
||||||
import io.legado.app.help.book.isLocal
|
|
||||||
import io.legado.app.help.book.isLocalTxt
|
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.model.webBook.WebBook
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.sendToClip
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import kotlinx.coroutines.withContext
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 内容编辑
|
|
||||||
*/
|
|
||||||
class ContentEditDialog : BaseBottomSheetDialogFragment(R.layout.dialog_content_edit) {
|
|
||||||
|
|
||||||
val binding by viewBinding(DialogContentEditBinding::bind)
|
|
||||||
val viewModel by viewModels<ContentEditViewModel>()
|
|
||||||
|
|
||||||
private val targetOffset: Int
|
|
||||||
get() = arguments?.getInt("start_position", -1).takeIf { it != -1 }
|
|
||||||
?: ReadBook.durChapterPos
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.toolBar.title = ReadBook.curTextChapter?.title
|
|
||||||
val book = ReadBook.book
|
|
||||||
if (book?.isLocalTxt == true) {
|
|
||||||
binding.cbSaveToSource.visible()
|
|
||||||
}
|
|
||||||
initMenu()
|
|
||||||
binding.toolBar.setOnClickListener {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val book1 = ReadBook.book ?: return@launch
|
|
||||||
val chapter = withContext(IO) {
|
|
||||||
appDb.bookChapterDao.getChapter(book1.bookUrl, ReadBook.durChapterIndex)
|
|
||||||
} ?: return@launch
|
|
||||||
editTitle(chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.loadStateLiveData.observe(viewLifecycleOwner) {
|
|
||||||
if (it) {
|
|
||||||
binding.rlLoading.visible()
|
|
||||||
} else {
|
|
||||||
binding.rlLoading.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.initContent {
|
|
||||||
binding.contentView.setText(it)
|
|
||||||
binding.contentView.post {
|
|
||||||
val layout = binding.contentView.layout ?: return@post
|
|
||||||
val targetY = binding.contentView.top +
|
|
||||||
layout.getLineTop(
|
|
||||||
layout.getLineForOffset(targetOffset)
|
|
||||||
)
|
|
||||||
|
|
||||||
binding.scrollView.smoothScrollTo(0, targetY)
|
|
||||||
//highlightSelectedTextTwice()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initMenu() {
|
|
||||||
binding.toolBar.inflateMenu(R.menu.content_edit)
|
|
||||||
//binding.toolBar.menu.applyTint(requireContext())
|
|
||||||
binding.toolBar.setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.menu_save -> {
|
|
||||||
save()
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
R.id.menu_reset -> viewModel.initContent(true) { content ->
|
|
||||||
binding.contentView.setText(content)
|
|
||||||
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
|
||||||
}
|
|
||||||
R.id.menu_copy_all -> requireContext()
|
|
||||||
.sendToClip("${binding.toolBar.title}\n${binding.contentView.text}")
|
|
||||||
}
|
|
||||||
return@setOnMenuItemClickListener true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun editTitle(chapter: BookChapter) {
|
|
||||||
alert {
|
|
||||||
setTitle(R.string.edit)
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
|
|
||||||
alertBinding.editView.setText(chapter.title)
|
|
||||||
setCustomView(alertBinding.root)
|
|
||||||
okButton {
|
|
||||||
chapter.title = alertBinding.editView.text.toString()
|
|
||||||
lifecycleScope.launch {
|
|
||||||
withContext(IO) {
|
|
||||||
appDb.bookChapterDao.update(chapter)
|
|
||||||
}
|
|
||||||
binding.toolBar.title = chapter.getDisplayTitle()
|
|
||||||
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCancel(dialog: DialogInterface) {
|
|
||||||
super.onCancel(dialog)
|
|
||||||
save()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun save() {
|
|
||||||
val content = binding.contentView.text?.toString() ?: return
|
|
||||||
val saveToSource = binding.cbSaveToSource.isChecked
|
|
||||||
Coroutine.async {
|
|
||||||
val book = ReadBook.book ?: return@async
|
|
||||||
val chapter = appDb.bookChapterDao
|
|
||||||
.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
|
||||||
?: return@async
|
|
||||||
BookHelp.saveText(book, chapter, content, saveToSource)
|
|
||||||
ReadBook.loadContent(ReadBook.durChapterIndex, resetPageOffset = false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class ContentEditViewModel(application: Application) : BaseViewModel(application) {
|
|
||||||
val loadStateLiveData = MutableLiveData<Boolean>()
|
|
||||||
var content: String? = null
|
|
||||||
|
|
||||||
fun initContent(reset: Boolean = false, success: (String) -> Unit) {
|
|
||||||
execute {
|
|
||||||
val book = ReadBook.book ?: return@execute null
|
|
||||||
val chapter = appDb.bookChapterDao
|
|
||||||
.getChapter(book.bookUrl, ReadBook.durChapterIndex)
|
|
||||||
?: return@execute null
|
|
||||||
if (reset) {
|
|
||||||
content = null
|
|
||||||
BookHelp.delContent(book, chapter)
|
|
||||||
if (!book.isLocal) ReadBook.bookSource?.let { bookSource ->
|
|
||||||
WebBook.getContentAwait(bookSource, book, chapter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@execute content ?: let {
|
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
|
||||||
val content = BookHelp.getContent(book, chapter) ?: return@let null
|
|
||||||
contentProcessor.getContent(book, chapter, content, includeTitle = false)
|
|
||||||
.toString()
|
|
||||||
}
|
|
||||||
}.onStart {
|
|
||||||
loadStateLiveData.postValue(true)
|
|
||||||
}.onSuccess {
|
|
||||||
content = it
|
|
||||||
success.invoke(it ?: "")
|
|
||||||
}.onFinally {
|
|
||||||
loadStateLiveData.postValue(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.activity.result.contract.ActivityResultContracts
|
|
||||||
import androidx.appcompat.app.AppCompatActivity
|
|
||||||
import androidx.fragment.app.activityViewModels
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseDialogFragment
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
|
||||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
|
||||||
import io.legado.app.databinding.Item1lineTextBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.replace.ReplaceEditRoute
|
|
||||||
import io.legado.app.ui.replace.ReplaceRuleActivity
|
|
||||||
import io.legado.app.utils.setLayout
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 起效的替换规则
|
|
||||||
*/
|
|
||||||
class EffectiveReplacesDialog : BaseDialogFragment(R.layout.dialog_recycler_view) {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
|
||||||
private val viewModel by activityViewModels<ReadBookViewModel>()
|
|
||||||
private val adapter by lazy { ReplaceAdapter(requireContext()) }
|
|
||||||
private val chineseConvert by lazy { ReplaceRule(0, "繁简转换") }
|
|
||||||
|
|
||||||
private var isEdit = false
|
|
||||||
|
|
||||||
private val editActivity =
|
|
||||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) {
|
|
||||||
if (it.resultCode == AppCompatActivity.RESULT_OK) {
|
|
||||||
isEdit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.run {
|
|
||||||
//toolBar.setBackgroundColor(primaryColor)
|
|
||||||
toolBar.setTitle(R.string.effective_replaces)
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
}
|
|
||||||
val effectiveReplaceRules = ReadBook.curTextChapter?.effectiveReplaceRules ?: emptyList()
|
|
||||||
if (AppConfig.chineseConverterType > 0) {
|
|
||||||
adapter.setItems(effectiveReplaceRules + chineseConvert)
|
|
||||||
} else {
|
|
||||||
adapter.setItems(effectiveReplaceRules)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
if (isEdit) {
|
|
||||||
viewModel.replaceRuleChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showChineseConvertAlert() {
|
|
||||||
alert(titleResource = R.string.chinese_converter) {
|
|
||||||
items(resources.getStringArray(R.array.chinese_mode).toList()) { _, i ->
|
|
||||||
if (AppConfig.chineseConverterType != i) {
|
|
||||||
AppConfig.chineseConverterType = i
|
|
||||||
isEdit = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private inner class ReplaceAdapter(context: Context) :
|
|
||||||
RecyclerAdapter<ReplaceRule, Item1lineTextBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): Item1lineTextBinding {
|
|
||||||
return Item1lineTextBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: Item1lineTextBinding) {
|
|
||||||
binding.root.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let { item ->
|
|
||||||
if (item == chineseConvert) {
|
|
||||||
showChineseConvertAlert()
|
|
||||||
return@let
|
|
||||||
}
|
|
||||||
val editRoute = ReplaceEditRoute(id = item.id, pattern = item.pattern)
|
|
||||||
ReplaceRuleActivity.startIntent(requireContext(), editRoute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: Item1lineTextBinding,
|
|
||||||
item: ReplaceRule,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.textView.text = item.name
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -20,13 +20,11 @@ import io.legado.app.lib.dialogs.alert
|
|||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.model.ReadManga
|
import io.legado.app.model.ReadManga
|
||||||
import io.legado.app.ui.browser.WebViewActivity
|
import io.legado.app.ui.browser.WebViewActivity
|
||||||
import io.legado.app.utils.ConstraintModify
|
|
||||||
import io.legado.app.utils.activity
|
import io.legado.app.utils.activity
|
||||||
import io.legado.app.utils.applyNavigationBarPadding
|
import io.legado.app.utils.applyNavigationBarPadding
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
import io.legado.app.utils.invisible
|
import io.legado.app.utils.invisible
|
||||||
import io.legado.app.utils.loadAnimation
|
import io.legado.app.utils.loadAnimation
|
||||||
import io.legado.app.utils.modifyBegin
|
|
||||||
import io.legado.app.utils.openUrl
|
import io.legado.app.utils.openUrl
|
||||||
import io.legado.app.utils.startActivity
|
import io.legado.app.utils.startActivity
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
@@ -125,27 +123,12 @@ class MangaMenu @JvmOverloads constructor(
|
|||||||
} else {
|
} else {
|
||||||
titleBarAddition.gone()
|
titleBarAddition.gone()
|
||||||
}
|
}
|
||||||
upBrightnessVwPos()
|
|
||||||
/**
|
/**
|
||||||
* 确保视图不被导航栏遮挡
|
* 确保视图不被导航栏遮挡
|
||||||
*/
|
*/
|
||||||
bottomView.applyNavigationBarPadding()
|
bottomView.applyNavigationBarPadding()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upBrightnessVwPos() {
|
|
||||||
if (AppConfig.brightnessVwPos) {
|
|
||||||
binding.root.modifyBegin()
|
|
||||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.LEFT)
|
|
||||||
.rightToRightOf(R.id.ll_brightness, R.id.vw_menu_root)
|
|
||||||
.commit()
|
|
||||||
} else {
|
|
||||||
binding.root.modifyBegin()
|
|
||||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.RIGHT)
|
|
||||||
.leftToLeftOf(R.id.ll_brightness, R.id.vw_menu_root)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAnimation() {
|
private fun initAnimation() {
|
||||||
menuTopIn.setAnimationListener(menuInListener)
|
menuTopIn.setAnimationListener(menuInListener)
|
||||||
menuTopOut.setAnimationListener(menuOutListener)
|
menuTopOut.setAnimationListener(menuOutListener)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,17 @@
|
|||||||
|
package io.legado.app.ui.book.read
|
||||||
|
|
||||||
|
object ReadBookColorPickerIds {
|
||||||
|
const val SHADOW_COLOR = 123
|
||||||
|
const val TEXT_COLOR = 121
|
||||||
|
const val TEXT_ACCENT_COLOR = 125
|
||||||
|
const val BG_COLOR = 122
|
||||||
|
const val HIGHLIGHT_RULE_COLOR = 7900
|
||||||
|
const val TITLE_COLOR = 7896
|
||||||
|
const val TIP_HEADER_COLOR = 7897
|
||||||
|
const val TIP_DIVIDER_COLOR = 7898
|
||||||
|
const val TIP_FOOTER_COLOR = 7899
|
||||||
|
const val MENU_BG_COLOR = 114
|
||||||
|
const val MENU_ACCENT_COLOR = 514
|
||||||
|
const val UNDERLINE_COLOR = 810
|
||||||
|
var pendingHighlightRulePosition = -1
|
||||||
|
}
|
||||||
@@ -0,0 +1,298 @@
|
|||||||
|
package io.legado.app.ui.book.read
|
||||||
|
|
||||||
|
import android.graphics.drawable.ColorDrawable
|
||||||
|
import androidx.compose.material3.ColorScheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.luminance
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import io.legado.app.data.repository.ReadPreferences
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import io.legado.app.help.config.ReadStyleResolver
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.ProvideThemeOverride
|
||||||
|
import io.legado.app.ui.theme.ThemeOverrideState
|
||||||
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.theme.buildThemeOverrideState
|
||||||
|
import io.legado.app.ui.theme.extractSeedColor
|
||||||
|
import io.legado.app.ui.theme.toSafeBitmap
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun ReadBookColorTheme(
|
||||||
|
styleConfig: ReadBookStyleConfig,
|
||||||
|
preferences: ReadPreferences,
|
||||||
|
content: @Composable () -> Unit,
|
||||||
|
) {
|
||||||
|
ProvideThemeOverride(
|
||||||
|
theme = rememberReadBookColorTheme(
|
||||||
|
styleConfig = styleConfig,
|
||||||
|
preferences = preferences,
|
||||||
|
),
|
||||||
|
content = content
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberReadBookColorTheme(
|
||||||
|
styleConfig: ReadBookStyleConfig,
|
||||||
|
preferences: ReadPreferences,
|
||||||
|
): ThemeOverrideState? {
|
||||||
|
val isAppDark = LegadoTheme.isDark
|
||||||
|
return when (preferences.readBarStyle) {
|
||||||
|
1 -> rememberReadBackgroundTheme(styleConfig, isAppDark)
|
||||||
|
2 -> rememberCustomReadMenuTheme(
|
||||||
|
styleConfig = styleConfig,
|
||||||
|
preferences = preferences,
|
||||||
|
isAppDark = isAppDark,
|
||||||
|
)
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberReadBackgroundTheme(
|
||||||
|
styleConfig: ReadBookStyleConfig,
|
||||||
|
isAppDark: Boolean,
|
||||||
|
): ThemeOverrideState? {
|
||||||
|
val background = remember(styleConfig, isAppDark) {
|
||||||
|
runCatching { ReadStyleResolver.currentBackground(ReadBookConfig.durConfig) }.getOrNull()
|
||||||
|
} ?: return null
|
||||||
|
var seedColor by remember(background) { mutableStateOf<Color?>(null) }
|
||||||
|
|
||||||
|
LaunchedEffect(background, styleConfig, isAppDark) {
|
||||||
|
seedColor = when (background.type) {
|
||||||
|
0 -> background.value.toColorOrNull()
|
||||||
|
else -> extractCurrentReadBackgroundSeed()
|
||||||
|
?: ReadBookConfig.bgMeanColor.takeIf { it != 0 }?.let(::Color)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceColor = seedColor ?: return null
|
||||||
|
val surfaceColor = when (background.type) {
|
||||||
|
0 -> background.value.toColorOrNull()
|
||||||
|
else -> ReadBookConfig.bgMeanColor.takeIf { it != 0 }?.let(::Color)
|
||||||
|
}
|
||||||
|
return rememberReadThemeOverride(
|
||||||
|
seedColor = sourceColor,
|
||||||
|
backgroundColor = surfaceColor,
|
||||||
|
containerColor = null,
|
||||||
|
fallbackDark = isAppDark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberCustomReadMenuTheme(
|
||||||
|
styleConfig: ReadBookStyleConfig,
|
||||||
|
preferences: ReadPreferences,
|
||||||
|
isAppDark: Boolean,
|
||||||
|
): ThemeOverrideState {
|
||||||
|
val menuBackgroundColor = remember(
|
||||||
|
styleConfig,
|
||||||
|
preferences.readMenuBgColor,
|
||||||
|
preferences.readMenuBgColorNight,
|
||||||
|
isAppDark,
|
||||||
|
) {
|
||||||
|
Color(preferences.readMenuBackgroundColor(isAppDark))
|
||||||
|
}
|
||||||
|
val accentColor = remember(
|
||||||
|
styleConfig,
|
||||||
|
preferences.readMenuAccentColor,
|
||||||
|
preferences.readMenuAccentColorNight,
|
||||||
|
isAppDark,
|
||||||
|
) {
|
||||||
|
Color(preferences.readMenuAccentColor(isAppDark))
|
||||||
|
}
|
||||||
|
val menuContainerColor = remember(
|
||||||
|
styleConfig,
|
||||||
|
preferences.readMenuContainerColor,
|
||||||
|
preferences.readMenuContainerColorNight,
|
||||||
|
preferences.readMenuBgColor,
|
||||||
|
preferences.readMenuBgColorNight,
|
||||||
|
isAppDark,
|
||||||
|
) {
|
||||||
|
Color(preferences.readMenuContainerColor(isAppDark))
|
||||||
|
}
|
||||||
|
val useSeedOnly = preferences.readMenuColorMode == 0
|
||||||
|
if (!useSeedOnly) {
|
||||||
|
return rememberCustomReadMenuThemeOverride(
|
||||||
|
accentColor = accentColor,
|
||||||
|
menuBackgroundColor = menuBackgroundColor,
|
||||||
|
menuContainerColor = menuContainerColor,
|
||||||
|
isDark = isAppDark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return rememberReadThemeOverride(
|
||||||
|
seedColor = accentColor,
|
||||||
|
backgroundColor = null,
|
||||||
|
containerColor = null,
|
||||||
|
fallbackDark = isAppDark,
|
||||||
|
deriveDarkFromColor = false,
|
||||||
|
) ?: buildReadThemeOverride(
|
||||||
|
seedColor = accentColor,
|
||||||
|
backgroundColor = null,
|
||||||
|
containerColor = null,
|
||||||
|
isDark = isAppDark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberCustomReadMenuThemeOverride(
|
||||||
|
accentColor: Color,
|
||||||
|
menuBackgroundColor: Color,
|
||||||
|
menuContainerColor: Color,
|
||||||
|
isDark: Boolean,
|
||||||
|
): ThemeOverrideState {
|
||||||
|
return remember(accentColor, menuBackgroundColor, menuContainerColor, isDark) {
|
||||||
|
buildReadThemeOverride(
|
||||||
|
seedColor = accentColor,
|
||||||
|
backgroundColor = null,
|
||||||
|
containerColor = null,
|
||||||
|
isDark = isDark,
|
||||||
|
).let { base ->
|
||||||
|
base.copy(
|
||||||
|
colorScheme = base.colorScheme.withCustomReadMenuColors(
|
||||||
|
accentColor = accentColor,
|
||||||
|
menuBackgroundColor = menuBackgroundColor,
|
||||||
|
menuContainerColor = menuContainerColor,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun rememberReadThemeOverride(
|
||||||
|
seedColor: Color,
|
||||||
|
backgroundColor: Color?,
|
||||||
|
containerColor: Color?,
|
||||||
|
fallbackDark: Boolean = LegadoTheme.isDark,
|
||||||
|
deriveDarkFromColor: Boolean = true,
|
||||||
|
): ThemeOverrideState? {
|
||||||
|
val isDark = remember(backgroundColor, containerColor, fallbackDark, deriveDarkFromColor) {
|
||||||
|
if (deriveDarkFromColor) {
|
||||||
|
(containerColor ?: backgroundColor)?.let { it.luminance() < 0.5f } ?: fallbackDark
|
||||||
|
} else {
|
||||||
|
fallbackDark
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return remember(seedColor, backgroundColor, containerColor, isDark) {
|
||||||
|
buildReadThemeOverride(
|
||||||
|
seedColor = seedColor,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
containerColor = containerColor,
|
||||||
|
isDark = isDark,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildReadThemeOverride(
|
||||||
|
seedColor: Color,
|
||||||
|
backgroundColor: Color?,
|
||||||
|
containerColor: Color?,
|
||||||
|
isDark: Boolean,
|
||||||
|
): ThemeOverrideState {
|
||||||
|
val colorSpec = ThemeResolver.resolveColorSpecFromMaterialVersion(ThemeConfig.materialVersion)
|
||||||
|
val base = buildThemeOverrideState(
|
||||||
|
seedColor = seedColor,
|
||||||
|
isDark = isDark,
|
||||||
|
paletteStyle = ThemeResolver.resolvePaletteStyle(ThemeConfig.paletteStyle),
|
||||||
|
colorSpec = colorSpec,
|
||||||
|
usePureBlack = false,
|
||||||
|
)
|
||||||
|
return base.copy(
|
||||||
|
colorScheme = base.colorScheme.withReadSurfaceColors(
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
containerColor = containerColor
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ColorScheme.withReadSurfaceColors(
|
||||||
|
backgroundColor: Color?,
|
||||||
|
containerColor: Color?,
|
||||||
|
): ColorScheme {
|
||||||
|
val resolvedBackground = backgroundColor ?: background
|
||||||
|
val resolvedContainer = containerColor ?: surfaceContainer
|
||||||
|
return copy(
|
||||||
|
background = resolvedBackground,
|
||||||
|
surface = resolvedBackground,
|
||||||
|
surfaceDim = resolvedBackground,
|
||||||
|
surfaceBright = resolvedBackground,
|
||||||
|
surfaceContainerLowest = resolvedBackground,
|
||||||
|
surfaceContainer = resolvedContainer,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ColorScheme.withCustomReadMenuColors(
|
||||||
|
accentColor: Color,
|
||||||
|
menuBackgroundColor: Color,
|
||||||
|
menuContainerColor: Color,
|
||||||
|
): ColorScheme {
|
||||||
|
return copy(
|
||||||
|
primary = accentColor,
|
||||||
|
onPrimary = accentColor.contrastContentColor(),
|
||||||
|
surfaceTint = accentColor,
|
||||||
|
surfaceContainerHigh = menuBackgroundColor,
|
||||||
|
surfaceContainerLow = menuContainerColor,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Color.contrastContentColor(): Color {
|
||||||
|
return if (luminance() > 0.5f) Color.Black else Color.White
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReadPreferences.readMenuBackgroundColor(isDark: Boolean): Int {
|
||||||
|
return if (isDark) {
|
||||||
|
readMenuBgColorNight.takeIf { it != 0 } ?: ReadBookConfig.durConfig.menuBgColor(isNight = true)
|
||||||
|
} else {
|
||||||
|
readMenuBgColor.takeIf { it != 0 } ?: ReadBookConfig.durConfig.menuBgColor(isNight = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReadPreferences.readMenuAccentColor(isDark: Boolean): Int {
|
||||||
|
return if (isDark) {
|
||||||
|
readMenuAccentColorNight.takeIf { it != 0 }
|
||||||
|
?: ReadBookConfig.durConfig.menuAccentColor(isNight = true)
|
||||||
|
} else {
|
||||||
|
readMenuAccentColor.takeIf { it != 0 }
|
||||||
|
?: ReadBookConfig.durConfig.menuAccentColor(isNight = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun ReadPreferences.readMenuContainerColor(isDark: Boolean): Int {
|
||||||
|
return if (isDark) {
|
||||||
|
readMenuContainerColorNight.takeIf { it != 0 } ?: readMenuBackgroundColor(isDark = true)
|
||||||
|
} else {
|
||||||
|
readMenuContainerColor.takeIf { it != 0 } ?: readMenuBackgroundColor(isDark = false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun extractCurrentReadBackgroundSeed(): Color? {
|
||||||
|
return withContext(Dispatchers.Default) {
|
||||||
|
runCatching {
|
||||||
|
val drawable = ReadStyleResolver.currentBackgroundDrawable(
|
||||||
|
config = ReadBookConfig.durConfig,
|
||||||
|
width = 128,
|
||||||
|
height = 128
|
||||||
|
)
|
||||||
|
if (drawable is ColorDrawable) {
|
||||||
|
Color(drawable.color)
|
||||||
|
} else {
|
||||||
|
Color(drawable.toSafeBitmap(128).extractSeedColor())
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun String.toColorOrNull(): Color? {
|
||||||
|
return runCatching { Color(toColorInt()) }.getOrNull()
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,473 @@
|
|||||||
|
package io.legado.app.ui.book.read
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.view.KeyEvent
|
||||||
|
import android.view.View
|
||||||
|
import android.widget.FrameLayout
|
||||||
|
import android.widget.ImageView
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.key
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.ReadMenuBlurMode
|
||||||
|
import io.legado.app.help.IntentData
|
||||||
|
import io.legado.app.help.IntentHelp
|
||||||
|
import io.legado.app.model.ReadBook
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import io.legado.app.ui.browser.WebViewActivity
|
||||||
|
import io.legado.app.ui.book.info.BookInfoActivity
|
||||||
|
import io.legado.app.ui.book.read.page.ContentTextView
|
||||||
|
import io.legado.app.ui.book.read.page.ReadView
|
||||||
|
import io.legado.app.ui.book.read.page.entities.PageDirection
|
||||||
|
import io.legado.app.ui.book.searchContent.SearchContentActivity
|
||||||
|
import io.legado.app.ui.book.searchContent.SearchResult
|
||||||
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
|
import io.legado.app.ui.book.toc.TocActivityResult
|
||||||
|
import io.legado.app.ui.book.toc.rule.TxtTocRuleActivity
|
||||||
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
|
import io.legado.app.ui.replace.ReplaceEditRoute
|
||||||
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
||||||
|
import io.legado.app.utils.StartActivityContract
|
||||||
|
import io.legado.app.utils.takePersistablePermissionSafely
|
||||||
|
import kotlinx.coroutines.coroutineScope
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.yield
|
||||||
|
|
||||||
|
|
||||||
|
data class ReadBookViewRefs(
|
||||||
|
val root: FrameLayout,
|
||||||
|
val readView: ReadView,
|
||||||
|
val textMenuPosition: View,
|
||||||
|
val cursorLeft: ImageView,
|
||||||
|
val cursorRight: ImageView,
|
||||||
|
val navigationBar: View,
|
||||||
|
)
|
||||||
|
|
||||||
|
interface ReadBookRouteHost :
|
||||||
|
View.OnTouchListener,
|
||||||
|
ReadView.CallBack,
|
||||||
|
ContentTextView.CallBack {
|
||||||
|
|
||||||
|
val isInMultiWindowModeCompat: Boolean
|
||||||
|
|
||||||
|
fun closeReadBook()
|
||||||
|
|
||||||
|
fun upSystemUiVisibility(
|
||||||
|
isInMultiWindow: Boolean,
|
||||||
|
toolBarHide: Boolean,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Narrow interface for hardware input delegation from Activity.
|
||||||
|
* MainActivity holds this instead of the full bridge/controller.
|
||||||
|
*/
|
||||||
|
interface ReadBookInputHandler {
|
||||||
|
fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean
|
||||||
|
fun onKeyUp(keyCode: Int, event: KeyEvent): Boolean
|
||||||
|
fun mouseWheelPage(direction: PageDirection)
|
||||||
|
fun handleKeyPage(direction: PageDirection, longPress: Boolean = false)
|
||||||
|
fun toggleMenu()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outer wrapper for ReadBookScreen — handles system UI state sync
|
||||||
|
* and ActivityResult launcher registration.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ReadBookRouteScreen(
|
||||||
|
viewModel: ReadBookViewModel,
|
||||||
|
host: ReadBookRouteHost,
|
||||||
|
controller: ReadBookController,
|
||||||
|
onEffectsReady: () -> Unit = {},
|
||||||
|
) {
|
||||||
|
val state by viewModel.uiState.collectAsStateWithLifecycle()
|
||||||
|
val readPreferences by viewModel.readPreferences.collectAsStateWithLifecycle()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val menuBackdrop = rememberLayerBackdrop()
|
||||||
|
val menuHazeState = remember { HazeState() }
|
||||||
|
val useMenuHazeSource = state.menuConfig.readMenuTopBarBlurMode == ReadMenuBlurMode.Haze ||
|
||||||
|
state.menuConfig.readMenuBottomBarBlurMode == ReadMenuBlurMode.Haze ||
|
||||||
|
(
|
||||||
|
!state.menuConfig.readMenuFloatingBottomBar &&
|
||||||
|
state.menuConfig.readMenuBottomBarBlurMode == ReadMenuBlurMode.LiquidGlass
|
||||||
|
)
|
||||||
|
|
||||||
|
// ── ActivityResult Launchers ──────────────────────────────────────
|
||||||
|
|
||||||
|
val tocLauncher = rememberLauncherForActivityResult(TocActivityResult()) { result ->
|
||||||
|
result?.let { (index, chapterPos, _) ->
|
||||||
|
viewModel.onIntent(ReadBookIntent.OpenChapterResult(index, chapterPos))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceEditLauncher = rememberLauncherForActivityResult(
|
||||||
|
StartActivityContract(BookSourceEditActivity::class.java)
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||||
|
viewModel.onIntent(ReadBookIntent.SourceEditResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val replaceLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||||
|
viewModel.onIntent(ReadBookIntent.ReplaceRuleResult)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontFolderPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
it.takePersistablePermissionSafely(context)
|
||||||
|
viewModel.onIntent(ReadBookIntent.FontFolderSelected(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val booksDirPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
it.takePersistablePermissionSafely(context)
|
||||||
|
viewModel.onIntent(ReadBookIntent.BooksDirSelected(it))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val readStyleImagePicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleImageSelected(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var pendingReadStyleImageIsNight by remember { mutableStateOf(false) }
|
||||||
|
val readStyleImagePickerForMode = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
viewModel.onIntent(ReadBookIntent.ReadStyleImageSelectedForMode(it, pendingReadStyleImageIsNight))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val readStyleImportPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleConfigImportSelected(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
val readStyleExportPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) { uri ->
|
||||||
|
uri?.let { viewModel.onIntent(ReadBookIntent.ReadStyleConfigExportSelected(it)) }
|
||||||
|
}
|
||||||
|
|
||||||
|
var pendingMenuCustomIconId by remember { mutableStateOf<String?>(null) }
|
||||||
|
val menuCustomIconPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
val id = pendingMenuCustomIconId
|
||||||
|
pendingMenuCustomIconId = null
|
||||||
|
if (id != null && uri != null) {
|
||||||
|
viewModel.onIntent(ReadBookIntent.SaveMenuCustomIcon(id, uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pendingTitleBarCustomIconId by remember { mutableStateOf<String?>(null) }
|
||||||
|
val titleBarCustomIconPicker = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
val id = pendingTitleBarCustomIconId
|
||||||
|
pendingTitleBarCustomIconId = null
|
||||||
|
if (id != null && uri != null) {
|
||||||
|
viewModel.onIntent(ReadBookIntent.SaveTitleBarCustomIcon(id, uri))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val txtTocRuleLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.StartActivityForResult()
|
||||||
|
) { result ->
|
||||||
|
if (result.resultCode == android.app.Activity.RESULT_OK) {
|
||||||
|
result.data?.getStringExtra("tocRegex")?.let { rule ->
|
||||||
|
viewModel.onIntent(ReadBookIntent.TocRegexResult(rule))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val searchContentLauncher = rememberLauncherForActivityResult(
|
||||||
|
StartActivityContract(SearchContentActivity::class.java)
|
||||||
|
) { result ->
|
||||||
|
val data = result.data ?: return@rememberLauncherForActivityResult
|
||||||
|
val key = data.getLongExtra("key", System.currentTimeMillis())
|
||||||
|
val index = data.getIntExtra("index", 0)
|
||||||
|
val searchResult = IntentData.get<SearchResult>("searchResult$key")
|
||||||
|
val searchResultList = IntentData.get<List<SearchResult>>("searchResultList$key")
|
||||||
|
if (searchResult != null && searchResultList != null) {
|
||||||
|
viewModel.onIntent(ReadBookIntent.SetSearchResults(searchResultList, index, searchResult.query))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val bookInfoLauncher = rememberLauncherForActivityResult(
|
||||||
|
StartActivityContract(BookInfoActivity::class.java)
|
||||||
|
) { result ->
|
||||||
|
viewModel.onIntent(ReadBookIntent.BookInfoResult(result.resultCode == android.app.Activity.RESULT_OK))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── Effect collection: route handles launcher effects, rest goes to bridge ──
|
||||||
|
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
coroutineScope {
|
||||||
|
val collector = launch {
|
||||||
|
viewModel.effects.collect { effect ->
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// All other effects — delegate to bridge (View/Window/Activity operations)
|
||||||
|
else -> controller.handleEffect(effect)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
yield()
|
||||||
|
onEffectsReady()
|
||||||
|
collector.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ── System UI sync ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
LaunchedEffect(state.menuVisible) {
|
||||||
|
host.upSystemUiVisibility(host.isInMultiWindowModeCompat, !state.menuVisible)
|
||||||
|
}
|
||||||
|
// ── View layer + Compose UI ───────────────────────────────────────
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
key(controller) {
|
||||||
|
ReadBookViewLayer(
|
||||||
|
modifier = Modifier
|
||||||
|
.then(if (useMenuHazeSource) Modifier.hazeSource(menuHazeState) else Modifier)
|
||||||
|
.layerBackdrop(menuBackdrop),
|
||||||
|
onRefsReady = { controller.onRefsReady(it) },
|
||||||
|
onCursorTouch = controller,
|
||||||
|
readViewCallBack = controller,
|
||||||
|
contentTextViewCallBack = controller,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ReadBookColorTheme(
|
||||||
|
styleConfig = state.styleConfig,
|
||||||
|
preferences = readPreferences,
|
||||||
|
) {
|
||||||
|
ReadBookMenuBar(
|
||||||
|
state = state,
|
||||||
|
onIntent = viewModel::onIntent,
|
||||||
|
backdrop = menuBackdrop,
|
||||||
|
hazeState = if (useMenuHazeSource) menuHazeState else null,
|
||||||
|
)
|
||||||
|
ReadBookSearchBar(state = state, onIntent = viewModel::onIntent)
|
||||||
|
ReadBookScreen(
|
||||||
|
state = state,
|
||||||
|
onIntent = viewModel::onIntent,
|
||||||
|
onBack = { controller.closeReadBook() },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ReadBookViewLayer(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
onRefsReady: (ReadBookViewRefs) -> Unit,
|
||||||
|
onCursorTouch: View.OnTouchListener,
|
||||||
|
readViewCallBack: ReadView.CallBack,
|
||||||
|
contentTextViewCallBack: ContentTextView.CallBack,
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
modifier = modifier.fillMaxSize(),
|
||||||
|
factory = { context ->
|
||||||
|
FrameLayout(context).apply {
|
||||||
|
val readView = ReadView(
|
||||||
|
context = context,
|
||||||
|
callBack = readViewCallBack,
|
||||||
|
contentCallBack = contentTextViewCallBack,
|
||||||
|
).apply {
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val textMenuPosition = View(context).apply {
|
||||||
|
id = R.id.text_menu_position
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
layoutParams = FrameLayout.LayoutParams(0, 0)
|
||||||
|
}
|
||||||
|
val cursorLeft = ImageView(context).apply {
|
||||||
|
id = R.id.cursor_left
|
||||||
|
contentDescription = context.getString(R.string.select_start)
|
||||||
|
setImageResource(R.drawable.ic_cursor_left)
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
setOnTouchListener(onCursorTouch)
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val cursorRight = ImageView(context).apply {
|
||||||
|
id = R.id.cursor_right
|
||||||
|
contentDescription = context.getString(R.string.select_end)
|
||||||
|
setImageResource(R.drawable.ic_cursor_right)
|
||||||
|
visibility = View.INVISIBLE
|
||||||
|
setOnTouchListener(onCursorTouch)
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
FrameLayout.LayoutParams.WRAP_CONTENT,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val navigationBar = View(context).apply {
|
||||||
|
id = R.id.navigation_bar
|
||||||
|
layoutParams = FrameLayout.LayoutParams(
|
||||||
|
FrameLayout.LayoutParams.MATCH_PARENT,
|
||||||
|
0,
|
||||||
|
android.view.Gravity.BOTTOM,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addView(readView)
|
||||||
|
addView(textMenuPosition)
|
||||||
|
addView(cursorLeft)
|
||||||
|
addView(cursorRight)
|
||||||
|
addView(navigationBar)
|
||||||
|
|
||||||
|
onRefsReady(
|
||||||
|
ReadBookViewRefs(
|
||||||
|
root = this,
|
||||||
|
readView = readView,
|
||||||
|
textMenuPosition = textMenuPosition,
|
||||||
|
cursorLeft = cursorLeft,
|
||||||
|
cursorRight = cursorRight,
|
||||||
|
navigationBar = navigationBar,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,414 @@
|
|||||||
|
package io.legado.app.ui.book.read
|
||||||
|
|
||||||
|
import androidx.activity.compose.BackHandler
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
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.res.stringResource
|
||||||
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.book.info.ChangeSourceSheet
|
||||||
|
import io.legado.app.ui.widget.components.log.AppLogSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.BgTextConfigSheet
|
||||||
|
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.ClickActionConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.ContentEditSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.DictSheet
|
||||||
|
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.FontSelectSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.MoreConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.PageAnimConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.PageKeyConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.PhotoSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.ReadAloudConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.ReadAloudNumberConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.HighlightRuleConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.ShadowSetSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.SimulatedReadingSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.SpeakEngineConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.TitleBarIconSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.ToolButtonConfigSheet
|
||||||
|
import io.legado.app.ui.book.read.sheet.UnderlineConfigSheet
|
||||||
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stateless ReadBook screen — renders BackHandler + dialogs + sheets.
|
||||||
|
* ReadView is hosted in the XML layout, not here.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ReadBookScreen(
|
||||||
|
state: ReadBookUiState,
|
||||||
|
onIntent: (ReadBookIntent) -> Unit,
|
||||||
|
onBack: () -> Unit,
|
||||||
|
) {
|
||||||
|
BackHandler {
|
||||||
|
when {
|
||||||
|
state.isShowingSearchResult -> onIntent(ReadBookIntent.ExitSearch)
|
||||||
|
state.menuVisible -> onIntent(ReadBookIntent.ReadMenuBack)
|
||||||
|
state.isAutoPage -> onIntent(ReadBookIntent.StopAutoPage)
|
||||||
|
else -> onBack()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dialogs driven by activeDialog state
|
||||||
|
when (val dialog = state.activeDialog) {
|
||||||
|
is ReadBookDialog.ConfirmRestoreProgress -> {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
title = stringResource(R.string.restore_progress),
|
||||||
|
text = stringResource(R.string.found_cloud_progress),
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
onConfirm = {
|
||||||
|
onIntent(ReadBookIntent.SureNewProgress(dialog.progress))
|
||||||
|
onIntent(ReadBookIntent.DismissDialog)
|
||||||
|
},
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
onDismiss = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookDialog.SureSyncProgress -> {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
title = stringResource(R.string.sync_progress),
|
||||||
|
text = stringResource(R.string.progress_exceeds_cloud),
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
onConfirm = {
|
||||||
|
onIntent(ReadBookIntent.SureSyncProgress(dialog.progress))
|
||||||
|
onIntent(ReadBookIntent.DismissDialog)
|
||||||
|
},
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
onDismiss = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookDialog.ConfirmSkipToChapter -> {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
title = stringResource(R.string.chapter_list),
|
||||||
|
text = stringResource(R.string.confirm_skip_to_chapter),
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
onConfirm = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
onDismiss = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookDialog.ConfirmChapterPay -> {
|
||||||
|
AppAlertDialog(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
title = stringResource(R.string.chapter_pay),
|
||||||
|
text = dialog.chapterTitle,
|
||||||
|
confirmText = stringResource(R.string.ok),
|
||||||
|
onConfirm = {
|
||||||
|
onIntent(ReadBookIntent.DismissDialog)
|
||||||
|
onIntent(ReadBookIntent.ConfirmPayAction)
|
||||||
|
},
|
||||||
|
dismissText = stringResource(R.string.cancel),
|
||||||
|
onDismiss = { onIntent(ReadBookIntent.DismissDialog) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// AppModalBottomSheet-based sheets — always composed, controlled by show flag
|
||||||
|
// for proper enter/exit animations
|
||||||
|
val dismissSheet = { onIntent(ReadBookIntent.DismissSheet) }
|
||||||
|
|
||||||
|
ShadowSetSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.ShadowSet,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
EffectiveReplacesSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.EffectiveReplaces,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onOpenReplaceEditor = { id, pattern ->
|
||||||
|
onIntent(ReadBookIntent.OpenReplaceEditor(id, pattern))
|
||||||
|
},
|
||||||
|
onReplaceRuleChanged = { onIntent(ReadBookIntent.ReplaceRuleChanged) },
|
||||||
|
)
|
||||||
|
UnderlineConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.UnderlineConfig,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
FontSelectSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.FontSelect,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onSelectFont = { onIntent(ReadBookIntent.SelectFont(it)) },
|
||||||
|
onSelectSystemTypeface = { onIntent(ReadBookIntent.SelectSystemTypeface(it)) },
|
||||||
|
onOpenFolderPicker = { onIntent(ReadBookIntent.OpenFontFolderPicker) },
|
||||||
|
)
|
||||||
|
ToolButtonConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.ToolButtonConfig,
|
||||||
|
items = state.menuConfig.bottomBarButtons,
|
||||||
|
customIcons = state.menuConfig.readMenuCustomIcons,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
TitleBarIconSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.TitleBarIconConfig,
|
||||||
|
items = state.menuConfig.titleBarButtons,
|
||||||
|
customIcons = state.menuConfig.titleBarCustomIcons,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
HighlightRuleConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.HighlightRuleConfig,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
ContentEditSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.ContentEdit,
|
||||||
|
state = state,
|
||||||
|
onIntent = onIntent,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
MoreConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.MoreConfig,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
onOpenClickRegionalConfig = {
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ClickActionConfig))
|
||||||
|
},
|
||||||
|
onOpenPageKeyConfig = {
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.PageKeyConfig))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ReadAloudConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.ReadAloudConfig,
|
||||||
|
state = state,
|
||||||
|
onIntent = onIntent,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
SpeakEngineConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.SpeakEngineConfig,
|
||||||
|
items = state.ttsEngineItems,
|
||||||
|
selectedValue = state.selectedTtsEngine,
|
||||||
|
onSelect = { onIntent(ReadBookIntent.ApplySpeakEngine(it)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ReadAloudNumberConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.PreDownloadConfig,
|
||||||
|
title = stringResource(R.string.read_aloud_preload),
|
||||||
|
description = stringResource(R.string.read_aloud_preload_summary, state.preDownloadNum),
|
||||||
|
value = state.preDownloadNum,
|
||||||
|
defaultValue = 10,
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { onIntent(ReadBookIntent.ApplyPreDownloadNum(it)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ReadAloudNumberConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.AudioCacheCleanConfig,
|
||||||
|
title = stringResource(R.string.audio_cache_clean_time),
|
||||||
|
description = stringResource(
|
||||||
|
R.string.audio_cache_clean_time_summary,
|
||||||
|
state.audioCacheCleanTime
|
||||||
|
),
|
||||||
|
value = state.audioCacheCleanTime,
|
||||||
|
defaultValue = 10,
|
||||||
|
valueRange = 0f..10080f,
|
||||||
|
onValueChange = { onIntent(ReadBookIntent.ApplyAudioCacheCleanTime(it)) },
|
||||||
|
onDismissRequest = {
|
||||||
|
onIntent(ReadBookIntent.ShowSheet(ReadBookSheet.ReadAloudConfig))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
AppLogSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.AppLog,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
BgTextConfigSheet(
|
||||||
|
show = state.activeSheet is ReadBookSheet.BgTextConfig,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onIntent = onIntent,
|
||||||
|
onSelectImage = { onIntent(ReadBookIntent.OpenReadStyleImagePicker) },
|
||||||
|
onSelectImageForMode = { isNight ->
|
||||||
|
onIntent(ReadBookIntent.OpenReadStyleImagePickerForMode(isNight))
|
||||||
|
},
|
||||||
|
onImportConfig = { onIntent(ReadBookIntent.OpenReadStyleImport) },
|
||||||
|
onExportConfig = { onIntent(ReadBookIntent.OpenReadStyleExport) },
|
||||||
|
styleConfig = state.styleConfig,
|
||||||
|
)
|
||||||
|
val dictSheet = state.activeSheet as? ReadBookSheet.Dict
|
||||||
|
DictSheet(
|
||||||
|
show = dictSheet != null,
|
||||||
|
word = dictSheet?.word ?: "",
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
val photoSheet = state.activeSheet as? ReadBookSheet.Photo
|
||||||
|
PhotoSheet(
|
||||||
|
show = photoSheet != null,
|
||||||
|
src = photoSheet?.src ?: "",
|
||||||
|
sourceOrigin = photoSheet?.sourceOrigin,
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertDialog-based sheets and special cases — conditionally composed
|
||||||
|
when (state.activeSheet) {
|
||||||
|
is ReadBookSheet.ClickActionConfig -> {
|
||||||
|
ClickActionConfigSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.PageKeyConfig -> {
|
||||||
|
PageKeyConfigSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.PageAnim -> {
|
||||||
|
PageAnimConfigSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onAnimChanged = { onIntent(ReadBookIntent.PageAnimChanged) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.Download -> {
|
||||||
|
DownloadSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onDownload = { start, end ->
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
onIntent(ReadBookIntent.DownloadChapters(start, end))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.Charset -> {
|
||||||
|
CharsetConfigSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.SimulatedReading -> {
|
||||||
|
SimulatedReadingSheet(
|
||||||
|
onDismissRequest = dismissSheet,
|
||||||
|
onApply = { onIntent(ReadBookIntent.ApplySimulatedReading) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.Bookmark -> {
|
||||||
|
// Handled by ViewModel — redirects to menu route
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.InfoConfig -> {
|
||||||
|
// Integrated into ReadStyleSheet's HeaderFooterPage
|
||||||
|
LaunchedEffect(state.activeSheet) {
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.ChangeChapterSource -> {
|
||||||
|
val sheet = state.activeSheet
|
||||||
|
val book = state.book
|
||||||
|
if (book != null) {
|
||||||
|
var showSheet by remember { mutableStateOf(true) }
|
||||||
|
LaunchedEffect(showSheet) {
|
||||||
|
if (!showSheet) {
|
||||||
|
kotlinx.coroutines.delay(300)
|
||||||
|
onIntent(ReadBookIntent.SetActiveSheet(null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val viewModel = androidx.compose.runtime.key(
|
||||||
|
"chapter-source-${book.bookUrl}-${sheet.chapterIndex}"
|
||||||
|
) {
|
||||||
|
org.koin.androidx.compose.koinViewModel<io.legado.app.ui.book.changesource.ChangeChapterSourceViewModel>()
|
||||||
|
}
|
||||||
|
androidx.compose.runtime.DisposableEffect(viewModel) {
|
||||||
|
onDispose { viewModel.dispose() }
|
||||||
|
}
|
||||||
|
LaunchedEffect(book.bookUrl, sheet.chapterIndex) {
|
||||||
|
viewModel.initData(
|
||||||
|
book,
|
||||||
|
sheet.chapterIndex,
|
||||||
|
sheet.chapterTitle
|
||||||
|
)
|
||||||
|
}
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
ChangeChapterSourceSheet(
|
||||||
|
state = viewModel.uiState.collectAsStateWithLifecycle().value,
|
||||||
|
onIntent = viewModel::onIntent,
|
||||||
|
show = showSheet,
|
||||||
|
onDismissRequest = { showSheet = false },
|
||||||
|
onAnimationFinish = { onIntent(ReadBookIntent.SetActiveSheet(null)) },
|
||||||
|
bookScoreFlow = viewModel::bookScoreFlow,
|
||||||
|
onBookScoreClick = viewModel::onBookScoreClick,
|
||||||
|
onEditSource = { sourceUrl ->
|
||||||
|
onIntent(ReadBookIntent.OpenSourceEditByUrl(sourceUrl))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
// Handle ReplaceContent effect
|
||||||
|
LaunchedEffect(viewModel) {
|
||||||
|
viewModel.effects.collectLatest { effect ->
|
||||||
|
when (effect) {
|
||||||
|
is io.legado.app.ui.book.changesource.ChangeChapterSourceEffect.ReplaceContent -> {
|
||||||
|
showSheet = false
|
||||||
|
onIntent(ReadBookIntent.SaveChapterContent(effect.content, sheet.chapterIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
is io.legado.app.ui.book.changesource.ChangeChapterSourceEffect.ShowToast -> {
|
||||||
|
context.toastOnUi(effect.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
is io.legado.app.ui.book.changesource.ChangeChapterSourceEffect.Dismiss -> {
|
||||||
|
// Handled by showSheet animation — no-op
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(sheet) {
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is ReadBookSheet.ChangeBookSource -> {
|
||||||
|
val sheet = state.activeSheet
|
||||||
|
val book = state.book
|
||||||
|
if (book != null) {
|
||||||
|
ChangeSourceSheet(
|
||||||
|
show = true,
|
||||||
|
oldBook = book,
|
||||||
|
onDismissRequest = { onIntent(ReadBookIntent.DismissSheet) },
|
||||||
|
onReplace = { _, newBook, toc, _ ->
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
onIntent(ReadBookIntent.ChangeSource(newBook, toc))
|
||||||
|
},
|
||||||
|
onAddAsNew = { newBook, toc ->
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
onIntent(ReadBookIntent.AddSourceAsNewBook(newBook, toc))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
LaunchedEffect(sheet) {
|
||||||
|
onIntent(ReadBookIntent.DismissSheet)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
null -> {}
|
||||||
|
|
||||||
|
// Sheets using AppModalBottomSheet are composed unconditionally above
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
package io.legado.app.ui.book.read
|
||||||
|
|
||||||
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
|
import androidx.compose.animation.fadeIn
|
||||||
|
import androidx.compose.animation.fadeOut
|
||||||
|
import androidx.compose.animation.slideInVertically
|
||||||
|
import androidx.compose.animation.slideOutVertically
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
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.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowBack
|
||||||
|
import androidx.compose.material.icons.automirrored.filled.ArrowForward
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material.icons.filled.Menu
|
||||||
|
import androidx.compose.material.icons.filled.Search
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallFloatingActionButton
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compose replacement for SearchMenu — search result navigation overlay.
|
||||||
|
*/
|
||||||
|
@Composable
|
||||||
|
fun ReadBookSearchBar(
|
||||||
|
state: ReadBookUiState,
|
||||||
|
onIntent: (ReadBookIntent) -> Unit,
|
||||||
|
) {
|
||||||
|
val searchVisible = state.isShowingSearchResult &&
|
||||||
|
!(state.menuVisible && state.menuState.currentRoute != ReadBookMenuRoute.Main)
|
||||||
|
val hasResults = state.searchResultList.isNotEmpty()
|
||||||
|
val currentIndex = state.searchResultIndex
|
||||||
|
val totalResults = state.searchResultList.size
|
||||||
|
val currentResult = if (hasResults && currentIndex in state.searchResultList.indices) {
|
||||||
|
state.searchResultList[currentIndex]
|
||||||
|
} else null
|
||||||
|
|
||||||
|
Box(Modifier.fillMaxSize()) {
|
||||||
|
// Left FAB - previous result
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = searchVisible && hasResults && currentIndex > 0,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterStart)
|
||||||
|
.padding(start = 16.dp),
|
||||||
|
) {
|
||||||
|
SmallFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
val prevIndex = currentIndex - 1
|
||||||
|
onIntent(
|
||||||
|
ReadBookIntent.NavigateToSearchResult(
|
||||||
|
state.searchResultList[prevIndex], prevIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowBack,
|
||||||
|
contentDescription = "Previous",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right FAB - next result
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = searchVisible && hasResults && currentIndex < totalResults - 1,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.CenterEnd)
|
||||||
|
.padding(end = 16.dp),
|
||||||
|
) {
|
||||||
|
SmallFloatingActionButton(
|
||||||
|
onClick = {
|
||||||
|
val nextIndex = currentIndex + 1
|
||||||
|
onIntent(
|
||||||
|
ReadBookIntent.NavigateToSearchResult(
|
||||||
|
state.searchResultList[nextIndex], nextIndex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.AutoMirrored.Filled.ArrowForward,
|
||||||
|
contentDescription = "Next",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tap background to dismiss search menu
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = searchVisible && state.searchMenuVisible,
|
||||||
|
enter = fadeIn(),
|
||||||
|
exit = fadeOut(),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.clickable(
|
||||||
|
indication = null,
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
) { onIntent(ReadBookIntent.HideSearchMenu) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom menu
|
||||||
|
AnimatedVisibility(
|
||||||
|
visible = searchVisible && state.searchMenuVisible,
|
||||||
|
enter = slideInVertically(initialOffsetY = { it }) + fadeIn(),
|
||||||
|
exit = slideOutVertically(targetOffsetY = { it }) + fadeOut(),
|
||||||
|
modifier = Modifier.align(Alignment.BottomCenter),
|
||||||
|
) {
|
||||||
|
SearchBottomMenu(
|
||||||
|
state = state,
|
||||||
|
currentResult = currentResult,
|
||||||
|
onIntent = onIntent,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun SearchBottomMenu(
|
||||||
|
state: ReadBookUiState,
|
||||||
|
currentResult: io.legado.app.ui.book.searchContent.SearchResult?,
|
||||||
|
onIntent: (ReadBookIntent) -> Unit,
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.background(MaterialTheme.colorScheme.surfaceContainer)
|
||||||
|
.padding(bottom = 16.dp),
|
||||||
|
) {
|
||||||
|
// Search progress info
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 8.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
// Fraction: "3 / 10"
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "${state.searchResultIndex + 1} / ${state.searchResultList.size}",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(8.dp))
|
||||||
|
val percent = if (state.searchResultList.isNotEmpty()) {
|
||||||
|
((state.searchResultIndex + 1) * 100 / state.searchResultList.size)
|
||||||
|
} else 0
|
||||||
|
Text(
|
||||||
|
text = "$percent%",
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Current chapter
|
||||||
|
Card(
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceContainerLow,
|
||||||
|
contentColor = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
),
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = state.chapterName,
|
||||||
|
modifier = Modifier.padding(horizontal = 12.dp, vertical = 6.dp),
|
||||||
|
style = MaterialTheme.typography.bodyMedium,
|
||||||
|
maxLines = 1,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer(Modifier.height(4.dp))
|
||||||
|
|
||||||
|
// Action buttons
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
horizontalArrangement = Arrangement.SpaceEvenly,
|
||||||
|
) {
|
||||||
|
TextButton(
|
||||||
|
onClick = { onIntent(ReadBookIntent.OpenSearch(null)) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
Icons.Default.Search,
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(18.dp)
|
||||||
|
)
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("搜索内容")
|
||||||
|
}
|
||||||
|
TextButton(
|
||||||
|
onClick = { onIntent(ReadBookIntent.ShowMenu) },
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Menu, contentDescription = null, modifier = Modifier.size(18.dp))
|
||||||
|
Spacer(Modifier.width(4.dp))
|
||||||
|
Text("主菜单")
|
||||||
|
}
|
||||||
|
IconButton(
|
||||||
|
onClick = { onIntent(ReadBookIntent.ExitSearch) },
|
||||||
|
) {
|
||||||
|
Icon(Icons.Default.Close, contentDescription = "Exit search")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -1,948 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.content.res.ColorStateList
|
|
||||||
import android.content.res.Configuration
|
|
||||||
import android.graphics.drawable.GradientDrawable
|
|
||||||
import android.graphics.drawable.RippleDrawable
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.WindowManager.LayoutParams.BRIGHTNESS_OVERRIDE_NONE
|
|
||||||
import android.view.animation.AlphaAnimation
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import android.widget.SeekBar
|
|
||||||
import androidx.annotation.OptIn
|
|
||||||
import androidx.appcompat.widget.PopupMenu
|
|
||||||
import androidx.core.graphics.ColorUtils
|
|
||||||
import androidx.core.graphics.toColorInt
|
|
||||||
import androidx.core.view.HapticFeedbackConstantsCompat
|
|
||||||
import androidx.core.view.doOnAttach
|
|
||||||
import androidx.core.view.forEach
|
|
||||||
import androidx.core.view.isGone
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import com.google.android.material.badge.BadgeDrawable
|
|
||||||
import com.google.android.material.badge.BadgeUtils
|
|
||||||
import com.google.android.material.badge.ExperimentalBadgeUtils
|
|
||||||
import com.google.android.material.button.MaterialButton
|
|
||||||
import com.google.android.material.button.MaterialButtonGroup
|
|
||||||
import com.google.android.material.overflow.OverflowLinearLayout
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.ViewReadMenuBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.LocalConfig
|
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
|
||||||
import io.legado.app.help.source.getSourceType
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.browser.WebViewActivity
|
|
||||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
|
||||||
import io.legado.app.utils.ConstraintModify
|
|
||||||
import io.legado.app.utils.activity
|
|
||||||
import io.legado.app.utils.applyNavigationBarPadding
|
|
||||||
import io.legado.app.utils.dpToPx
|
|
||||||
import io.legado.app.utils.getPrefBoolean
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.invisible
|
|
||||||
import io.legado.app.utils.loadAnimation
|
|
||||||
import io.legado.app.utils.modifyBegin
|
|
||||||
import io.legado.app.utils.openUrl
|
|
||||||
import io.legado.app.utils.putPrefBoolean
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.themeColor
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import splitties.views.onClick
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 阅读界面菜单
|
|
||||||
*/
|
|
||||||
class ReadMenu @JvmOverloads constructor(
|
|
||||||
context: Context,
|
|
||||||
attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs) {
|
|
||||||
var canShowMenu: Boolean = false
|
|
||||||
private val callBack: CallBack get() = activity as CallBack
|
|
||||||
private val binding = ViewReadMenuBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
private var confirmSkipToChapter: Boolean = false
|
|
||||||
private var isMenuOutAnimating = false
|
|
||||||
|
|
||||||
private val menuTopIn: Animation by lazy {
|
|
||||||
loadAnimation(context, R.anim.anim_readbook_top_in)
|
|
||||||
}
|
|
||||||
private val menuTopOut: Animation by lazy {
|
|
||||||
loadAnimation(context, R.anim.anim_readbook_top_out)
|
|
||||||
}
|
|
||||||
private val menuBottomIn: Animation by lazy {
|
|
||||||
loadAnimation(context, R.anim.anim_readbook_bottom_in)
|
|
||||||
}
|
|
||||||
private val menuBottomOut: Animation by lazy {
|
|
||||||
loadAnimation(context, R.anim.anim_readbook_bottom_out)
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fadeIn = AlphaAnimation(0f, 1f).apply {
|
|
||||||
duration = 280
|
|
||||||
fillAfter = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val fadeOut = AlphaAnimation(1f, 0f).apply {
|
|
||||||
duration = 280
|
|
||||||
fillAfter = true
|
|
||||||
}
|
|
||||||
|
|
||||||
private val colorSurfaceContainer: Int
|
|
||||||
get() = context.themeColor(com.google.android.material.R.attr.colorSurfaceContainer)
|
|
||||||
|
|
||||||
private val colorSecondary: Int
|
|
||||||
get() = context.themeColor(androidx.appcompat.R.attr.colorPrimary)
|
|
||||||
|
|
||||||
private val colorSecondaryContainer: Int
|
|
||||||
get() = context.themeColor(com.google.android.material.R.attr.colorSecondaryContainer)
|
|
||||||
|
|
||||||
private val bgColor: Int
|
|
||||||
get() = when (AppConfig.readBarStyle) {
|
|
||||||
0 -> colorSurfaceContainer
|
|
||||||
1 -> runCatching {
|
|
||||||
ReadBookConfig.durConfig.curBgStr().toColorInt()
|
|
||||||
}.getOrDefault(colorSurfaceContainer)
|
|
||||||
else -> ReadBookConfig.durConfig.curMenuBg()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val acColor: Int
|
|
||||||
get() = when (AppConfig.readBarStyle) {
|
|
||||||
0 -> colorSecondary
|
|
||||||
1 -> runCatching {
|
|
||||||
ReadBookConfig.durConfig.curTextColor()
|
|
||||||
}.getOrDefault(colorSecondary)
|
|
||||||
else -> ReadBookConfig.durConfig.curMenuAc()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private val bgcColor: Int
|
|
||||||
get() = when (AppConfig.readBarStyle) {
|
|
||||||
0 -> colorSecondaryContainer
|
|
||||||
1 -> runCatching {
|
|
||||||
val baseColor = ReadBookConfig.durConfig.curTextColor()
|
|
||||||
ColorUtils.setAlphaComponent(baseColor, (255 * 0.1f).toInt())
|
|
||||||
}.getOrDefault(colorSecondaryContainer)
|
|
||||||
else -> ColorUtils.setAlphaComponent(acColor, (255 * 0.1f).toInt())
|
|
||||||
}
|
|
||||||
|
|
||||||
private var onMenuOutEnd: (() -> Unit)? = null
|
|
||||||
|
|
||||||
private val showBrightnessView
|
|
||||||
get() = context.getPrefBoolean(
|
|
||||||
PreferKey.showBrightnessView,
|
|
||||||
true
|
|
||||||
)
|
|
||||||
|
|
||||||
private val sourceMenu by lazy {
|
|
||||||
PopupMenu(context, binding.tvSourceAction).apply {
|
|
||||||
inflate(R.menu.book_read_source)
|
|
||||||
setOnMenuItemClickListener {
|
|
||||||
when (it.itemId) {
|
|
||||||
R.id.menu_login -> callBack.showLogin()
|
|
||||||
R.id.menu_chapter_pay -> callBack.payAction()
|
|
||||||
R.id.menu_edit_source -> callBack.openSourceEditActivity()
|
|
||||||
R.id.menu_disable_source -> callBack.disableSource()
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val menuInListener = object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(animation: Animation) {
|
|
||||||
binding.tvSourceAction.text =
|
|
||||||
ReadBook.bookSource?.bookSourceName ?: context.getString(R.string.book_source)
|
|
||||||
binding.tvSourceAction.isGone = ReadBook.isLocalBook
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
binding.llBrightness.visible(showBrightnessView)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
|
||||||
binding.vwMenuBg.setOnClickListener { runMenuOut() }
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
if (!LocalConfig.readMenuHelpVersionIsLast) {
|
|
||||||
callBack.showHelp()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
|
||||||
}
|
|
||||||
private val menuOutListener = object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(animation: Animation) {
|
|
||||||
isMenuOutAnimating = true
|
|
||||||
binding.vwMenuBg.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
|
||||||
this@ReadMenu.invisible()
|
|
||||||
binding.titleBar.invisible()
|
|
||||||
binding.bottomMenu.invisible()
|
|
||||||
canShowMenu = false
|
|
||||||
isMenuOutAnimating = false
|
|
||||||
onMenuOutEnd?.invoke()
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
doOnAttach {
|
|
||||||
initView()
|
|
||||||
upBrightnessState()
|
|
||||||
bindEvent()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
val bgColor = this@ReadMenu.bgColor
|
|
||||||
val acColor = this@ReadMenu.acColor
|
|
||||||
val bgcColor = this@ReadMenu.bgcColor
|
|
||||||
val alphaBgColor = ColorUtils.setAlphaComponent(bgColor, (AppConfig.menuAlpha / 100f * 255).toInt())
|
|
||||||
initAnimation()
|
|
||||||
updateSliderVisibility()
|
|
||||||
val brightnessBackground = GradientDrawable()
|
|
||||||
brightnessBackground.cornerRadius = 5F.dpToPx()
|
|
||||||
llBrightness.background = brightnessBackground
|
|
||||||
|
|
||||||
// if (AppConfig.isEInkMode) {
|
|
||||||
// titleBar.setBackgroundResource(R.drawable.bg_eink_border_bottom)
|
|
||||||
// }
|
|
||||||
|
|
||||||
llBrightness.setOnClickListener(null)
|
|
||||||
seekBrightness.post {
|
|
||||||
seekBrightness.progress = AppConfig.readBrightness
|
|
||||||
}
|
|
||||||
if (AppConfig.showReadTitleBarAddition) {
|
|
||||||
titleBarAddition.visible()
|
|
||||||
} else {
|
|
||||||
titleBarAddition.gone()
|
|
||||||
}
|
|
||||||
binding.bottomView.post {
|
|
||||||
val allButtons = getUserButtons()
|
|
||||||
renderButtons(binding.bottomView, allButtons)
|
|
||||||
}
|
|
||||||
titleBar.setBackgroundColor(alphaBgColor)
|
|
||||||
titleBar.toolbar.setBackgroundColor(alphaBgColor)
|
|
||||||
bottomView.setBackgroundColor(alphaBgColor)
|
|
||||||
(tvPre.background as? RippleDrawable)?.setColor(ColorStateList.valueOf(bgcColor))
|
|
||||||
(tvNext.background as? RippleDrawable)?.setColor(ColorStateList.valueOf(bgcColor))
|
|
||||||
cdSlider.setCardBackgroundColor(alphaBgColor)
|
|
||||||
seekReadPage.trackInactiveTintList = ColorStateList.valueOf(bgcColor)
|
|
||||||
seekReadPage.trackActiveTintList = ColorStateList.valueOf(acColor)
|
|
||||||
seekReadPage.thumbTintList = ColorStateList.valueOf(acColor)
|
|
||||||
seekReadPage.tickActiveTintList = ColorStateList.valueOf(bgColor)
|
|
||||||
seekReadPage.tickInactiveTintList = ColorStateList.valueOf(acColor)
|
|
||||||
tvPre.iconTint = ColorStateList.valueOf(acColor)
|
|
||||||
tvNext.iconTint = ColorStateList.valueOf(acColor)
|
|
||||||
tvBookName.setTextColor(acColor)
|
|
||||||
tvChapterName.setTextColor(acColor)
|
|
||||||
tvChapterUrl.setTextColor(acColor)
|
|
||||||
tvSourceAction.setTextColor(acColor)
|
|
||||||
tvPre.backgroundTintList = ColorStateList.valueOf(bgColor)
|
|
||||||
tvNext.backgroundTintList = ColorStateList.valueOf(bgColor)
|
|
||||||
tvPre.alpha = AppConfig.menuAlpha / 100f * 255
|
|
||||||
tvNext.alpha = AppConfig.menuAlpha / 100f * 255
|
|
||||||
upBrightnessVwPos()
|
|
||||||
/**
|
|
||||||
* 确保视图不被导航栏遮挡
|
|
||||||
*/
|
|
||||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
|
||||||
binding.bottomMenu.applyNavigationBarPadding()
|
|
||||||
} else {
|
|
||||||
binding.bottomView.applyNavigationBarPadding()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateToolBarColor() {
|
|
||||||
val acColor = this@ReadMenu.acColor
|
|
||||||
binding.titleBar.toolbar.navigationIcon?.setTint(acColor)
|
|
||||||
binding.titleBar.toolbar.apply {
|
|
||||||
setTitleTextColor(acColor)
|
|
||||||
setSubtitleTextColor(acColor)
|
|
||||||
}
|
|
||||||
binding.titleBar.toolbar.menu.forEach { item ->
|
|
||||||
item.icon?.setTint(acColor)
|
|
||||||
}
|
|
||||||
binding.titleBar.toolbar.overflowIcon?.setTint(acColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun reset() {
|
|
||||||
initView()
|
|
||||||
updateToolBarColor()
|
|
||||||
upBookView()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun refreshMenuColorFilter() {
|
|
||||||
// if (immersiveMenu) {
|
|
||||||
// //binding.titleBar.setColorFilter(textColor)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upBrightnessState() {
|
|
||||||
if (brightnessAuto()) {
|
|
||||||
binding.ivBrightnessAuto.setColorFilter(context.themeColor(androidx.appcompat.R.attr.colorPrimary))
|
|
||||||
binding.seekBrightness.isEnabled = false
|
|
||||||
} else {
|
|
||||||
binding.ivBrightnessAuto.setColorFilter(context.themeColor(com.google.android.material.R.attr.colorOnSurface))
|
|
||||||
binding.seekBrightness.isEnabled = true
|
|
||||||
}
|
|
||||||
setScreenBrightness(AppConfig.readBrightness.toFloat())
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 设置屏幕亮度
|
|
||||||
*/
|
|
||||||
fun setScreenBrightness(value: Float) {
|
|
||||||
activity?.run {
|
|
||||||
var brightness = BRIGHTNESS_OVERRIDE_NONE
|
|
||||||
if (!brightnessAuto() && value != BRIGHTNESS_OVERRIDE_NONE) {
|
|
||||||
brightness = value
|
|
||||||
if (brightness < 1f) brightness = 1f
|
|
||||||
brightness /= 255f
|
|
||||||
}
|
|
||||||
val params = window.attributes
|
|
||||||
params.screenBrightness = brightness
|
|
||||||
window.attributes = params
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runMenuIn(anim: Boolean = !AppConfig.isEInkMode) {
|
|
||||||
callBack.onMenuShow()
|
|
||||||
this.visible()
|
|
||||||
binding.titleBar.visible()
|
|
||||||
binding.bottomMenu.visible()
|
|
||||||
updateToolBarColor()
|
|
||||||
changeReplace(ReadBook.book?.getUseReplaceRule() ?: false)
|
|
||||||
updateBadge("replace_badge", ReadBook.curTextChapter?.effectiveReplaceRules?.size ?: 0)
|
|
||||||
if (anim) {
|
|
||||||
binding.titleBar.startAnimation(menuTopIn)
|
|
||||||
binding.bottomMenu.startAnimation(menuBottomIn)
|
|
||||||
updateBrightnessVisibility(true)
|
|
||||||
} else {
|
|
||||||
menuInListener.onAnimationStart(menuBottomIn)
|
|
||||||
menuInListener.onAnimationEnd(menuBottomIn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runMenuOut(anim: Boolean = !AppConfig.isEInkMode, onMenuOutEnd: (() -> Unit)? = null) {
|
|
||||||
if (isMenuOutAnimating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
callBack.onMenuHide()
|
|
||||||
this.onMenuOutEnd = onMenuOutEnd
|
|
||||||
if (this.isVisible) {
|
|
||||||
if (anim) {
|
|
||||||
binding.titleBar.startAnimation(menuTopOut)
|
|
||||||
binding.bottomMenu.startAnimation(menuBottomOut)
|
|
||||||
updateBrightnessVisibility(false)
|
|
||||||
|
|
||||||
} else {
|
|
||||||
menuOutListener.onAnimationStart(menuBottomOut)
|
|
||||||
menuOutListener.onAnimationEnd(menuBottomOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateBrightnessVisibility(boolean: Boolean) {
|
|
||||||
if (showBrightnessView) {
|
|
||||||
if(boolean){
|
|
||||||
binding.llBrightness.startAnimation(fadeIn)
|
|
||||||
}else{
|
|
||||||
binding.llBrightness.startAnimation(fadeOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun brightnessAuto(): Boolean {
|
|
||||||
return context.getPrefBoolean("brightnessAuto", true) || !showBrightnessView
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindEvent() = binding.run {
|
|
||||||
vwMenuBg.setOnClickListener { runMenuOut() }
|
|
||||||
titleBar.toolbar.setOnClickListener {
|
|
||||||
callBack.openBookInfoActivity()
|
|
||||||
}
|
|
||||||
tvBookName.setOnClickListener {
|
|
||||||
callBack.openBookInfoActivity()
|
|
||||||
}
|
|
||||||
val chapterViewClickListener = OnClickListener {
|
|
||||||
if (ReadBook.isLocalBook) {
|
|
||||||
return@OnClickListener
|
|
||||||
}
|
|
||||||
if (AppConfig.readUrlInBrowser) {
|
|
||||||
context.openUrl(tvChapterUrl.text.toString().substringBefore(",{"))
|
|
||||||
} else {
|
|
||||||
Coroutine.async {
|
|
||||||
context.startActivity<WebViewActivity> {
|
|
||||||
val url = tvChapterUrl.text.toString()
|
|
||||||
val bookSource = ReadBook.bookSource
|
|
||||||
putExtra("title", tvChapterName.text)
|
|
||||||
putExtra("url", url)
|
|
||||||
putExtra("sourceOrigin", bookSource?.bookSourceUrl)
|
|
||||||
putExtra("sourceName", bookSource?.bookSourceName)
|
|
||||||
putExtra("sourceType", bookSource?.getSourceType())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
val chapterViewLongClickListener = OnLongClickListener {
|
|
||||||
if (ReadBook.isLocalBook) {
|
|
||||||
return@OnLongClickListener true
|
|
||||||
}
|
|
||||||
context.alert(R.string.open_fun) {
|
|
||||||
setMessage(R.string.use_browser_open)
|
|
||||||
okButton {
|
|
||||||
AppConfig.readUrlInBrowser = true
|
|
||||||
}
|
|
||||||
noButton {
|
|
||||||
AppConfig.readUrlInBrowser = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
true
|
|
||||||
}
|
|
||||||
tvChapterName.setOnClickListener(chapterViewClickListener)
|
|
||||||
tvChapterName.setOnLongClickListener(chapterViewLongClickListener)
|
|
||||||
//书源操作
|
|
||||||
tvSourceAction.onClick {
|
|
||||||
sourceMenu.menu.findItem(R.id.menu_login).isVisible =
|
|
||||||
!ReadBook.bookSource?.loginUrl.isNullOrEmpty()
|
|
||||||
sourceMenu.menu.findItem(R.id.menu_chapter_pay).isVisible =
|
|
||||||
!ReadBook.bookSource?.loginUrl.isNullOrEmpty()
|
|
||||||
&& ReadBook.curTextChapter?.isVip == true
|
|
||||||
&& ReadBook.curTextChapter?.isPay != true
|
|
||||||
sourceMenu.show()
|
|
||||||
}
|
|
||||||
//亮度跟随
|
|
||||||
ivBrightnessAuto.setOnClickListener {
|
|
||||||
context.putPrefBoolean("brightnessAuto", !brightnessAuto())
|
|
||||||
upBrightnessState()
|
|
||||||
}
|
|
||||||
//亮度调节
|
|
||||||
seekBrightness.setOnSeekBarChangeListener(object : SeekBarChangeListener {
|
|
||||||
|
|
||||||
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
|
|
||||||
if (fromUser) {
|
|
||||||
setScreenBrightness(progress.toFloat())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(seekBar: SeekBar) {
|
|
||||||
AppConfig.readBrightness = seekBar.progress
|
|
||||||
}
|
|
||||||
|
|
||||||
})
|
|
||||||
|
|
||||||
vwBrightnessPosAdjust.setOnClickListener {
|
|
||||||
AppConfig.brightnessVwPos = !AppConfig.brightnessVwPos
|
|
||||||
upBrightnessVwPos()
|
|
||||||
}
|
|
||||||
|
|
||||||
seekReadPage.addOnChangeListener { _, value, fromUser ->
|
|
||||||
if (fromUser) {
|
|
||||||
if (AppConfig.progressBarBehavior == "page")
|
|
||||||
ReadBook.skipToPage(value.toInt() - 1)
|
|
||||||
if (AppConfig.sliderVibrator)
|
|
||||||
HapticFeedbackConstantsCompat.TEXT_HANDLE_MOVE
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seekReadPage.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
|
||||||
vwMenuBg.setOnClickListener(null)
|
|
||||||
//VibrationUtils.vibrate(context, 16)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
vwMenuBg.setOnClickListener { runMenuOut() }
|
|
||||||
val progress = slider.value.toInt()
|
|
||||||
|
|
||||||
when (AppConfig.progressBarBehavior) {
|
|
||||||
"page" -> ReadBook.skipToPage(progress - 1)
|
|
||||||
"chapter" -> {
|
|
||||||
if (confirmSkipToChapter) {
|
|
||||||
callBack.skipToChapter(progress - 1)
|
|
||||||
} else {
|
|
||||||
context.alert("章节跳转确认", "确定要跳转章节吗?") {
|
|
||||||
yesButton {
|
|
||||||
confirmSkipToChapter = true
|
|
||||||
callBack.skipToChapter(progress - 1)
|
|
||||||
}
|
|
||||||
noButton { upSeekBar() }
|
|
||||||
onCancelled { upSeekBar() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
//上一章
|
|
||||||
tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) }
|
|
||||||
|
|
||||||
//下一章
|
|
||||||
tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSliderVisibility() {
|
|
||||||
when (AppConfig.readSliderMode) {
|
|
||||||
"0" -> {
|
|
||||||
binding.llSlider.gravity = Gravity.CENTER
|
|
||||||
binding.llSlider.isVisible = true
|
|
||||||
binding.cdSlider.isVisible = true
|
|
||||||
binding.tvPre.isVisible = true
|
|
||||||
binding.tvNext.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
"1" -> {
|
|
||||||
binding.llSlider.gravity = Gravity.CENTER
|
|
||||||
binding.llSlider.isVisible = false
|
|
||||||
binding.cdSlider.isVisible = false
|
|
||||||
binding.tvPre.isVisible = false
|
|
||||||
binding.tvNext.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
"2" -> {
|
|
||||||
binding.llSlider.gravity = Gravity.START
|
|
||||||
binding.llSlider.isVisible = true
|
|
||||||
binding.cdSlider.isVisible = false
|
|
||||||
binding.tvPre.isVisible = true
|
|
||||||
binding.tvNext.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
"3" -> {
|
|
||||||
binding.llSlider.gravity = Gravity.END
|
|
||||||
binding.llSlider.isVisible = true
|
|
||||||
binding.cdSlider.isVisible = false
|
|
||||||
binding.tvPre.isVisible = true
|
|
||||||
binding.tvNext.isVisible = true
|
|
||||||
}
|
|
||||||
|
|
||||||
"4" -> {
|
|
||||||
binding.llSlider.gravity = Gravity.CENTER
|
|
||||||
binding.llSlider.isVisible = true
|
|
||||||
binding.cdSlider.isVisible = true
|
|
||||||
binding.tvPre.isVisible = false
|
|
||||||
binding.tvNext.isVisible = false
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
binding.llSlider.isVisible = true
|
|
||||||
binding.cdSlider.isVisible = true
|
|
||||||
binding.tvPre.isVisible = true
|
|
||||||
binding.tvNext.isVisible = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val buttonMap = mutableMapOf<String, MaterialButton>()
|
|
||||||
|
|
||||||
fun renderButtons(group: MaterialButtonGroup, buttons: List<ToolButton>) {
|
|
||||||
group.removeAllViews()
|
|
||||||
buttonMap.clear()
|
|
||||||
|
|
||||||
buttons.forEach { btn ->
|
|
||||||
val style = com.google.android.material.R.attr.materialIconButtonOutlinedStyle
|
|
||||||
val button = MaterialButton(group.context, null, style).apply {
|
|
||||||
id = btn.id.hashCode()
|
|
||||||
setIconResource(btn.iconRes)
|
|
||||||
contentDescription = btn.description
|
|
||||||
tooltipText = btn.description
|
|
||||||
strokeWidth = 0
|
|
||||||
iconGravity = MaterialButton.ICON_GRAVITY_TEXT_START
|
|
||||||
iconTint = ColorStateList.valueOf(acColor)
|
|
||||||
val bgColorState = ColorStateList(
|
|
||||||
arrayOf(
|
|
||||||
intArrayOf(android.R.attr.state_checked),
|
|
||||||
intArrayOf(-android.R.attr.state_checked)
|
|
||||||
),
|
|
||||||
intArrayOf(
|
|
||||||
bgcColor,
|
|
||||||
bgColor
|
|
||||||
)
|
|
||||||
)
|
|
||||||
backgroundTintList = bgColorState
|
|
||||||
maxLines = 1
|
|
||||||
if (btn.onCheck != null) {
|
|
||||||
isCheckable = true
|
|
||||||
isChecked = btn.state
|
|
||||||
setOnClickListener {
|
|
||||||
isChecked = !isChecked
|
|
||||||
btn.state = isChecked
|
|
||||||
btn.onCheck.invoke()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setOnClickListener { btn.onClick() }
|
|
||||||
}
|
|
||||||
btn.onLongClick?.let { longAction ->
|
|
||||||
setOnLongClickListener {
|
|
||||||
longAction()
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
group.addView(
|
|
||||||
button,
|
|
||||||
OverflowLinearLayout.LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, 1f)
|
|
||||||
)
|
|
||||||
val lp =
|
|
||||||
button.layoutParams as MaterialButtonGroup.LayoutParams
|
|
||||||
lp.overflowText = btn.description
|
|
||||||
buttonMap[btn.id] = button
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val badgeMap = mutableMapOf<MaterialButton, BadgeDrawable>()
|
|
||||||
|
|
||||||
@OptIn(ExperimentalBadgeUtils::class)
|
|
||||||
fun updateBadge(id: String, count: Int) {
|
|
||||||
val btn = buttonMap[id] ?: return
|
|
||||||
if (count != 0 && btn.isChecked) {
|
|
||||||
btn.addBadge(count)
|
|
||||||
} else {
|
|
||||||
badgeMap[btn]?.let { BadgeUtils.detachBadgeDrawable(it, btn) }
|
|
||||||
badgeMap.remove(btn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@OptIn(ExperimentalBadgeUtils::class)
|
|
||||||
private fun MaterialButton.addBadge(count: Int) {
|
|
||||||
val badgeDrawable = BadgeDrawable.create(context).apply {
|
|
||||||
number = count
|
|
||||||
backgroundColor = colorSecondary
|
|
||||||
badgeTextColor = colorSecondaryContainer
|
|
||||||
maxCharacterCount = 3
|
|
||||||
badgeGravity = BadgeDrawable.TOP_END
|
|
||||||
verticalOffset = (16).dpToPx()
|
|
||||||
}
|
|
||||||
|
|
||||||
BadgeUtils.attachBadgeDrawable(badgeDrawable, this, null)
|
|
||||||
badgeMap[this] = badgeDrawable
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun getAllButtons(): List<ToolButton> {
|
|
||||||
return listOf(
|
|
||||||
ToolButton(
|
|
||||||
id = "search",
|
|
||||||
iconRes = R.drawable.ic_search,
|
|
||||||
description = context.getString(R.string.search_content),
|
|
||||||
onClick = { runMenuOut { callBack.openSearchActivity(null) } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "catalog",
|
|
||||||
iconRes = R.drawable.ic_toc,
|
|
||||||
description = context.getString(R.string.chapter_list),
|
|
||||||
onClick = { runMenuOut { callBack.openChapterList() } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "read_aloud",
|
|
||||||
iconRes = R.drawable.ic_read_aloud,
|
|
||||||
description = context.getString(R.string.read_aloud),
|
|
||||||
onClick = { runMenuOut { callBack.onClickReadAloud() } },
|
|
||||||
onLongClick = { runMenuOut { callBack.onClickReadAloud() } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "setting",
|
|
||||||
iconRes = R.drawable.ic_settings,
|
|
||||||
description = context.getString(R.string.setting),
|
|
||||||
onClick = { runMenuOut { callBack.showReadStyle() } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "addBookmark",
|
|
||||||
iconRes = R.drawable.ic_bookmark,
|
|
||||||
description = context.getString(R.string.bookmark),
|
|
||||||
onClick = { runMenuOut { callBack.addBookmark() } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "theme",
|
|
||||||
iconRes = if (AppConfig.isNightTheme) R.drawable.ic_daytime else R.drawable.ic_brightness,
|
|
||||||
description = context.getString(R.string.day_night_switch),
|
|
||||||
onClick = {
|
|
||||||
AppConfig.isNightTheme = !AppConfig.isNightTheme
|
|
||||||
OldThemeConfig.applyDayNight(context)
|
|
||||||
buttonMap["theme"]?.setIconResource(
|
|
||||||
if (AppConfig.isNightTheme) R.drawable.ic_daytime else R.drawable.ic_brightness
|
|
||||||
)
|
|
||||||
}
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "prev_chapter",
|
|
||||||
iconRes = R.drawable.ic_previous,
|
|
||||||
description = context.getString(R.string.previous_chapter),
|
|
||||||
onClick = { ReadBook.moveToPrevChapter(upContent = true, toLast = false) }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "next_chapter",
|
|
||||||
iconRes = R.drawable.ic_next,
|
|
||||||
description = context.getString(R.string.next_chapter),
|
|
||||||
onClick = { ReadBook.moveToNextChapter(true) }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "replace",
|
|
||||||
iconRes = R.drawable.ic_find_replace,
|
|
||||||
description = context.getString(R.string.replace_purify),
|
|
||||||
onLongClick = { runMenuOut { callBack.openReplaceRule() } },
|
|
||||||
onCheck = { runMenuOut { callBack.changeReplaceRuleState() } },
|
|
||||||
onClick = { }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "replace_badge",
|
|
||||||
iconRes = R.drawable.ic_find_replace,
|
|
||||||
description = context.getString(R.string.replace_purify_badge),
|
|
||||||
onLongClick = { runMenuOut { callBack.openReplaceRule() } },
|
|
||||||
onCheck = { runMenuOut { callBack.changeReplaceRuleState() } },
|
|
||||||
onClick = { }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "auto_page",
|
|
||||||
iconRes = R.drawable.ic_auto_page,
|
|
||||||
description = context.getString(R.string.auto_next_page),
|
|
||||||
onClick = { runMenuOut { callBack.autoPage() } }
|
|
||||||
),
|
|
||||||
ToolButton(
|
|
||||||
id = "translate",
|
|
||||||
iconRes = R.drawable.ic_translate,
|
|
||||||
description = context.getString(R.string.translate),
|
|
||||||
onClick = { runMenuOut { callBack.onTranslationClick() } },
|
|
||||||
onLongClick = { runMenuOut { callBack.onTranslationLongClick() } }
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changeReplace(boolean: Boolean) {
|
|
||||||
buttonMap["replace"]?.isChecked = boolean
|
|
||||||
buttonMap["replace_badge"]?.isChecked = boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUserButtons(): List<ToolButton> {
|
|
||||||
val prefs by lazy {
|
|
||||||
context.getSharedPreferences("tool_button_config", Context.MODE_PRIVATE)
|
|
||||||
}
|
|
||||||
val allButtons = getAllButtons().associateBy { it.id }
|
|
||||||
|
|
||||||
val str = prefs.getString("tool_buttons", null)
|
|
||||||
val savedList = str?.split(";")?.mapNotNull {
|
|
||||||
val parts = it.split(",")
|
|
||||||
if (parts.size == 2) parts[0] to parts[1].toBoolean() else null
|
|
||||||
} ?: emptyList()
|
|
||||||
|
|
||||||
val result = mutableListOf<ToolButton>()
|
|
||||||
|
|
||||||
if (savedList.isNotEmpty()) {
|
|
||||||
savedList.forEach { (id, enabled) ->
|
|
||||||
if (enabled) allButtons[id]?.let { result.add(it) }
|
|
||||||
}
|
|
||||||
|
|
||||||
getAllButtons().forEach { btn ->
|
|
||||||
if (savedList.none { it.first == btn.id }) {
|
|
||||||
result.add(btn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
result.addAll(getAllButtons().take(5))
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
fun setAutoPage(autoPage: Boolean) {
|
|
||||||
buttonMap["auto_page"]?.apply {
|
|
||||||
val icon = if (autoPage) R.drawable.ic_auto_page_stop else R.drawable.ic_auto_page
|
|
||||||
val desc =
|
|
||||||
context.getString(if (autoPage) R.string.auto_next_page_stop else R.string.auto_next_page)
|
|
||||||
setIconResource(icon)
|
|
||||||
contentDescription = desc
|
|
||||||
tooltipText = desc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateTranslationButton(translationMode: Boolean) {
|
|
||||||
val btn = buttonMap["translate"] ?: return
|
|
||||||
|
|
||||||
// Update icon based on mode
|
|
||||||
if (translationMode) {
|
|
||||||
btn.setIconResource(R.drawable.ic_return)
|
|
||||||
} else {
|
|
||||||
btn.setIconResource(R.drawable.ic_translate)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAnimation() {
|
|
||||||
menuTopIn.setAnimationListener(menuInListener)
|
|
||||||
menuTopOut.setAnimationListener(menuOutListener)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upBookView() {
|
|
||||||
val bookName = ReadBook.book?.name ?: ""
|
|
||||||
|
|
||||||
val mode = AppConfig.titleBarMode?.toInt()
|
|
||||||
|
|
||||||
when (mode) {
|
|
||||||
0 -> { // 在应用栏上显示
|
|
||||||
binding.titleBar.title = bookName
|
|
||||||
binding.llBook.visible()
|
|
||||||
binding.tvBookName.gone()
|
|
||||||
}
|
|
||||||
1 -> { // 在独立行上显示
|
|
||||||
binding.titleBar.title = " "
|
|
||||||
binding.tvBookName.text = bookName
|
|
||||||
binding.llBook.visible()
|
|
||||||
binding.tvBookName.visible()
|
|
||||||
}
|
|
||||||
2 -> { // 仅显示标题
|
|
||||||
binding.titleBar.title = bookName
|
|
||||||
binding.llBook.gone()
|
|
||||||
}
|
|
||||||
3 -> { // 不显示
|
|
||||||
binding.titleBar.title = " "
|
|
||||||
binding.llBook.gone()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
binding.titleBar.title = " "
|
|
||||||
binding.tvBookName.text = bookName
|
|
||||||
binding.llBook.visible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ReadBook.curTextChapter?.let {
|
|
||||||
binding.tvChapterName.text = it.title
|
|
||||||
if (!ReadBook.isLocalBook) {
|
|
||||||
binding.tvChapterUrl.text = it.chapter.getAbsoluteURL()
|
|
||||||
//binding.tvChapterUrl.visible()
|
|
||||||
} else {
|
|
||||||
binding.tvChapterUrl.gone()
|
|
||||||
}
|
|
||||||
|
|
||||||
upSeekBar()
|
|
||||||
binding.tvPre.isEnabled = ReadBook.durChapterIndex != 0
|
|
||||||
binding.tvNext.isEnabled = ReadBook.durChapterIndex != ReadBook.simulatedChapterSize - 1
|
|
||||||
} ?: run {
|
|
||||||
binding.tvChapterUrl.gone()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun upSeekBar() = binding.seekReadPage.apply {
|
|
||||||
|
|
||||||
fun safeSet(rangeFrom: Float, rangeTo: Float, step: Float, rawValue: Float) {
|
|
||||||
valueFrom = rangeFrom
|
|
||||||
valueTo = rangeTo
|
|
||||||
stepSize = step
|
|
||||||
val safeValue = rawValue.coerceIn(rangeFrom, rangeTo)
|
|
||||||
if (value != safeValue) value = safeValue
|
|
||||||
}
|
|
||||||
|
|
||||||
when (AppConfig.progressBarBehavior) {
|
|
||||||
"page" -> {
|
|
||||||
ReadBook.curTextChapter?.let { chapter ->
|
|
||||||
if (chapter.pageSize > 0 && ReadBook.durPageIndex >= 0) {
|
|
||||||
safeSet(
|
|
||||||
rangeFrom = 1f,
|
|
||||||
rangeTo = chapter.pageSize.toFloat().coerceAtLeast(2f),
|
|
||||||
step = 1f,
|
|
||||||
rawValue = ReadBook.durPageIndex.toFloat()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
safeSet(0f, 100000f, 0f, 0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
"chapter" -> {
|
|
||||||
if (ReadBook.simulatedChapterSize > 0) {
|
|
||||||
safeSet(
|
|
||||||
rangeFrom = 1f,
|
|
||||||
rangeTo = ReadBook.simulatedChapterSize.toFloat().coerceAtLeast(2f),
|
|
||||||
step = 1f,
|
|
||||||
rawValue = ReadBook.durChapterIndex.toFloat()
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
safeSet(0f, 100000f, 0f, 0f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// fun upSeekBar() {
|
|
||||||
// binding.seekReadPage.apply {
|
|
||||||
// when (AppConfig.progressBarBehavior) {
|
|
||||||
// "page" -> {
|
|
||||||
// ReadBook.curTextChapter?.let {
|
|
||||||
// max = it.pageSize.minus(1)
|
|
||||||
// progress = ReadBook.durPageIndex
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// "chapter" -> {
|
|
||||||
// max = ReadBook.simulatedChapterSize - 1
|
|
||||||
// progress = ReadBook.durChapterIndex
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
fun setSeekPage(seek: Int) {
|
|
||||||
binding.seekReadPage.value = seek.toFloat() + 1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upBrightnessVwPos() {
|
|
||||||
if (AppConfig.brightnessVwPos) {
|
|
||||||
binding.root.modifyBegin()
|
|
||||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.LEFT)
|
|
||||||
.rightToRightOf(R.id.ll_brightness, R.id.vw_menu_root)
|
|
||||||
.commit()
|
|
||||||
} else {
|
|
||||||
binding.root.modifyBegin()
|
|
||||||
.clear(R.id.ll_brightness, ConstraintModify.Anchor.RIGHT)
|
|
||||||
.leftToLeftOf(R.id.ll_brightness, R.id.vw_menu_root)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun autoPage()
|
|
||||||
fun openReplaceRule()
|
|
||||||
fun openChapterList()
|
|
||||||
fun openSearchActivity(searchWord: String?)
|
|
||||||
fun openSourceEditActivity()
|
|
||||||
fun openBookInfoActivity()
|
|
||||||
fun showReadStyle()
|
|
||||||
fun addBookmark()
|
|
||||||
fun showReadAloudDialog()
|
|
||||||
fun upSystemUiVisibility()
|
|
||||||
fun onClickReadAloud()
|
|
||||||
fun showHelp()
|
|
||||||
fun showLogin()
|
|
||||||
fun payAction()
|
|
||||||
fun disableSource()
|
|
||||||
fun skipToChapter(index: Int)
|
|
||||||
fun onMenuShow()
|
|
||||||
fun onMenuHide()
|
|
||||||
fun changeReplaceRuleState()
|
|
||||||
fun onTranslationClick()
|
|
||||||
fun onTranslationLongClick()
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ToolButton(
|
|
||||||
val id: String, // 唯一标识
|
|
||||||
val iconRes: Int, // 图标资源
|
|
||||||
val description: String, // contentDescription / tooltipText
|
|
||||||
val onClick: () -> Unit, // 点击事件
|
|
||||||
val onLongClick: (() -> Unit)? = null, // 可选长按
|
|
||||||
val onCheck: (() -> Unit)? = null,// 可选
|
|
||||||
var state: Boolean = false // 动态
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,208 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.animation.Animation
|
|
||||||
import android.widget.FrameLayout
|
|
||||||
import androidx.core.view.isVisible
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.databinding.ViewSearchMenuBinding
|
|
||||||
//import io.legado.app.lib.theme.bottomBackground
|
|
||||||
//import io.legado.app.lib.theme.getPrimaryTextColor
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.book.searchContent.SearchResult
|
|
||||||
import io.legado.app.utils.activity
|
|
||||||
import io.legado.app.utils.applyNavigationBarPadding
|
|
||||||
import io.legado.app.utils.invisible
|
|
||||||
import io.legado.app.utils.loadAnimation
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 搜索界面菜单
|
|
||||||
*/
|
|
||||||
class SearchMenu @JvmOverloads constructor(
|
|
||||||
context: Context, attrs: AttributeSet? = null
|
|
||||||
) : FrameLayout(context, attrs) {
|
|
||||||
|
|
||||||
private val callBack: CallBack get() = activity as CallBack
|
|
||||||
private val binding = ViewSearchMenuBinding.inflate(LayoutInflater.from(context), this, true)
|
|
||||||
|
|
||||||
private val menuBottomIn: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_in)
|
|
||||||
private val menuBottomOut: Animation = loadAnimation(context, R.anim.anim_readbook_bottom_out)
|
|
||||||
|
|
||||||
private var onMenuOutEnd: (() -> Unit)? = null
|
|
||||||
private var isMenuOutAnimating = false
|
|
||||||
|
|
||||||
private val searchResultList: MutableList<SearchResult> = mutableListOf()
|
|
||||||
private var currentSearchResultIndex: Int = -1
|
|
||||||
private var lastSearchResultIndex: Int = -1
|
|
||||||
private val hasSearchResult: Boolean
|
|
||||||
get() = searchResultList.isNotEmpty()
|
|
||||||
val selectedSearchResult: SearchResult?
|
|
||||||
get() = searchResultList.getOrNull(currentSearchResultIndex)
|
|
||||||
val previousSearchResult: SearchResult?
|
|
||||||
get() = searchResultList.getOrNull(lastSearchResultIndex)
|
|
||||||
val bottomMenuVisible get() = isVisible && binding.llBottomMenu.isVisible
|
|
||||||
|
|
||||||
init {
|
|
||||||
initAnimation()
|
|
||||||
initView()
|
|
||||||
bindEvent()
|
|
||||||
updateSearchInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun upSearchResultList(resultList: List<SearchResult>) {
|
|
||||||
searchResultList.clear()
|
|
||||||
searchResultList.addAll(resultList)
|
|
||||||
updateSearchInfo()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
applyNavigationBarPadding()
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
fun runMenuIn() {
|
|
||||||
this.visible()
|
|
||||||
binding.llBottomMenu.visible()
|
|
||||||
binding.vwMenuBg.visible()
|
|
||||||
binding.llBottomMenu.startAnimation(menuBottomIn)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun runMenuOut(onMenuOutEnd: (() -> Unit)? = null) {
|
|
||||||
if (isMenuOutAnimating) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
this.onMenuOutEnd = onMenuOutEnd
|
|
||||||
if (this.isVisible) {
|
|
||||||
binding.llBottomMenu.startAnimation(menuBottomOut)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
fun updateSearchInfo() {
|
|
||||||
ReadBook.curTextChapter?.let {
|
|
||||||
binding.tvCurrentChapter.text = "当前章节: ${it.title}"
|
|
||||||
}
|
|
||||||
updateSearchProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun updateSearchResultIndex(updateIndex: Int) {
|
|
||||||
lastSearchResultIndex = currentSearchResultIndex
|
|
||||||
currentSearchResultIndex = when {
|
|
||||||
updateIndex < 0 -> 0
|
|
||||||
updateIndex >= searchResultList.size -> searchResultList.size - 1
|
|
||||||
else -> updateIndex
|
|
||||||
}
|
|
||||||
updateSearchProgress()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateSearchProgress() {
|
|
||||||
val total = searchResultList.size
|
|
||||||
if (total == 0) {
|
|
||||||
binding.tvSearchProgress.text = "0%"
|
|
||||||
binding.tvSearchFraction.text = "0/0"
|
|
||||||
} else {
|
|
||||||
val current = currentSearchResultIndex + 1
|
|
||||||
val progress = (current * 100 / total)
|
|
||||||
binding.tvSearchProgress.text = "$progress%"
|
|
||||||
binding.tvSearchFraction.text = "$current / $total"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun bindEvent() = binding.run {
|
|
||||||
//搜索结果
|
|
||||||
ivSearchResults.setOnClickListener {
|
|
||||||
runMenuOut {
|
|
||||||
callBack.openSearchActivity(selectedSearchResult?.query)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//主菜单
|
|
||||||
ivMainMenu.setOnClickListener {
|
|
||||||
runMenuOut {
|
|
||||||
callBack.cancelSelect()
|
|
||||||
callBack.showMenuBar()
|
|
||||||
this@SearchMenu.invisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//退出
|
|
||||||
ivSearchExit.setOnClickListener {
|
|
||||||
runMenuOut {
|
|
||||||
callBack.exitSearchMenu()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fabLeft.setOnClickListener {
|
|
||||||
updateSearchResultIndex(currentSearchResultIndex - 1)
|
|
||||||
callBack.navigateToSearch(
|
|
||||||
searchResultList[currentSearchResultIndex],
|
|
||||||
currentSearchResultIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fabRight.setOnClickListener {
|
|
||||||
updateSearchResultIndex(currentSearchResultIndex + 1)
|
|
||||||
callBack.navigateToSearch(
|
|
||||||
searchResultList[currentSearchResultIndex],
|
|
||||||
currentSearchResultIndex
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initAnimation() {
|
|
||||||
//显示菜单
|
|
||||||
menuBottomIn.setAnimationListener(object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(animation: Animation) {
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
binding.fabLeft.visible(hasSearchResult)
|
|
||||||
binding.fabRight.visible(hasSearchResult)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("RtlHardcoded")
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
|
||||||
binding.vwMenuBg.setOnClickListener { runMenuOut() }
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
|
||||||
})
|
|
||||||
|
|
||||||
//隐藏菜单
|
|
||||||
menuBottomOut.setAnimationListener(object : Animation.AnimationListener {
|
|
||||||
override fun onAnimationStart(animation: Animation) {
|
|
||||||
isMenuOutAnimating = true
|
|
||||||
binding.vwMenuBg.setOnClickListener(null)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationEnd(animation: Animation) {
|
|
||||||
isMenuOutAnimating = false
|
|
||||||
binding.llBottomMenu.invisible()
|
|
||||||
binding.vwMenuBg.invisible()
|
|
||||||
binding.vwMenuBg.setOnClickListener { runMenuOut() }
|
|
||||||
|
|
||||||
onMenuOutEnd?.invoke()
|
|
||||||
callBack.upSystemUiVisibility()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onAnimationRepeat(animation: Animation) = Unit
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
var isShowingSearchResult: Boolean
|
|
||||||
fun openSearchActivity(searchWord: String?)
|
|
||||||
fun showSearchSetting()
|
|
||||||
fun upSystemUiVisibility()
|
|
||||||
fun exitSearchMenu()
|
|
||||||
fun showMenuBar()
|
|
||||||
fun navigateToSearch(searchResult: SearchResult, index: Int)
|
|
||||||
fun onMenuShow()
|
|
||||||
fun onMenuHide()
|
|
||||||
fun cancelSelect()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -18,11 +18,9 @@ import androidx.core.view.isVisible
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
import io.legado.app.base.adapter.ItemViewHolder
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
import io.legado.app.base.adapter.RecyclerAdapter
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.ItemTextBinding
|
import io.legado.app.databinding.ItemTextBinding
|
||||||
import io.legado.app.databinding.PopupActionMenuBinding
|
import io.legado.app.databinding.PopupActionMenuBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.utils.getPrefBoolean
|
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
@@ -34,7 +32,11 @@ import androidx.core.net.toUri
|
|||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
class TextActionMenu(private val context: Context, private val callBack: CallBack) :
|
class TextActionMenu(
|
||||||
|
private val context: Context,
|
||||||
|
private val callBack: CallBack,
|
||||||
|
private val expandTextMenu: () -> Boolean
|
||||||
|
) :
|
||||||
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
|
PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) {
|
||||||
|
|
||||||
private val binding = PopupActionMenuBinding.inflate(LayoutInflater.from(context))
|
private val binding = PopupActionMenuBinding.inflate(LayoutInflater.from(context))
|
||||||
@@ -45,7 +47,6 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
|||||||
private val menuItems: List<MenuItemImpl>
|
private val menuItems: List<MenuItemImpl>
|
||||||
private val visibleMenuItems = arrayListOf<MenuItemImpl>()
|
private val visibleMenuItems = arrayListOf<MenuItemImpl>()
|
||||||
private val moreMenuItems = arrayListOf<MenuItemImpl>()
|
private val moreMenuItems = arrayListOf<MenuItemImpl>()
|
||||||
private val expandTextMenu get() = context.getPrefBoolean(PreferKey.expandTextMenu)
|
|
||||||
|
|
||||||
init {
|
init {
|
||||||
@SuppressLint("InflateParams")
|
@SuppressLint("InflateParams")
|
||||||
@@ -66,7 +67,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
|||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerViewMore.adapter = adapter
|
binding.recyclerViewMore.adapter = adapter
|
||||||
setOnDismissListener {
|
setOnDismissListener {
|
||||||
if (!context.getPrefBoolean(PreferKey.expandTextMenu)) {
|
if (!expandTextMenu()) {
|
||||||
binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert)
|
binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert)
|
||||||
binding.recyclerViewMore.gone()
|
binding.recyclerViewMore.gone()
|
||||||
adapter.setItems(visibleMenuItems)
|
adapter.setItems(visibleMenuItems)
|
||||||
@@ -90,7 +91,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun upMenu() {
|
fun upMenu() {
|
||||||
if (expandTextMenu) {
|
if (expandTextMenu()) {
|
||||||
adapter.setItems(menuItems)
|
adapter.setItems(menuItems)
|
||||||
binding.ivMenuMore.gone()
|
binding.ivMenuMore.gone()
|
||||||
} else {
|
} else {
|
||||||
@@ -108,7 +109,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
|||||||
endX: Int,
|
endX: Int,
|
||||||
endBottomY: Int
|
endBottomY: Int
|
||||||
) {
|
) {
|
||||||
if (expandTextMenu) {
|
if (expandTextMenu()) {
|
||||||
when {
|
when {
|
||||||
startTopY > 500 -> {
|
startTopY > 500 -> {
|
||||||
showAtLocation(
|
showAtLocation(
|
||||||
@@ -272,4 +273,4 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
|||||||
|
|
||||||
fun onMenuActionFinally()
|
fun onMenuActionFinally()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,100 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.databinding.DialogAutoReadBinding
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
//import io.legado.app.lib.theme.bottomBackground
|
|
||||||
//import io.legado.app.lib.theme.getPrimaryTextColor
|
|
||||||
import io.legado.app.model.ReadAloud
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.service.BaseReadAloudService
|
|
||||||
import io.legado.app.ui.book.read.BaseReadBookActivity
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import java.util.Locale
|
|
||||||
|
|
||||||
|
|
||||||
class AutoReadDialog : BaseBottomSheetDialogFragment(R.layout.dialog_auto_read) {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogAutoReadBinding::bind)
|
|
||||||
private val callBack: CallBack? get() = activity as? CallBack
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) = binding.run {
|
|
||||||
val bottomDialog = (activity as ReadBookActivity).bottomDialog++
|
|
||||||
if (bottomDialog > 0) {
|
|
||||||
dismiss()
|
|
||||||
return@run
|
|
||||||
}
|
|
||||||
initOnChange()
|
|
||||||
initData()
|
|
||||||
initEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() {
|
|
||||||
val speed = if (ReadBookConfig.autoReadSpeed < 1) 1 else ReadBookConfig.autoReadSpeed
|
|
||||||
binding.tvReadSpeed.text = String.format(Locale.ROOT, "%ds", speed)
|
|
||||||
binding.seekAutoRead.value = speed.toFloat()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initOnChange() {
|
|
||||||
binding.seekAutoRead.addOnChangeListener { slider, value, fromUser ->
|
|
||||||
val speed = if (value < 1) 1 else value.toInt()
|
|
||||||
binding.tvReadSpeed.text = String.format(Locale.ROOT, "%ds", speed)
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.seekAutoRead.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
ReadBookConfig.autoReadSpeed = if (slider.value < 1) 1 else slider.value.toInt()
|
|
||||||
upTtsSpeechRate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvent() {
|
|
||||||
binding.btnMainMenu.setOnClickListener {
|
|
||||||
callBack?.showMenuBar()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
binding.btnSetting.setOnClickListener {
|
|
||||||
(activity as BaseReadBookActivity).showPageAnimConfig {
|
|
||||||
(activity as ReadBookActivity).upPageAnim()
|
|
||||||
ReadBook.loadContent(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.btnCatalog.setOnClickListener { callBack?.openChapterList() }
|
|
||||||
binding.btnAutoPageStop.setOnClickListener {
|
|
||||||
callBack?.autoPageStop()
|
|
||||||
binding.btnAutoPageStop.post {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTtsSpeechRate() {
|
|
||||||
ReadAloud.upTtsSpeechRate(requireContext())
|
|
||||||
if (!BaseReadAloudService.pause) {
|
|
||||||
ReadAloud.pause(requireContext())
|
|
||||||
ReadAloud.resume(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun showMenuBar()
|
|
||||||
fun openChapterList()
|
|
||||||
fun autoPageStop()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.ItemBgImageBinding
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.glide.ImageLoader
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import java.io.File
|
|
||||||
|
|
||||||
class BgAdapter(context: Context, val textColor: Int) :
|
|
||||||
RecyclerAdapter<String, ItemBgImageBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemBgImageBinding {
|
|
||||||
return ItemBgImageBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemBgImageBinding,
|
|
||||||
item: String,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.run {
|
|
||||||
tvName.text = item.substringBeforeLast(".")
|
|
||||||
ImageLoader.load(
|
|
||||||
context,
|
|
||||||
context.assets.open("bg${File.separator}$item").readBytes()
|
|
||||||
)
|
|
||||||
.centerCrop()
|
|
||||||
.into(ivBg)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemBgImageBinding) {
|
|
||||||
holder.itemView.apply {
|
|
||||||
this.setOnClickListener {
|
|
||||||
getItemByLayoutPosition(holder.layoutPosition)?.let {
|
|
||||||
ReadBookConfig.durConfig.setCurBg(1, it)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Bitmap
|
||||||
|
import android.graphics.BitmapFactory
|
||||||
|
import android.graphics.BitmapShader
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Matrix
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.graphics.RectF
|
||||||
|
import android.graphics.Shader
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 背景图+下划线 Span,用于高亮规则匹配区域
|
||||||
|
*/
|
||||||
|
class BgImageSpan(
|
||||||
|
private val textColor: Int,
|
||||||
|
private val bgImagePath: String,
|
||||||
|
private val bgImageFit: Int = 0,
|
||||||
|
private val bgImageScale: Float = 1f,
|
||||||
|
private val underlineMode: Int = 0,
|
||||||
|
private val underlineColor: Int = 0,
|
||||||
|
private val underlineWidth: Float = 1f,
|
||||||
|
private val underlineSvgPath: String = "",
|
||||||
|
private val underlineOffset: Float = 6f,
|
||||||
|
) : ReplacementSpan() {
|
||||||
|
|
||||||
|
private val offsetPx = underlineOffset.toInt().dpToPx()
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
if (fm != null) {
|
||||||
|
val metrics = paint.fontMetricsInt
|
||||||
|
fm.top = metrics.top
|
||||||
|
fm.ascent = metrics.ascent
|
||||||
|
fm.descent = metrics.descent + if (underlineMode != 0) offsetPx else 0
|
||||||
|
fm.bottom = metrics.bottom + if (underlineMode != 0) offsetPx else 0
|
||||||
|
}
|
||||||
|
return paint.measureText(text, start, end).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(
|
||||||
|
canvas: Canvas,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
x: Float,
|
||||||
|
top: Int,
|
||||||
|
y: Int,
|
||||||
|
bottom: Int,
|
||||||
|
paint: Paint
|
||||||
|
) {
|
||||||
|
val width = paint.measureText(text, start, end)
|
||||||
|
val rectWidth = width
|
||||||
|
val rectHeight = (bottom - top).toFloat()
|
||||||
|
val scale = bgImageScale.coerceIn(0.1f, 5f)
|
||||||
|
|
||||||
|
val bitmap = TextLine.getBgBitmap(bgImagePath)
|
||||||
|
if (bitmap != null) {
|
||||||
|
val bgPaint = Paint().apply {
|
||||||
|
style = Paint.Style.FILL
|
||||||
|
isAntiAlias = true
|
||||||
|
isFilterBitmap = true
|
||||||
|
}
|
||||||
|
when (bgImageFit) {
|
||||||
|
1 -> {
|
||||||
|
val sw = rectWidth * scale
|
||||||
|
val sh = rectHeight * scale
|
||||||
|
val dx = x + (rectWidth - sw) / 2f
|
||||||
|
val dy = top + (rectHeight - sh) / 2f
|
||||||
|
canvas.save()
|
||||||
|
canvas.clipRect(x, top.toFloat(), x + width, bottom.toFloat())
|
||||||
|
canvas.drawBitmap(bitmap, null, RectF(dx, dy, dx + sw, dy + sh), bgPaint)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
2 -> {
|
||||||
|
val bw = bitmap.width.toFloat()
|
||||||
|
val bh = bitmap.height.toFloat()
|
||||||
|
val fitScale = (rectWidth / bw).coerceAtLeast(rectHeight / bh) * scale
|
||||||
|
val scaledW = bw * fitScale
|
||||||
|
val scaledH = bh * fitScale
|
||||||
|
val dx = x + (rectWidth - scaledW) / 2f
|
||||||
|
val dy = top + (rectHeight - scaledH) / 2f
|
||||||
|
canvas.save()
|
||||||
|
canvas.clipRect(x, top.toFloat(), x + width, bottom.toFloat())
|
||||||
|
canvas.drawBitmap(bitmap, null, RectF(dx, dy, dx + scaledW, dy + scaledH), bgPaint)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
val shader = BitmapShader(bitmap, Shader.TileMode.REPEAT, Shader.TileMode.REPEAT)
|
||||||
|
val matrix = Matrix()
|
||||||
|
if (scale != 1f) {
|
||||||
|
matrix.setScale(scale, scale)
|
||||||
|
}
|
||||||
|
matrix.postTranslate(x, top.toFloat())
|
||||||
|
shader.setLocalMatrix(matrix)
|
||||||
|
bgPaint.shader = shader
|
||||||
|
canvas.drawRect(x, top.toFloat(), x + width, bottom.toFloat(), bgPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
paint.color = textColor
|
||||||
|
paint.shader = null
|
||||||
|
canvas.drawText(text, start, end, x, y.toFloat(), paint)
|
||||||
|
|
||||||
|
if (underlineMode != 0) {
|
||||||
|
drawUnderline(canvas, x, x + width, y + offsetPx, paint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawUnderline(canvas: Canvas, startX: Float, endX: Float, lineY: Int, paint: Paint) {
|
||||||
|
val ulPaint = Paint(paint).apply {
|
||||||
|
color = underlineColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = underlineWidth.dpToPx()
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
when (underlineMode) {
|
||||||
|
1 -> canvas.drawLine(startX, lineY.toFloat(), endX, lineY.toFloat(), ulPaint)
|
||||||
|
2 -> {
|
||||||
|
ulPaint.pathEffect = android.graphics.DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||||
|
canvas.drawLine(startX, lineY.toFloat(), endX, lineY.toFloat(), ulPaint)
|
||||||
|
}
|
||||||
|
3 -> {
|
||||||
|
val path = android.graphics.Path()
|
||||||
|
val waveAmplitude = 3.dpToPx().toFloat()
|
||||||
|
val waveLength = 12.dpToPx().toFloat()
|
||||||
|
path.moveTo(startX, lineY.toFloat())
|
||||||
|
var currentX = startX
|
||||||
|
val endY = lineY.toFloat()
|
||||||
|
while (currentX < endX) {
|
||||||
|
val nextX = (currentX + waveLength).coerceAtMost(endX)
|
||||||
|
val midX = (currentX + nextX) / 2
|
||||||
|
path.quadTo(midX, endY - waveAmplitude, nextX, endY)
|
||||||
|
currentX = nextX
|
||||||
|
if (currentX < endX) {
|
||||||
|
val nextX2 = (currentX + waveLength).coerceAtMost(endX)
|
||||||
|
val midX2 = (currentX + nextX2) / 2
|
||||||
|
path.quadTo(midX2, endY + waveAmplitude, nextX2, endY)
|
||||||
|
currentX = nextX2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
canvas.drawPath(path, ulPaint)
|
||||||
|
}
|
||||||
|
4 -> {
|
||||||
|
val lineGap = 3.dpToPx()
|
||||||
|
val line2Y = lineY + lineGap + underlineWidth.dpToPx()
|
||||||
|
canvas.drawLine(startX, lineY.toFloat(), endX, lineY.toFloat(), ulPaint)
|
||||||
|
canvas.drawLine(startX, line2Y.toFloat(), endX, line2Y.toFloat(), ulPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.TooltipCompat
|
|
||||||
import androidx.core.graphics.toColorInt
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
|
||||||
import io.legado.app.databinding.DialogReadBgTextBinding
|
|
||||||
import io.legado.app.databinding.ItemBgImageBinding
|
|
||||||
import io.legado.app.help.DefaultData
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
|
||||||
import io.legado.app.help.http.okHttpClient
|
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.lib.dialogs.selector
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
|
||||||
import io.legado.app.utils.FileDoc
|
|
||||||
import io.legado.app.utils.FileUtils
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.MD5Utils
|
|
||||||
import io.legado.app.utils.SelectImageContract
|
|
||||||
import io.legado.app.utils.compress.ZipUtils
|
|
||||||
import io.legado.app.utils.createFileIfNotExist
|
|
||||||
import io.legado.app.utils.createFileReplace
|
|
||||||
import io.legado.app.utils.createFolderReplace
|
|
||||||
import io.legado.app.utils.delete
|
|
||||||
import io.legado.app.utils.externalCache
|
|
||||||
import io.legado.app.utils.externalFiles
|
|
||||||
import io.legado.app.utils.find
|
|
||||||
import io.legado.app.utils.getFile
|
|
||||||
import io.legado.app.utils.inputStream
|
|
||||||
import io.legado.app.utils.isContentScheme
|
|
||||||
import io.legado.app.utils.launch
|
|
||||||
import io.legado.app.utils.longToast
|
|
||||||
import io.legado.app.utils.openInputStream
|
|
||||||
import io.legado.app.utils.openOutputStream
|
|
||||||
import io.legado.app.utils.outputStream
|
|
||||||
import io.legado.app.utils.parseToUri
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.printOnDebug
|
|
||||||
import io.legado.app.utils.readBytes
|
|
||||||
import io.legado.app.utils.readUri
|
|
||||||
import io.legado.app.utils.stackTraceStr
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import splitties.init.appCtx
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
class BgTextConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_bg_text) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val BG_COLOR = 122
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogReadBgTextBinding::bind)
|
|
||||||
private val configFileName = "readConfig.zip"
|
|
||||||
private val adapter by lazy { BgAdapter(requireContext(), secondaryTextColor) }
|
|
||||||
private var secondaryTextColor = 0
|
|
||||||
private val importFormNet = "网络导入"
|
|
||||||
private val selectBgImage = registerForActivityResult(SelectImageContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
setBgFromUri(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val selectExportDir = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
exportConfig(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val selectImportDoc = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
if (uri.toString() == importFormNet) {
|
|
||||||
importNetConfigAlert()
|
|
||||||
} else {
|
|
||||||
importConfig(uri)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
dialog?.window?.run {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as ReadBookActivity).bottomDialog++
|
|
||||||
initView()
|
|
||||||
initData()
|
|
||||||
initEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
ReadBookConfig.save()
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
adapter.addHeaderView {
|
|
||||||
ItemBgImageBinding.inflate(layoutInflater, it, false).apply {
|
|
||||||
tvName.text = getString(R.string.select_image)
|
|
||||||
ivBg.setImageResource(R.drawable.ic_add)
|
|
||||||
root.setOnClickListener {
|
|
||||||
selectBgImage.launch()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
requireContext().assets.list("bg")?.let {
|
|
||||||
adapter.setItems(it.toList())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun initData() = with(ReadBookConfig.durConfig) {
|
|
||||||
binding.tvName.text = name.ifBlank { "文字" }
|
|
||||||
binding.swDarkStatusIcon.isChecked = curStatusIconDark()
|
|
||||||
binding.sbBgAlpha.value = ReadBookConfig.bgAlpha.toFloat()
|
|
||||||
binding.dottedRatio.valueFormat = {
|
|
||||||
(ReadBookConfig.dottedRatio * 100).toInt().toString()
|
|
||||||
}
|
|
||||||
binding.dottedBase.valueFormat = {
|
|
||||||
(ReadBookConfig.dottedBase * 100).toInt().toString()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun initEvent() = with(ReadBookConfig.durConfig) {
|
|
||||||
binding.ivEdit.setOnClickListener {
|
|
||||||
alert(R.string.style_name) {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = "name"
|
|
||||||
editView.setText(ReadBookConfig.durConfig.name)
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let {
|
|
||||||
binding.tvName.text = it
|
|
||||||
ReadBookConfig.durConfig.name = it
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvRestore.setOnClickListener {
|
|
||||||
val defaultConfigs = DefaultData.readConfigs
|
|
||||||
val layoutNames = defaultConfigs.map { it.name }
|
|
||||||
context?.selector("选择预设布局", layoutNames) { _, i ->
|
|
||||||
if (i >= 0) {
|
|
||||||
ReadBookConfig.durConfig = defaultConfigs[i].copy()
|
|
||||||
initData()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.swDarkStatusIcon.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
setCurStatusIconDark(isChecked)
|
|
||||||
(activity as? ReadBookActivity)?.upSystemUiVisibility()
|
|
||||||
}
|
|
||||||
binding.tvBgColor.setOnClickListener {
|
|
||||||
val bgColor =
|
|
||||||
if (curBgType() == 0) curBgStr().toColorInt()
|
|
||||||
else "#015A86".toColorInt()
|
|
||||||
ColorPickerDialog.newBuilder()
|
|
||||||
.setColor(bgColor)
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(BG_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
binding.tvBgColor.apply {
|
|
||||||
TooltipCompat.setTooltipText(this, text)
|
|
||||||
}
|
|
||||||
binding.ivImport.setOnClickListener {
|
|
||||||
selectImportDoc.launch {
|
|
||||||
mode = HandleFileContract.FILE
|
|
||||||
title = getString(R.string.import_str)
|
|
||||||
allowExtensions = arrayOf("zip")
|
|
||||||
otherActions = arrayListOf(SelectItem(importFormNet, -1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.ivExport.setOnClickListener {
|
|
||||||
selectExportDir.launch {
|
|
||||||
title = getString(R.string.export_str)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.ivDelete.setOnClickListener {
|
|
||||||
if (ReadBookConfig.deleteDur()) {
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
} else {
|
|
||||||
toastOnUi("数量已是最少,不能删除.")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.sbBgAlpha.addOnChangeListener { slider, value, fromUser ->
|
|
||||||
ReadBookConfig.bgAlpha = value.toInt()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(3))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.sbBgAlpha.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {
|
|
||||||
// 可留空
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(3))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
binding.dottedRatio.onChanged = {
|
|
||||||
ReadBookConfig.dottedRatio = it / 100f
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11))
|
|
||||||
}
|
|
||||||
binding.dottedBase.onChanged = {
|
|
||||||
ReadBookConfig.dottedBase = it / 100f
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun exportConfig(uri: Uri) {
|
|
||||||
val exportFileName = if (ReadBookConfig.config.name.isBlank()) {
|
|
||||||
configFileName
|
|
||||||
} else {
|
|
||||||
"${ReadBookConfig.config.name}.zip"
|
|
||||||
}
|
|
||||||
execute {
|
|
||||||
val exportFiles = arrayListOf<File>()
|
|
||||||
val configDir = requireContext().externalCache.getFile("readConfig")
|
|
||||||
configDir.createFolderReplace()
|
|
||||||
val configFile = configDir.getFile("readConfig.json")
|
|
||||||
configFile.createFileReplace()
|
|
||||||
val config = ReadBookConfig.getExportConfig()
|
|
||||||
val fontPath = ReadBookConfig.textFont
|
|
||||||
if (fontPath.isNotEmpty()) {
|
|
||||||
val fontDoc = FileDoc.fromFile(fontPath)
|
|
||||||
val fontName = fontDoc.name
|
|
||||||
val fontInputStream = fontDoc.openInputStream().getOrNull()
|
|
||||||
fontInputStream?.use {
|
|
||||||
val fontExportFile = FileUtils.createFileIfNotExist(configDir, fontName)
|
|
||||||
fontExportFile.outputStream().use { out ->
|
|
||||||
it.copyTo(out)
|
|
||||||
}
|
|
||||||
config.textFont = fontName
|
|
||||||
exportFiles.add(fontExportFile)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
configFile.writeText(GSON.toJson(config))
|
|
||||||
exportFiles.add(configFile)
|
|
||||||
repeat(3) {
|
|
||||||
val path = ReadBookConfig.durConfig.getBgPath(it) ?: return@repeat
|
|
||||||
val bgExportFile = copyBgImage(path, configDir) ?: return@repeat
|
|
||||||
exportFiles.add(bgExportFile)
|
|
||||||
}
|
|
||||||
val configZipPath = FileUtils.getPath(requireContext().externalCache, configFileName)
|
|
||||||
if (ZipUtils.zipFiles(exportFiles, File(configZipPath))) {
|
|
||||||
val exportDir = FileDoc.fromDir(uri)
|
|
||||||
exportDir.find(exportFileName)?.delete()
|
|
||||||
val exportFileDoc = exportDir.createFileIfNotExist(exportFileName)
|
|
||||||
exportFileDoc.openOutputStream().getOrThrow().use { out ->
|
|
||||||
File(configZipPath).inputStream().use {
|
|
||||||
it.copyTo(out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
toastOnUi("导出成功, 文件名为 $exportFileName")
|
|
||||||
}.onError {
|
|
||||||
it.printOnDebug()
|
|
||||||
AppLog.put("导出失败:${it.localizedMessage}", it)
|
|
||||||
longToast("导出失败:${it.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun copyBgImage(path: String, configDir: File): File? {
|
|
||||||
val bgName = FileUtils.getName(path)
|
|
||||||
val bgFile = File(path)
|
|
||||||
if (bgFile.exists()) {
|
|
||||||
val bgExportFile = File(FileUtils.getPath(configDir, bgName))
|
|
||||||
if (!bgExportFile.exists()) {
|
|
||||||
bgFile.copyTo(bgExportFile)
|
|
||||||
return bgExportFile
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("InflateParams")
|
|
||||||
private fun importNetConfigAlert() {
|
|
||||||
alert("输入地址") {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater)
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let { url ->
|
|
||||||
importNetConfig(url)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importNetConfig(url: String) {
|
|
||||||
execute {
|
|
||||||
okHttpClient.newCallResponseBody {
|
|
||||||
url(url)
|
|
||||||
}.bytes().let {
|
|
||||||
importConfig(it)
|
|
||||||
}
|
|
||||||
}.onError {
|
|
||||||
longToast(it.stackTraceStr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importConfig(uri: Uri) {
|
|
||||||
execute {
|
|
||||||
importConfig(uri.readBytes(requireContext()))
|
|
||||||
}.onError {
|
|
||||||
it.printOnDebug()
|
|
||||||
longToast("导入失败:${it.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importConfig(byteArray: ByteArray) {
|
|
||||||
execute {
|
|
||||||
ReadBookConfig.import(byteArray)
|
|
||||||
}.onSuccess {
|
|
||||||
ReadBookConfig.durConfig = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
||||||
toastOnUi("导入成功")
|
|
||||||
}.onError {
|
|
||||||
it.printOnDebug()
|
|
||||||
longToast("导入失败:${it.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setBgFromUri(uri: Uri) {
|
|
||||||
readUri(uri) { fileDoc, inputStream ->
|
|
||||||
kotlin.runCatching {
|
|
||||||
var file = requireContext().externalFiles
|
|
||||||
val suffix = fileDoc.name.substringAfterLast(".")
|
|
||||||
val fileName = uri.inputStream(requireContext()).getOrThrow().use {
|
|
||||||
MD5Utils.md5Encode(it) + ".$suffix"
|
|
||||||
}
|
|
||||||
file = FileUtils.createFileIfNotExist(file, "bg", fileName)
|
|
||||||
FileOutputStream(file).use { outputStream ->
|
|
||||||
inputStream.copyTo(outputStream)
|
|
||||||
}
|
|
||||||
ReadBookConfig.durConfig.setCurBg(2, fileName)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1))
|
|
||||||
}.onFailure {
|
|
||||||
appCtx.toastOnUi(it.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.TextView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseOverlayDialogFragment
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.DialogClickActionConfigBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.selector
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.utils.getCompatColor
|
|
||||||
import io.legado.app.utils.putPrefInt
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 点击区域设置
|
|
||||||
*/
|
|
||||||
class ClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_click_action_config) {
|
|
||||||
private val binding by viewBinding(DialogClickActionConfigBinding::bind)
|
|
||||||
private val actions by lazy {
|
|
||||||
linkedMapOf(
|
|
||||||
Pair(-1, getString(R.string.non_action)),
|
|
||||||
Pair(0, getString(R.string.menu)),
|
|
||||||
Pair(1, getString(R.string.next_page)),
|
|
||||||
Pair(2, getString(R.string.prev_page)),
|
|
||||||
Pair(3, getString(R.string.next_chapter)),
|
|
||||||
Pair(4, getString(R.string.previous_chapter)),
|
|
||||||
Pair(5, getString(R.string.read_aloud_prev_paragraph)),
|
|
||||||
Pair(6, getString(R.string.read_aloud_next_paragraph)),
|
|
||||||
Pair(7, getString(R.string.bookmark_add)),
|
|
||||||
Pair(8, getString(R.string.edit_content)),
|
|
||||||
Pair(9, getString(R.string.replace_state_change)),
|
|
||||||
Pair(10, getString(R.string.chapter_list)),
|
|
||||||
Pair(11, getString(R.string.search_content)),
|
|
||||||
Pair(12, getString(R.string.sync_book_progress_t)),
|
|
||||||
Pair(13, getString(R.string.read_aloud_pause_resume))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
dialog?.window?.run {
|
|
||||||
setBackgroundDrawableResource(R.color.transparent)
|
|
||||||
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as ReadBookActivity).bottomDialog++
|
|
||||||
view.setBackgroundColor(getCompatColor(R.color.translucent))
|
|
||||||
initData()
|
|
||||||
initViewEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() = binding.run {
|
|
||||||
tvTopLeft.text = actions[AppConfig.clickActionTL]
|
|
||||||
tvTopCenter.text = actions[AppConfig.clickActionTC]
|
|
||||||
tvTopRight.text = actions[AppConfig.clickActionTR]
|
|
||||||
tvMiddleLeft.text = actions[AppConfig.clickActionML]
|
|
||||||
tvMiddleCenter.text = actions[AppConfig.clickActionMC]
|
|
||||||
tvMiddleRight.text = actions[AppConfig.clickActionMR]
|
|
||||||
tvBottomLeft.text = actions[AppConfig.clickActionBL]
|
|
||||||
tvBottomCenter.text = actions[AppConfig.clickActionBC]
|
|
||||||
tvBottomRight.text = actions[AppConfig.clickActionBR]
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initViewEvent() {
|
|
||||||
binding.ivClose.setOnClickListener {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
binding.tvTopLeft.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionTL, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvTopCenter.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionTC, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvTopRight.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionTR, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvMiddleLeft.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionML, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvMiddleCenter.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionMC, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvMiddleRight.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionMR, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvBottomLeft.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionBL, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvBottomCenter.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionBC, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.tvBottomRight.setOnClickListener {
|
|
||||||
selectAction { action ->
|
|
||||||
putPrefInt(PreferKey.clickActionBR, action)
|
|
||||||
(it as? TextView)?.text = actions[action]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun selectAction(success: (action: Int) -> Unit) {
|
|
||||||
context?.selector(
|
|
||||||
getString(R.string.select_action),
|
|
||||||
actions.values.toList()
|
|
||||||
) { _, index ->
|
|
||||||
success.invoke(actions.keys.toList()[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDestroy() {
|
|
||||||
super.onDestroy()
|
|
||||||
AppConfig.detectClickArea()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.DashPathEffect
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 虚线下划线 Span
|
||||||
|
*/
|
||||||
|
class DashUnderlineSpan(
|
||||||
|
private val textColor: Int,
|
||||||
|
private val underlineColor: Int,
|
||||||
|
private val underlineWidth: Float = 1f,
|
||||||
|
private val underlineOffset: Float = 6f,
|
||||||
|
) : ReplacementSpan() {
|
||||||
|
|
||||||
|
private val offsetPx = underlineOffset.toInt().dpToPx()
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
if (fm != null) {
|
||||||
|
val metrics = paint.fontMetricsInt
|
||||||
|
fm.top = metrics.top
|
||||||
|
fm.ascent = metrics.ascent
|
||||||
|
fm.descent = metrics.descent + offsetPx
|
||||||
|
fm.bottom = metrics.bottom + offsetPx
|
||||||
|
}
|
||||||
|
return paint.measureText(text, start, end).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(
|
||||||
|
canvas: Canvas,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
x: Float,
|
||||||
|
top: Int,
|
||||||
|
y: Int,
|
||||||
|
bottom: Int,
|
||||||
|
paint: Paint
|
||||||
|
) {
|
||||||
|
val textStr = text.subSequence(start, end).toString()
|
||||||
|
paint.color = textColor
|
||||||
|
canvas.drawText(textStr, x, y.toFloat(), paint)
|
||||||
|
|
||||||
|
val width = paint.measureText(text, start, end)
|
||||||
|
val lineY = y + offsetPx
|
||||||
|
val dashPaint = Paint(paint).apply {
|
||||||
|
color = underlineColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = underlineWidth.dpToPx()
|
||||||
|
pathEffect = DashPathEffect(floatArrayOf(10f, 10f), 0f)
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawLine(x, lineY.toFloat(), x + width, lineY.toFloat(), dashPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双线下划线 Span
|
||||||
|
*/
|
||||||
|
class DoubleUnderlineSpan(
|
||||||
|
private val textColor: Int,
|
||||||
|
private val underlineColor: Int,
|
||||||
|
private val underlineWidth: Float = 1f,
|
||||||
|
private val underlineOffset: Float = 6f,
|
||||||
|
) : ReplacementSpan() {
|
||||||
|
|
||||||
|
private val offsetPx = underlineOffset.toInt().dpToPx()
|
||||||
|
private val lineGap = 3.dpToPx()
|
||||||
|
private val widthPx = underlineWidth.toInt().dpToPx()
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
if (fm != null) {
|
||||||
|
val metrics = paint.fontMetricsInt
|
||||||
|
fm.top = metrics.top
|
||||||
|
fm.ascent = metrics.ascent
|
||||||
|
fm.descent = metrics.descent + offsetPx + lineGap + widthPx
|
||||||
|
fm.bottom = metrics.bottom + offsetPx + lineGap + widthPx
|
||||||
|
}
|
||||||
|
return paint.measureText(text, start, end).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(
|
||||||
|
canvas: Canvas,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
x: Float,
|
||||||
|
top: Int,
|
||||||
|
y: Int,
|
||||||
|
bottom: Int,
|
||||||
|
paint: Paint
|
||||||
|
) {
|
||||||
|
val textStr = text.subSequence(start, end).toString()
|
||||||
|
paint.color = textColor
|
||||||
|
canvas.drawText(textStr, x, y.toFloat(), paint)
|
||||||
|
|
||||||
|
val width = paint.measureText(text, start, end)
|
||||||
|
val line1Y = y + offsetPx
|
||||||
|
val line2Y = line1Y + lineGap + widthPx
|
||||||
|
val linePaint = Paint(paint).apply {
|
||||||
|
color = underlineColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = underlineWidth.dpToPx()
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawLine(x, line1Y.toFloat(), x + width, line1Y.toFloat(), linePaint)
|
||||||
|
canvas.drawLine(x, line2Y.toFloat(), x + width, line2Y.toFloat(), linePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogFontConfigBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig.underline
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字体选择对话框
|
|
||||||
*/
|
|
||||||
class FontConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_config) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val S_COLOR = 123
|
|
||||||
const val TEXT_COLOR = 121
|
|
||||||
const val TEXT_ACCENT_COLOR = 125
|
|
||||||
}
|
|
||||||
private val binding by viewBinding(DialogFontConfigBinding::bind)
|
|
||||||
|
|
||||||
private val callBack2 get() = activity as? ReadBookActivity
|
|
||||||
private val weightIconMap = mapOf(
|
|
||||||
0 to R.drawable.ic_text_weight_0,
|
|
||||||
1 to R.drawable.ic_text_weight_1,
|
|
||||||
2 to R.drawable.ic_text_weight_2,
|
|
||||||
)
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) { list ->
|
|
||||||
if (list.contains(2)) {
|
|
||||||
binding.btnTextColor.color = ReadBookConfig.durConfig.curTextColor()
|
|
||||||
binding.btnShadowColor.color = ReadBookConfig.durConfig.curTextShadowColor()
|
|
||||||
binding.btnTextAccentColor.color = ReadBookConfig.durConfig.curTextAccentColor()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
initView()
|
|
||||||
upView()
|
|
||||||
initViewEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
binding.btnTextColor.color = ReadBookConfig.durConfig.curTextColor()
|
|
||||||
binding.btnShadowColor.color = ReadBookConfig.durConfig.curTextShadowColor()
|
|
||||||
binding.btnTextAccentColor.color = ReadBookConfig.durConfig.curTextAccentColor()
|
|
||||||
binding.swUnderline.isChecked = underline
|
|
||||||
dsbTextLetterSpacing.valueFormat = {
|
|
||||||
((it - 50) / 100f).toString()
|
|
||||||
}
|
|
||||||
dsbLineSize.valueFormat = { ((it - 10) / 10f).toString() }
|
|
||||||
binding.dsbParagraphSpacing.valueFormat = { value ->
|
|
||||||
(value / 10f).toString()
|
|
||||||
}
|
|
||||||
binding.btnIndentLayout.apply {
|
|
||||||
valueFormat = { value ->
|
|
||||||
value.toString()
|
|
||||||
}
|
|
||||||
onChanged = { value ->
|
|
||||||
val indentCount = value.coerceIn(0, 4)
|
|
||||||
ReadBookConfig.paragraphIndent = " ".repeat(indentCount)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
progress = ReadBookConfig.paragraphIndent.length
|
|
||||||
}
|
|
||||||
|
|
||||||
val weightOptions = context?.resources?.getStringArray(R.array.text_font_weight)
|
|
||||||
val weightValues = listOf(0, 1, 2)
|
|
||||||
val initialIndex = weightValues.indexOf(ReadBookConfig.textBold)
|
|
||||||
val initialIconRes = weightIconMap[initialIndex] ?: R.drawable.ic_custom_text
|
|
||||||
binding.textFontWeightConverter.setIconResource(initialIconRes)
|
|
||||||
binding.textFontWeightConverter.setOnClickListener {
|
|
||||||
context?.alert(titleResource = R.string.text_font_weight_converter) {
|
|
||||||
weightOptions?.let { options ->
|
|
||||||
items(options.toList()) { _, i ->
|
|
||||||
ReadBookConfig.textBold = weightValues[i]
|
|
||||||
binding.sliderFontWeight.progress =
|
|
||||||
ReadBookConfig.textBold.coerceAtLeast(100)
|
|
||||||
val iconRes = weightIconMap[i] ?: R.drawable.ic_custom_text
|
|
||||||
binding.textFontWeightConverter.setIconResource(iconRes)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.sliderFontWeight.apply {
|
|
||||||
min = 100
|
|
||||||
max = 900
|
|
||||||
progress = ReadBookConfig.textBold.coerceAtLeast(100)
|
|
||||||
onChanged = {
|
|
||||||
binding.textFontWeightConverter.setIconResource(R.drawable.ic_custom_text)
|
|
||||||
ReadBookConfig.textBold = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnShadowSet.setOnClickListener {
|
|
||||||
callBack2?.showShadowSet()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnRegexColor.setOnClickListener {
|
|
||||||
RegexColorConfigDialog().show(childFragmentManager, "regexColorConfig")
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnSelectFonts.setOnClickListener {
|
|
||||||
callBack2?.showFontSelect()
|
|
||||||
}
|
|
||||||
binding.btnTextItalic.isChecked = ReadBookConfig.textItalic
|
|
||||||
binding.btnTextShadow.isChecked = ReadBookConfig.textShadow
|
|
||||||
binding.btnShadowColor.color = ReadBookConfig.textShadowColor
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initViewEvent() = binding.run {
|
|
||||||
dsbTextLetterSpacing.onChanged = {
|
|
||||||
ReadBookConfig.letterSpacing = (it - 50) / 100f
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
dsbLineSize.onChanged = {
|
|
||||||
ReadBookConfig.lineSpacingExtra = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
binding.btnTextColor.setOnClickListener {
|
|
||||||
ColorPickerDialog.newBuilder()
|
|
||||||
.setColor(ReadBookConfig.durConfig.curTextColor())
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(TEXT_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
binding.btnTextAccentColor.setOnClickListener {
|
|
||||||
ColorPickerDialog.newBuilder()
|
|
||||||
.setColor(ReadBookConfig.durConfig.curTextAccentColor())
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(TEXT_ACCENT_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
binding.swUnderline.addOnCheckedChangeListener { _, isChecked ->
|
|
||||||
callBack2?.showUnderlineConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnDefaultFonts.setOnClickListener {
|
|
||||||
val requireContext = requireContext()
|
|
||||||
alert(titleResource = R.string.system_typeface) {
|
|
||||||
items(
|
|
||||||
requireContext.resources.getStringArray(R.array.system_typefaces).toList()
|
|
||||||
) { _, i ->
|
|
||||||
AppConfig.systemTypefaces = i
|
|
||||||
onDefaultFontChange()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.dsbParagraphSpacing.onChanged = { value ->
|
|
||||||
ReadBookConfig.paragraphSpacing = value
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnTextItalic.addOnCheckedChangeListener { _, isChecked ->
|
|
||||||
ReadBookConfig.textItalic = isChecked
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
binding.btnTextShadow.addOnCheckedChangeListener { _, isChecked ->
|
|
||||||
ReadBookConfig.textShadow = isChecked
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
binding.btnShadowColor.setOnClickListener {
|
|
||||||
ColorPickerDialog.newBuilder()
|
|
||||||
.setColor(ReadBookConfig.config.curTextShadowColor())
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(S_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
//postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upView() = binding.run {
|
|
||||||
ReadBookConfig.let {
|
|
||||||
dsbTextLetterSpacing.progress = (it.letterSpacing * 100).toInt() + 50
|
|
||||||
dsbLineSize.progress = it.lineSpacingExtra
|
|
||||||
dsbParagraphSpacing.progress = it.paragraphSpacing
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDefaultFontChange() {
|
|
||||||
callBack?.selectFont("")
|
|
||||||
}
|
|
||||||
|
|
||||||
private val callBack: CallBack?
|
|
||||||
get() = (parentFragment as? CallBack) ?: (activity as? CallBack)
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun selectFont(path: String)
|
|
||||||
val curFontPath: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.graphics.Typeface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.net.toUri
|
|
||||||
import androidx.core.widget.addTextChangedListener
|
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.DialogFontSelectBinding
|
|
||||||
import io.legado.app.databinding.ItemFontBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.lib.permission.Permissions
|
|
||||||
import io.legado.app.lib.permission.PermissionsCompat
|
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
|
||||||
import io.legado.app.utils.FileDoc
|
|
||||||
import io.legado.app.utils.FileUtils
|
|
||||||
import io.legado.app.utils.RealPathUtil
|
|
||||||
import io.legado.app.utils.cnCompare
|
|
||||||
import io.legado.app.utils.externalFiles
|
|
||||||
import io.legado.app.utils.getPrefString
|
|
||||||
import io.legado.app.utils.invisible
|
|
||||||
import io.legado.app.utils.isContentScheme
|
|
||||||
import io.legado.app.utils.list
|
|
||||||
import io.legado.app.utils.listFileDocs
|
|
||||||
import io.legado.app.utils.printOnDebug
|
|
||||||
import io.legado.app.utils.putPrefString
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
import java.io.File
|
|
||||||
import java.net.URLDecoder
|
|
||||||
|
|
||||||
class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_select) {
|
|
||||||
|
|
||||||
private val fontRegex = Regex("(?i).*\\.[ot]tf")
|
|
||||||
private val binding by viewBinding(DialogFontSelectBinding::bind)
|
|
||||||
private val adapter by lazy {
|
|
||||||
val curFontPath = callBack?.curFontPath ?: ""
|
|
||||||
FontAdapter(requireContext(), curFontPath)
|
|
||||||
}
|
|
||||||
private var allFontItems: List<FileDoc> = emptyList()
|
|
||||||
|
|
||||||
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
if (uri.isContentScheme()) {
|
|
||||||
putPrefString(PreferKey.fontFolder, uri.toString())
|
|
||||||
val doc = DocumentFile.fromTreeUri(requireContext(), uri)
|
|
||||||
if (doc != null) {
|
|
||||||
loadFontFiles(FileDoc.fromDocumentFile(doc))
|
|
||||||
} else {
|
|
||||||
RealPathUtil.getPath(requireContext(), uri)?.let { path ->
|
|
||||||
loadFontFilesByPermission(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
uri.path?.let { path ->
|
|
||||||
putPrefString(PreferKey.fontFolder, path)
|
|
||||||
loadFontFilesByPermission(path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
binding.recyclerView.layoutManager = GridLayoutManager(context, 2)
|
|
||||||
binding.recyclerView.adapter = adapter
|
|
||||||
initView()
|
|
||||||
initSearch()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initSearch() {
|
|
||||||
binding.etSearch.addTextChangedListener {
|
|
||||||
filterFonts(it.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun filterFonts(keyword: String) {
|
|
||||||
val filtered = if (keyword.isBlank()) {
|
|
||||||
allFontItems
|
|
||||||
} else {
|
|
||||||
allFontItems.filter { it.name.contains(keyword, ignoreCase = true) }
|
|
||||||
}
|
|
||||||
adapter.setItems(filtered)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
val fontPath = getPrefString(PreferKey.fontFolder)
|
|
||||||
if (fontPath.isNullOrEmpty()) {
|
|
||||||
openFolder()
|
|
||||||
} else {
|
|
||||||
if (fontPath.isContentScheme()) {
|
|
||||||
val doc = DocumentFile.fromTreeUri(requireContext(), fontPath.toUri())
|
|
||||||
if (doc?.canRead() == true) {
|
|
||||||
loadFontFiles(FileDoc.fromDocumentFile(doc))
|
|
||||||
} else {
|
|
||||||
openFolder()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
loadFontFilesByPermission(fontPath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
binding.btnOtherDir.setOnClickListener {
|
|
||||||
openFolder()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun openFolder() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
val defaultPath = "SD${File.separator}Fonts"
|
|
||||||
selectFontDir.launch {
|
|
||||||
otherActions = arrayListOf(SelectItem(defaultPath, -1))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLocalFonts(): ArrayList<FileDoc> {
|
|
||||||
val path = FileUtils.getPath(requireContext().externalFiles, "font")
|
|
||||||
return File(path).listFileDocs {
|
|
||||||
it.name.matches(fontRegex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFontFilesByPermission(path: String) {
|
|
||||||
PermissionsCompat.Builder()
|
|
||||||
.addPermissions(*Permissions.Group.STORAGE)
|
|
||||||
.rationale(R.string.tip_perm_request_storage)
|
|
||||||
.onGranted {
|
|
||||||
loadFontFiles(
|
|
||||||
FileDoc.fromFile(File(path))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
.request()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun loadFontFiles(fileDoc: FileDoc) {
|
|
||||||
execute {
|
|
||||||
val fontItems = fileDoc.list {
|
|
||||||
it.name.matches(fontRegex)
|
|
||||||
} ?: ArrayList()
|
|
||||||
mergeFontItems(fontItems, getLocalFonts())
|
|
||||||
}.onSuccess {
|
|
||||||
allFontItems = it
|
|
||||||
adapter.setItems(it)
|
|
||||||
}.onError {
|
|
||||||
AppLog.put("加载字体文件失败\n${it.localizedMessage}", it)
|
|
||||||
toastOnUi("getFontFiles:${it.localizedMessage}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun mergeFontItems(
|
|
||||||
items1: ArrayList<FileDoc>,
|
|
||||||
items2: ArrayList<FileDoc>
|
|
||||||
): List<FileDoc> {
|
|
||||||
val items = ArrayList(items1)
|
|
||||||
items2.forEach { item2 ->
|
|
||||||
var isInFirst = false
|
|
||||||
items1.forEach for1@{ item1 ->
|
|
||||||
if (item2.name == item1.name) {
|
|
||||||
isInFirst = true
|
|
||||||
return@for1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!isInFirst) {
|
|
||||||
items.add(item2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return items.sortedWith { o1, o2 ->
|
|
||||||
o1.name.cnCompare(o2.name)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onFontSelect(docItem: FileDoc) {
|
|
||||||
execute {
|
|
||||||
callBack?.selectFont(docItem.toString())
|
|
||||||
}.onSuccess {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun onDefaultFontChange() {
|
|
||||||
callBack?.selectFont("")
|
|
||||||
}
|
|
||||||
|
|
||||||
var explicitCallback: CallBack? = null
|
|
||||||
|
|
||||||
private val callBack: CallBack?
|
|
||||||
get() = explicitCallback ?: (parentFragment as? CallBack) ?: (activity as? CallBack)
|
|
||||||
|
|
||||||
inner class FontAdapter(context: Context, curFilePath: String) :
|
|
||||||
RecyclerAdapter<FileDoc, ItemFontBinding>(context) {
|
|
||||||
|
|
||||||
private val curName = runCatching {
|
|
||||||
URLDecoder.decode(curFilePath, "utf-8")
|
|
||||||
}.getOrNull()?.substringAfterLast(File.separator)
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemFontBinding {
|
|
||||||
return ItemFontBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemFontBinding,
|
|
||||||
item: FileDoc,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.run {
|
|
||||||
runCatching {
|
|
||||||
val typeface: Typeface? = if (item.isContentScheme) {
|
|
||||||
context.contentResolver
|
|
||||||
.openFileDescriptor(item.uri, "r")?.use {
|
|
||||||
Typeface.Builder(it.fileDescriptor).build()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Typeface.createFromFile(item.uri.path!!)
|
|
||||||
}
|
|
||||||
tvFont.typeface = typeface
|
|
||||||
}.onFailure {
|
|
||||||
it.printOnDebug()
|
|
||||||
AppLog.put("读取字体 ${item.name} 出错\n${it.localizedMessage}", it, true)
|
|
||||||
}
|
|
||||||
tvFont.text = item.name
|
|
||||||
root.setOnClickListener { onFontSelect(item) }
|
|
||||||
if (item.name == curName) {
|
|
||||||
ivChecked.visible()
|
|
||||||
} else {
|
|
||||||
ivChecked.invisible()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemFontBinding) {
|
|
||||||
holder.itemView.setOnClickListener {
|
|
||||||
getItem(holder.layoutPosition)?.let {
|
|
||||||
onFontSelect(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun selectFont(path: String)
|
|
||||||
val curFontPath: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.HighlightRule
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.fromJsonArray
|
||||||
|
import io.legado.app.utils.getPrefBoolean
|
||||||
|
import io.legado.app.utils.getPrefString
|
||||||
|
import io.legado.app.utils.putPrefBoolean
|
||||||
|
import io.legado.app.utils.putPrefString
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
object HighlightRuleStore {
|
||||||
|
|
||||||
|
const val backupFileName = "highlightRule.json"
|
||||||
|
|
||||||
|
data class BackupData(
|
||||||
|
val rules: List<HighlightRule> = emptyList(),
|
||||||
|
val dialogEnabled: Boolean = true,
|
||||||
|
val bookTitleEnabled: Boolean = true,
|
||||||
|
val bracketNoteEnabled: Boolean = true,
|
||||||
|
)
|
||||||
|
|
||||||
|
private val dao get() = appDb.highlightRuleDao
|
||||||
|
|
||||||
|
fun load(): List<HighlightRule> {
|
||||||
|
migrateFromPrefsIfNeeded()
|
||||||
|
return dao.getAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadEnabled(): List<HighlightRule> {
|
||||||
|
migrateFromPrefsIfNeeded()
|
||||||
|
return dao.getEnabled()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun save(rules: List<HighlightRule>) {
|
||||||
|
val sanitized = rules.mapIndexed { index, rule ->
|
||||||
|
sanitizeRule(rule).copy(position = index)
|
||||||
|
}
|
||||||
|
dao.replaceAll(sanitized)
|
||||||
|
cleanupUnusedBgImages(sanitized)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun update(rule: HighlightRule) {
|
||||||
|
dao.update(sanitizeRule(rule))
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delete(rule: HighlightRule) {
|
||||||
|
dao.delete(rule)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reset(): List<HighlightRule> {
|
||||||
|
val defaults = createDefaultRules()
|
||||||
|
dao.replaceAll(defaults)
|
||||||
|
return defaults
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createBackupData(): BackupData {
|
||||||
|
return BackupData(
|
||||||
|
rules = load(),
|
||||||
|
dialogEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleDialog, true),
|
||||||
|
bookTitleEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleBookTitle, true),
|
||||||
|
bracketNoteEnabled = appCtx.getPrefBoolean(PreferKey.highlightRuleBracketNote, true),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun restoreBackupData(backupData: BackupData, backupRootPath: String? = null) {
|
||||||
|
val rules = backupData.rules.map { rule ->
|
||||||
|
val safeRule = sanitizeRule(rule)
|
||||||
|
val restoredBgImage = restoreRuleBgImage(backupRootPath, safeRule.bgImage)
|
||||||
|
safeRule.copy(bgImage = restoredBgImage)
|
||||||
|
}
|
||||||
|
save(rules)
|
||||||
|
appCtx.putPrefBoolean(PreferKey.highlightRuleDialog, backupData.dialogEnabled)
|
||||||
|
appCtx.putPrefBoolean(PreferKey.highlightRuleBookTitle, backupData.bookTitleEnabled)
|
||||||
|
appCtx.putPrefBoolean(PreferKey.highlightRuleBracketNote, backupData.bracketNoteEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 从旧版 SharedPreferences 迁移数据(一次性)
|
||||||
|
*/
|
||||||
|
private fun migrateFromPrefsIfNeeded() {
|
||||||
|
if (dao.count() > 0) return
|
||||||
|
// 尝试从 SharedPreferences 读取旧数据
|
||||||
|
val stored = appCtx.getPrefString(PreferKey.highlightRuleItems)
|
||||||
|
if (!stored.isNullOrBlank()) {
|
||||||
|
val oldRules = GSON.fromJsonArray<LegacyHighlightRule>(stored).getOrNull()
|
||||||
|
if (!oldRules.isNullOrEmpty()) {
|
||||||
|
val migrated = oldRules.mapIndexed { index, old ->
|
||||||
|
sanitizeRule(
|
||||||
|
HighlightRule(
|
||||||
|
id = old.id,
|
||||||
|
name = old.name,
|
||||||
|
pattern = old.pattern,
|
||||||
|
sampleText = old.sampleText,
|
||||||
|
targetScope = old.targetScope,
|
||||||
|
enabled = old.enabled,
|
||||||
|
position = index,
|
||||||
|
textColor = old.textColor,
|
||||||
|
underlineMode = old.underlineMode,
|
||||||
|
underlineColor = old.underlineColor,
|
||||||
|
underlineWidth = old.underlineWidth,
|
||||||
|
underlineOffset = old.underlineOffset,
|
||||||
|
underlineSvgPath = old.underlineSvgPath,
|
||||||
|
bgImage = old.bgImage,
|
||||||
|
bgImageFit = old.bgImageFit,
|
||||||
|
bgImageScale = old.bgImageScale,
|
||||||
|
)
|
||||||
|
).copy(position = index)
|
||||||
|
}
|
||||||
|
dao.insertAll(migrated)
|
||||||
|
// 清除旧 SharedPreferences 数据
|
||||||
|
appCtx.putPrefString(PreferKey.highlightRuleItems, null)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 尝试从旧版 RegexColorRule 迁移
|
||||||
|
migrateFromRegexColorRules()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun migrateFromRegexColorRules() {
|
||||||
|
val oldRules = ReadBookConfig.regexColorRules
|
||||||
|
if (oldRules.isEmpty()) return
|
||||||
|
val migrated = oldRules.mapIndexed { index, old ->
|
||||||
|
HighlightRule(
|
||||||
|
name = old.name,
|
||||||
|
pattern = old.pattern,
|
||||||
|
position = index,
|
||||||
|
textColor = old.color,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
dao.insertAll(migrated)
|
||||||
|
oldRules.clear()
|
||||||
|
ReadBookConfig.save()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sanitizeRule(rule: HighlightRule): HighlightRule {
|
||||||
|
val name = runCatching { rule.name }.getOrNull().orEmpty()
|
||||||
|
val pattern = runCatching { rule.pattern }.getOrNull().orEmpty()
|
||||||
|
val sampleText = runCatching { rule.sampleText }.getOrNull().orEmpty()
|
||||||
|
val id = runCatching { rule.id }.getOrNull().orEmpty().ifBlank {
|
||||||
|
"${System.currentTimeMillis()}_${listOf(name, pattern).joinToString("|").hashCode().toUInt().toString(16)}"
|
||||||
|
}
|
||||||
|
return HighlightRule(
|
||||||
|
id = id,
|
||||||
|
name = name,
|
||||||
|
pattern = pattern,
|
||||||
|
sampleText = sampleText,
|
||||||
|
targetScope = normalizeTargetScope(runCatching { rule.targetScope }.getOrDefault(HighlightRule.TARGET_ALL)),
|
||||||
|
enabled = runCatching { rule.enabled }.getOrDefault(true),
|
||||||
|
position = runCatching { rule.position }.getOrDefault(0),
|
||||||
|
textColor = runCatching { rule.textColor }.getOrNull(),
|
||||||
|
bgColor = runCatching { rule.bgColor }.getOrNull(),
|
||||||
|
underlineMode = runCatching { rule.underlineMode }.getOrDefault(0).coerceIn(0, 5),
|
||||||
|
underlineColor = runCatching { rule.underlineColor }.getOrNull(),
|
||||||
|
underlineWidth = runCatching { rule.underlineWidth }.getOrDefault(1f).coerceIn(0.1f, 10f),
|
||||||
|
underlineOffset = runCatching { rule.underlineOffset }.getOrDefault(2f).coerceIn(0f, 20f),
|
||||||
|
underlineSvgPath = runCatching { rule.underlineSvgPath }.getOrNull(),
|
||||||
|
bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
|
||||||
|
bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2),
|
||||||
|
bgImageScale = runCatching { rule.bgImageScale }.getOrDefault(1f).coerceIn(0.1f, 5f),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun normalizeTargetScope(value: Int, fallback: Int = HighlightRule.TARGET_ALL): Int {
|
||||||
|
return when (value) {
|
||||||
|
HighlightRule.TARGET_ALL,
|
||||||
|
HighlightRule.TARGET_TITLE,
|
||||||
|
HighlightRule.TARGET_BODY -> value
|
||||||
|
else -> fallback
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createDefaultRules(): List<HighlightRule> {
|
||||||
|
val ctx = appCtx
|
||||||
|
return listOf(
|
||||||
|
HighlightRule(
|
||||||
|
id = "dialog_default",
|
||||||
|
name = "对话高亮",
|
||||||
|
pattern = "“[^\\u201d\\n]{1,120}\\u201d|\"[^\"\\n]{1,120}\"|「[^」\\n]{1,120}」|『[^』\\n]{1,120}』",
|
||||||
|
sampleText = "她轻声说:“今晚就出发。”",
|
||||||
|
position = 0,
|
||||||
|
enabled = ctx.getPrefBoolean(PreferKey.highlightRuleDialog, true),
|
||||||
|
textColor = 0xFFFF8C00.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "book_title_default",
|
||||||
|
name = "书名号高亮",
|
||||||
|
pattern = "《[^》\\n]{1,80}》",
|
||||||
|
sampleText = "最近在重读《百年孤独》,节奏依然很稳。",
|
||||||
|
position = 1,
|
||||||
|
enabled = ctx.getPrefBoolean(PreferKey.highlightRuleBookTitle, true),
|
||||||
|
underlineMode = 3,
|
||||||
|
underlineWidth = 0.5f,
|
||||||
|
underlineColor = 0xFF63C37D.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "bracket_note_default",
|
||||||
|
name = "括号标注高亮",
|
||||||
|
pattern = "([^()\\n]{1,80})|\\([^()\\n]{1,80}\\)|【[^】\\n]{1,80}】|\\[[^\\]\\n]{1,80}]",
|
||||||
|
sampleText = "他停了一下(像是忽然想起了什么)。",
|
||||||
|
position = 2,
|
||||||
|
enabled = ctx.getPrefBoolean(PreferKey.highlightRuleBracketNote, true),
|
||||||
|
textColor = 0xFF8F959E.toInt(),
|
||||||
|
underlineMode = 2,
|
||||||
|
underlineWidth = 0.5f,
|
||||||
|
underlineColor = 0xFF5A8DEE.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "title_emphasis_default",
|
||||||
|
name = "标题强调",
|
||||||
|
pattern = "(?m)^\\s{0,2}(?:第[0-9零〇一二两三四五六七八九十百千万IVXLCDMivxlcdm]{1,12}[章节卷回部篇集幕]|序章|楔子|引子|终章|尾声|后记|番外)[^\\n]{0,40}$",
|
||||||
|
sampleText = "第一章 雨夜来客",
|
||||||
|
targetScope = HighlightRule.TARGET_TITLE,
|
||||||
|
position = 3,
|
||||||
|
enabled = true,
|
||||||
|
textColor = 0xFF333333.toInt(),
|
||||||
|
underlineMode = 4,
|
||||||
|
underlineColor = 0xFF7C5634.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "thought_default",
|
||||||
|
name = "心理活动",
|
||||||
|
pattern = "([^)\\n]{0,40}(?:心想|暗道|心道|想到|寻思着|琢磨|嘀咕)[^)\\n]{0,40})",
|
||||||
|
sampleText = "她心中一紧(暗道不对,这里一定有问题)。",
|
||||||
|
position = 4,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF9370DB.toInt(),
|
||||||
|
underlineMode = 1,
|
||||||
|
underlineWidth = 0.5f,
|
||||||
|
underlineColor = 0xFF9370DB.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "narrator_default",
|
||||||
|
name = "旁白说明",
|
||||||
|
pattern = "(?:未完待续|待续|下文再表|按:?|注:?)[^\\n]{0,40}|((?:注|旁白|作者有话说)[::][^)\\n]{0,40})",
|
||||||
|
sampleText = "(注:此处时间线与前文同步)",
|
||||||
|
position = 5,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF708090.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "emphasis_default",
|
||||||
|
name = "重点强调",
|
||||||
|
pattern = "(?:\\*\\*|__)[^\\n*_]{1,40}(?:\\*\\*|__)|(?:!!!|!?|\\?!)[^\\n]{0,20}",
|
||||||
|
sampleText = "**这是重点内容**,需要特别注意。",
|
||||||
|
position = 6,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFFDC143C.toInt(),
|
||||||
|
underlineMode = 1,
|
||||||
|
underlineColor = 0xFFDC143C.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "poetry_default",
|
||||||
|
name = "诗词引用",
|
||||||
|
pattern = "(?m)^[\\p{IsHan},。!?;:、]{5,24}$",
|
||||||
|
sampleText = "床前明月光,\n疑是地上霜。",
|
||||||
|
position = 7,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF2F4F4F.toInt(),
|
||||||
|
underlineMode = 3,
|
||||||
|
underlineWidth = 0.5f,
|
||||||
|
underlineColor = 0xFF2F4F4F.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "ellipsis_default",
|
||||||
|
name = "省略停顿",
|
||||||
|
pattern = "…{2,}|\\.{3,}|—{2,}|-{3,}",
|
||||||
|
sampleText = "他沉默了很久……最后还是点了头。",
|
||||||
|
position = 8,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF8B8B8B.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "number_default",
|
||||||
|
name = "数字金额",
|
||||||
|
pattern = "(?:¥|¥)?\\d+(?:\\.\\d+)?(?:元|块|万|千|百|亿|%|%)|[零〇一二两三四五六七八九十百千万亿]+(?:元|块|万|千|百|亿)",
|
||||||
|
sampleText = "原价100元,现在只要50元。",
|
||||||
|
position = 9,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF4169E1.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "english_default",
|
||||||
|
name = "英文单词",
|
||||||
|
pattern = "\\b[A-Za-z]{2,}[A-Za-z0-9'-]*\\b",
|
||||||
|
sampleText = "Hello World,你好世界。",
|
||||||
|
position = 10,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF4169E1.toInt()
|
||||||
|
),
|
||||||
|
HighlightRule(
|
||||||
|
id = "date_time_default",
|
||||||
|
name = "时间日期",
|
||||||
|
pattern = "(?:\\d{2,4}|[零〇一二两三四五六七八九十]{2,4})年(?:\\d{1,2}|[正一二三四五六七八九十冬腊])月(?:\\d{1,2}|[一二三四五六七八九十廿三])?[日号]?|\\b\\d{1,2}:\\d{2}\\b|(?:[0-1]?\\d|2[0-3])点(?:[0-5]?\\d分?)?",
|
||||||
|
sampleText = "2024年8月12日,上午10:30出发。",
|
||||||
|
position = 11,
|
||||||
|
enabled = false,
|
||||||
|
textColor = 0xFF20B2AA.toInt()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cleanupUnusedBgImages(rules: List<HighlightRule>) {
|
||||||
|
val usedPaths = rules.mapNotNull { it.bgImage }
|
||||||
|
.filter { it.isNotBlank() && !it.startsWith("assets://") }
|
||||||
|
.toSet()
|
||||||
|
val dir = File(appCtx.filesDir, "bg_images")
|
||||||
|
if (!dir.exists()) return
|
||||||
|
dir.listFiles()?.forEach { file ->
|
||||||
|
if (file.absolutePath !in usedPaths) {
|
||||||
|
runCatching { file.delete() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreRuleBgImage(backupRootPath: String?, bgImage: String?): String? {
|
||||||
|
val path = bgImage ?: return null
|
||||||
|
if (path.isBlank() || path.startsWith("assets://")) return path
|
||||||
|
val rootPath = backupRootPath ?: return path
|
||||||
|
val backupFile = File(rootPath, "highlightRuleBg${File.separator}${File(path).name}")
|
||||||
|
.takeIf { it.exists() && it.isFile }
|
||||||
|
?: return path
|
||||||
|
val dir = File(appCtx.filesDir, "bg_images")
|
||||||
|
if (!dir.exists()) dir.mkdirs()
|
||||||
|
val targetFile = File(dir, backupFile.name)
|
||||||
|
if (!targetFile.exists() || targetFile.length() != backupFile.length()) {
|
||||||
|
backupFile.copyTo(targetFile, overwrite = true)
|
||||||
|
}
|
||||||
|
return targetFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 旧版 SharedPreferences 数据结构(用于迁移)
|
||||||
|
*/
|
||||||
|
private data class LegacyHighlightRule(
|
||||||
|
val id: String = "",
|
||||||
|
val name: String = "",
|
||||||
|
val pattern: String = "",
|
||||||
|
val sampleText: String = "",
|
||||||
|
val targetScope: Int = 0,
|
||||||
|
val enabled: Boolean = true,
|
||||||
|
val textColor: Int? = null,
|
||||||
|
val underlineMode: Int = 0,
|
||||||
|
val underlineColor: Int? = null,
|
||||||
|
val underlineWidth: Float = 1f,
|
||||||
|
val underlineOffset: Float = 2f,
|
||||||
|
val underlineSvgPath: String? = null,
|
||||||
|
val bgImage: String? = null,
|
||||||
|
val bgImageFit: Int = 0,
|
||||||
|
val bgImageScale: Float = 1f,
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.text.TextPaint
|
||||||
|
import android.text.style.CharacterStyle
|
||||||
|
import android.text.style.UpdateAppearance
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 用于在阅读排版阶段传递局部下划线样式
|
||||||
|
*/
|
||||||
|
class HighlightStyleSpan(
|
||||||
|
val underlineMode: Int,
|
||||||
|
val underlineColor: Int,
|
||||||
|
val underlineWidth: Float = 1f,
|
||||||
|
val underlineOffset: Float = 2f,
|
||||||
|
val underlineSvgPath: String = "",
|
||||||
|
val bgImage: String = "",
|
||||||
|
val bgImageFit: Int = 0,
|
||||||
|
val bgImageScale: Float = 1f,
|
||||||
|
) : CharacterStyle(), UpdateAppearance {
|
||||||
|
|
||||||
|
override fun updateDrawState(tp: TextPaint) = Unit
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.data.entities.HttpTTS
|
|
||||||
import io.legado.app.databinding.DialogHttpTtsEditBinding
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.ui.about.AppLogDialog
|
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
|
||||||
import io.legado.app.ui.widget.code.addJsPattern
|
|
||||||
import io.legado.app.ui.widget.code.addJsonPattern
|
|
||||||
import io.legado.app.ui.widget.code.addLegadoPattern
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.sendToClip
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.showHelp
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class HttpTtsEditDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_http_tts_edit),
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
constructor(id: Long) : this() {
|
|
||||||
arguments = Bundle().apply {
|
|
||||||
putLong("id", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogHttpTtsEditBinding::bind)
|
|
||||||
private val viewModel by viewModels<HttpTtsEditViewModel>()
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
|
||||||
binding.tvUrl.run {
|
|
||||||
addLegadoPattern()
|
|
||||||
addJsonPattern()
|
|
||||||
addJsPattern()
|
|
||||||
}
|
|
||||||
binding.tvLoginUrl.run {
|
|
||||||
addLegadoPattern()
|
|
||||||
addJsonPattern()
|
|
||||||
addJsPattern()
|
|
||||||
}
|
|
||||||
binding.tvLoginUi.addJsonPattern()
|
|
||||||
binding.tvLoginCheckJs.addJsPattern()
|
|
||||||
binding.tvHeaders.run {
|
|
||||||
addLegadoPattern()
|
|
||||||
addJsonPattern()
|
|
||||||
addJsPattern()
|
|
||||||
}
|
|
||||||
viewModel.initData(arguments) {
|
|
||||||
initView(httpTTS = it)
|
|
||||||
}
|
|
||||||
initMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initMenu() {
|
|
||||||
binding.toolBar.inflateMenu(R.menu.speak_engine_edit)
|
|
||||||
//binding.toolBar.menu.applyTint(requireContext())
|
|
||||||
binding.toolBar.setOnMenuItemClickListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun initView(httpTTS: HttpTTS) {
|
|
||||||
binding.tvName.setText(httpTTS.name)
|
|
||||||
binding.tvUrl.setText(httpTTS.url)
|
|
||||||
binding.tvContentType.setText(httpTTS.contentType)
|
|
||||||
binding.tvConcurrentRate.setText(httpTTS.concurrentRate)
|
|
||||||
binding.tvLoginUrl.setText(httpTTS.loginUrl)
|
|
||||||
binding.tvLoginUi.setText(httpTTS.loginUi)
|
|
||||||
binding.tvLoginCheckJs.setText(httpTTS.loginCheckJs)
|
|
||||||
binding.tvHeaders.setText(httpTTS.header)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
when (item?.itemId) {
|
|
||||||
R.id.menu_save -> viewModel.save(dataFromView()) {
|
|
||||||
toastOnUi("保存成功")
|
|
||||||
}
|
|
||||||
R.id.menu_login -> dataFromView().let { httpTts ->
|
|
||||||
if (httpTts.loginUrl.isNullOrBlank()) {
|
|
||||||
toastOnUi("登录url不能为空")
|
|
||||||
} else {
|
|
||||||
viewModel.save(httpTts) {
|
|
||||||
startActivity<SourceLoginActivity> {
|
|
||||||
putExtra("type", "httpTts")
|
|
||||||
putExtra("key", httpTts.id.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.menu_show_login_header -> alert {
|
|
||||||
setTitle(R.string.login_header)
|
|
||||||
dataFromView().getLoginHeader()?.let { loginHeader ->
|
|
||||||
setMessage(loginHeader)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
R.id.menu_del_login_header -> dataFromView().removeLoginHeader()
|
|
||||||
R.id.menu_copy_source -> dataFromView().let {
|
|
||||||
context?.sendToClip(GSON.toJson(it))
|
|
||||||
}
|
|
||||||
R.id.menu_paste_source -> viewModel.importFromClip {
|
|
||||||
initView(it)
|
|
||||||
}
|
|
||||||
R.id.menu_log -> showDialogFragment<AppLogDialog>()
|
|
||||||
R.id.menu_help -> showHelp("httpTTSHelp")
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun dataFromView(): HttpTTS {
|
|
||||||
return HttpTTS(
|
|
||||||
id = viewModel.id ?: System.currentTimeMillis(),
|
|
||||||
name = binding.tvName.text.toString(),
|
|
||||||
url = binding.tvUrl.text.toString(),
|
|
||||||
contentType = binding.tvContentType.text?.toString(),
|
|
||||||
concurrentRate = binding.tvConcurrentRate.text?.toString(),
|
|
||||||
loginUrl = binding.tvLoginUrl.text?.toString(),
|
|
||||||
loginUi = binding.tvLoginUi.text?.toString(),
|
|
||||||
loginCheckJs = binding.tvLoginCheckJs.text?.toString(),
|
|
||||||
header = binding.tvHeaders.text?.toString()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.os.Bundle
|
|
||||||
import io.legado.app.base.BaseViewModel
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.HttpTTS
|
|
||||||
import io.legado.app.exception.NoStackTraceException
|
|
||||||
import io.legado.app.model.ReadAloud
|
|
||||||
import io.legado.app.utils.getClipText
|
|
||||||
import io.legado.app.utils.isJsonArray
|
|
||||||
import io.legado.app.utils.isJsonObject
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
|
|
||||||
class HttpTtsEditViewModel(app: Application) : BaseViewModel(app) {
|
|
||||||
|
|
||||||
var id: Long? = null
|
|
||||||
|
|
||||||
fun initData(arguments: Bundle?, success: (httpTTS: HttpTTS) -> Unit) {
|
|
||||||
execute {
|
|
||||||
if (id == null) {
|
|
||||||
val argumentId = arguments?.getLong("id")
|
|
||||||
if (argumentId != null && argumentId != 0L) {
|
|
||||||
id = argumentId
|
|
||||||
return@execute appDb.httpTTSDao.get(argumentId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return@execute null
|
|
||||||
}.onSuccess {
|
|
||||||
it?.let {
|
|
||||||
success.invoke(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun save(httpTTS: HttpTTS, success: (() -> Unit)? = null) {
|
|
||||||
id = httpTTS.id
|
|
||||||
execute {
|
|
||||||
appDb.httpTTSDao.insert(httpTTS)
|
|
||||||
if (ReadAloud.ttsEngine == httpTTS.id.toString()) ReadAloud.upReadAloudClass()
|
|
||||||
}.onSuccess {
|
|
||||||
success?.invoke()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importFromClip(onSuccess: (httpTTS: HttpTTS) -> Unit) {
|
|
||||||
val text = context.getClipText()
|
|
||||||
if (text.isNullOrBlank()) {
|
|
||||||
context.toastOnUi("剪贴板为空")
|
|
||||||
} else {
|
|
||||||
importSource(text, onSuccess)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importSource(text: String, onSuccess: (httpTTS: HttpTTS) -> Unit) {
|
|
||||||
val text1 = text.trim()
|
|
||||||
execute {
|
|
||||||
when {
|
|
||||||
text1.isJsonObject() -> {
|
|
||||||
HttpTTS.fromJson(text1).getOrThrow()
|
|
||||||
}
|
|
||||||
text1.isJsonArray() -> {
|
|
||||||
HttpTTS.fromJsonArray(text1).getOrThrow().first()
|
|
||||||
}
|
|
||||||
else -> {
|
|
||||||
throw NoStackTraceException("格式不对")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
onSuccess.invoke(it)
|
|
||||||
}.onError {
|
|
||||||
context.toastOnUi(it.localizedMessage)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogReadInfoBinding
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.config.ReadTipConfig
|
|
||||||
import io.legado.app.lib.dialogs.selector
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
|
|
||||||
import io.legado.app.utils.getCompatColor
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info), FontSelectDialog.CallBack {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogReadInfoBinding::bind)
|
|
||||||
private val callBack get() = activity as? ReadBookActivity
|
|
||||||
|
|
||||||
override val curFontPath: String
|
|
||||||
get() = ReadBookConfig.headerFont
|
|
||||||
|
|
||||||
override fun selectFont(path: String) {
|
|
||||||
ReadBookConfig.headerFont = path
|
|
||||||
ReadBookConfig.save()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
initView()
|
|
||||||
initEvent()
|
|
||||||
observeEvent<String>(EventBus.TIP_COLOR) {
|
|
||||||
upTvHeaderColor()
|
|
||||||
upTvFooterColor()
|
|
||||||
upTvTipDividerColor()
|
|
||||||
}
|
|
||||||
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) { list ->
|
|
||||||
if (list.contains(2)) {
|
|
||||||
upBtnHeaderMode()
|
|
||||||
upBtnFooterMode()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() {
|
|
||||||
ReadTipConfig.run {
|
|
||||||
tipNames.let { tipNames ->
|
|
||||||
binding.tvHeaderLeft.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipHeaderLeft)) { tipNames[none] }
|
|
||||||
binding.tvHeaderMiddle.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipHeaderMiddle)) { tipNames[none] }
|
|
||||||
binding.tvHeaderRight.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipHeaderRight)) { tipNames[none] }
|
|
||||||
binding.tvFooterLeft.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipFooterLeft)) { tipNames[none] }
|
|
||||||
binding.tvFooterMiddle.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipFooterMiddle)) { tipNames[none] }
|
|
||||||
binding.tvFooterRight.text =
|
|
||||||
tipNames.getOrElse(tipValues.indexOf(tipFooterRight)) { tipNames[none] }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
binding.btnPaddingSetting.setOnClickListener {
|
|
||||||
callBack?.showPaddingConfig()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
upTvHeaderColor()
|
|
||||||
upTvFooterColor()
|
|
||||||
upTvTipDividerColor()
|
|
||||||
binding.scvHeaderFontSize.progress = ReadBookConfig.headerFontSize
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTvHeaderColor() {
|
|
||||||
val tipColor = if (ReadTipConfig.tipHeaderColor == 0) {
|
|
||||||
ReadBookConfig.textColor
|
|
||||||
} else {
|
|
||||||
ReadTipConfig.tipHeaderColor
|
|
||||||
}
|
|
||||||
binding.btnHeaderColor.color = tipColor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTvFooterColor() {
|
|
||||||
val tipColor = if (ReadTipConfig.tipFooterColor == 0) {
|
|
||||||
ReadBookConfig.textColor
|
|
||||||
} else {
|
|
||||||
ReadTipConfig.tipFooterColor
|
|
||||||
}
|
|
||||||
binding.btnFooterColor.color = tipColor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTvTipDividerColor() {
|
|
||||||
val tipDividerColor = when (ReadTipConfig.tipDividerColor) {
|
|
||||||
-1 -> getCompatColor(R.color.divider)
|
|
||||||
0 -> ReadBookConfig.textColor
|
|
||||||
else -> ReadTipConfig.tipDividerColor
|
|
||||||
}
|
|
||||||
binding.btnDividerColor.color = tipDividerColor
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upBtnHeaderMode() {
|
|
||||||
val headerModes = ReadTipConfig.getHeaderModes(requireContext())
|
|
||||||
binding.btnHeaderMode.text = headerModes[ReadTipConfig.headerMode] ?: getString(R.string.header)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upBtnFooterMode() {
|
|
||||||
val footerModes = ReadTipConfig.getFooterModes(requireContext())
|
|
||||||
binding.btnFooterMode.text = footerModes[ReadTipConfig.footerMode] ?: getString(R.string.footer)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvent() = binding.run {
|
|
||||||
|
|
||||||
val headerModes = ReadTipConfig.getHeaderModes(requireContext())
|
|
||||||
binding.btnHeaderMode.text = headerModes[ReadTipConfig.headerMode] ?: getString(R.string.header)
|
|
||||||
binding.btnHeaderMode.setOnClickListener {
|
|
||||||
val items = headerModes.values.toList()
|
|
||||||
context?.selector(items = items) { _, index ->
|
|
||||||
val selectedKey = headerModes.keys.toList()[index]
|
|
||||||
ReadTipConfig.headerMode = selectedKey
|
|
||||||
binding.btnHeaderMode.text = headerModes[selectedKey]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val footerModes = ReadTipConfig.getFooterModes(requireContext())
|
|
||||||
binding.btnFooterMode.text = footerModes[ReadTipConfig.footerMode] ?: getString(R.string.footer)
|
|
||||||
binding.btnFooterMode.setOnClickListener {
|
|
||||||
val items = footerModes.values.toList()
|
|
||||||
context?.selector(items = items) { _, index ->
|
|
||||||
val selectedKey = footerModes.keys.toList()[index]
|
|
||||||
ReadTipConfig.footerMode = selectedKey
|
|
||||||
binding.btnFooterMode.text = footerModes[selectedKey]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
llHeaderLeft.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipHeaderLeft = tipValue
|
|
||||||
tvHeaderLeft.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
llHeaderMiddle.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipHeaderMiddle = tipValue
|
|
||||||
tvHeaderMiddle.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
llHeaderRight.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipHeaderRight = tipValue
|
|
||||||
tvHeaderRight.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
llFooterLeft.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipFooterLeft = tipValue
|
|
||||||
tvFooterLeft.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
llFooterMiddle.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipFooterMiddle = tipValue
|
|
||||||
tvFooterMiddle.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
llFooterRight.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipNames) { _, i ->
|
|
||||||
val tipValue = ReadTipConfig.tipValues[i]
|
|
||||||
clearRepeat(tipValue)
|
|
||||||
ReadTipConfig.tipFooterRight = tipValue
|
|
||||||
tvFooterRight.text = ReadTipConfig.tipNames[i]
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btnHeaderColor.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
|
||||||
when (i) {
|
|
||||||
0 -> {
|
|
||||||
ReadTipConfig.tipHeaderColor = 0
|
|
||||||
upTvHeaderColor()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> ColorPickerDialog.newBuilder()
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(TipConfigDialog.TIP_HEADER_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btnFooterColor.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
|
||||||
when (i) {
|
|
||||||
0 -> {
|
|
||||||
ReadTipConfig.tipFooterColor = 0
|
|
||||||
upTvFooterColor()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
1 -> ColorPickerDialog.newBuilder()
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(TipConfigDialog.TIP_FOOTER_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btnDividerColor.setOnClickListener {
|
|
||||||
context?.selector(items = ReadTipConfig.tipDividerColorNames) { _, i ->
|
|
||||||
when (i) {
|
|
||||||
0, 1 -> {
|
|
||||||
ReadTipConfig.tipDividerColor = i - 1
|
|
||||||
upTvTipDividerColor()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
|
|
||||||
2 -> ColorPickerDialog.newBuilder()
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(TIP_DIVIDER_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
btnHeaderFont.setOnClickListener {
|
|
||||||
FontSelectDialog().show(childFragmentManager, "headerFontSelect")
|
|
||||||
}
|
|
||||||
binding.scvHeaderFontSize.onChanged = {
|
|
||||||
ReadBookConfig.headerFontSize = it
|
|
||||||
ReadBookConfig.save()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun clearRepeat(repeat: Int) = ReadTipConfig.apply {
|
|
||||||
if (repeat != none) {
|
|
||||||
if (tipHeaderLeft == repeat) {
|
|
||||||
tipHeaderLeft = none
|
|
||||||
binding.tvHeaderLeft.text = tipNames[none]
|
|
||||||
}
|
|
||||||
if (tipHeaderMiddle == repeat) {
|
|
||||||
tipHeaderMiddle = none
|
|
||||||
binding.tvHeaderMiddle.text = tipNames[none]
|
|
||||||
}
|
|
||||||
if (tipHeaderRight == repeat) {
|
|
||||||
tipHeaderRight = none
|
|
||||||
binding.tvHeaderRight.text = tipNames[none]
|
|
||||||
}
|
|
||||||
if (tipFooterLeft == repeat) {
|
|
||||||
tipFooterLeft = none
|
|
||||||
binding.tvFooterLeft.text = tipNames[none]
|
|
||||||
}
|
|
||||||
if (tipFooterMiddle == repeat) {
|
|
||||||
tipFooterMiddle = none
|
|
||||||
binding.tvFooterMiddle.text = tipNames[none]
|
|
||||||
}
|
|
||||||
if (tipFooterRight == repeat) {
|
|
||||||
tipFooterRight = none
|
|
||||||
binding.tvFooterRight.text = tipNames[none]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,188 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.bottomBackground
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewConfiguration
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import androidx.preference.PreferenceFragmentCompat
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
|
||||||
import io.legado.app.ui.widget.number.NumberPickerDialog
|
|
||||||
import io.legado.app.utils.canvasrecorder.CanvasRecorderFactory
|
|
||||||
import io.legado.app.utils.getPrefBoolean
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.removePref
|
|
||||||
|
|
||||||
class MoreConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_more_config) {
|
|
||||||
private val readPreferTag = "readPreferenceFragment"
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as? ReadBookActivity)?.bottomDialog++
|
|
||||||
var preferenceFragment = childFragmentManager.findFragmentByTag(readPreferTag)
|
|
||||||
if (preferenceFragment == null) preferenceFragment = ReadPreferenceFragment()
|
|
||||||
childFragmentManager.beginTransaction()
|
|
||||||
.replace(R.id.containerPreferences, preferenceFragment, readPreferTag)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReadPreferenceFragment : PreferenceFragmentCompat(),
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private val slopSquare by lazy { ViewConfiguration.get(requireContext()).scaledTouchSlop }
|
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_config_read)
|
|
||||||
upPreferenceSummary(PreferKey.menuAlpha, AppConfig.menuAlpha.toString())
|
|
||||||
upPreferenceSummary(PreferKey.pageTouchSlop, slopSquare.toString())
|
|
||||||
if (!CanvasRecorderFactory.isSupport) {
|
|
||||||
removePref(PreferKey.optimizeRender)
|
|
||||||
preferenceScreen.removePreferenceRecursively(PreferKey.optimizeRender)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
preferenceManager
|
|
||||||
.sharedPreferences
|
|
||||||
?.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
preferenceManager
|
|
||||||
.sharedPreferences
|
|
||||||
?.unregisterOnSharedPreferenceChangeListener(this)
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(
|
|
||||||
sharedPreferences: SharedPreferences?,
|
|
||||||
key: String?
|
|
||||||
) {
|
|
||||||
when (key) {
|
|
||||||
PreferKey.readBodyToLh -> activity?.recreate()
|
|
||||||
PreferKey.hideStatusBar -> {
|
|
||||||
ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.hideNavigationBar -> {
|
|
||||||
ReadBookConfig.hideNavigationBar = getPrefBoolean(PreferKey.hideNavigationBar)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(0, 2))
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.keepLight -> postEvent(key, true)
|
|
||||||
PreferKey.readSliderMode, PreferKey.titleBarMode -> postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
|
||||||
PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key))
|
|
||||||
PreferKey.screenOrientation -> {
|
|
||||||
(activity as? ReadBookActivity)?.setOrientation()
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.textFullJustify,
|
|
||||||
PreferKey.textBottomJustify,
|
|
||||||
PreferKey.useZhLayout, PreferKey.adaptSpecialStyle, PreferKey.useUnderline -> {
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.showBrightnessView -> {
|
|
||||||
postEvent(PreferKey.showBrightnessView, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.expandTextMenu -> {
|
|
||||||
(activity as? ReadBookActivity)?.textActionMenu?.upMenu()
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.doublePageHorizontal -> {
|
|
||||||
ChapterProvider.upLayout()
|
|
||||||
ReadBook.loadContent(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.showReadTitleAddition,
|
|
||||||
PreferKey.readBarStyleFollowPage -> {
|
|
||||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.progressBarBehavior -> {
|
|
||||||
postEvent(EventBus.UP_SEEK_BAR, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.noAnimScrollPage -> {
|
|
||||||
ReadBook.callBack?.upPageAnim()
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.optimizeRender -> {
|
|
||||||
ChapterProvider.upStyle()
|
|
||||||
ReadBook.callBack?.upPageAnim(true)
|
|
||||||
ReadBook.loadContent(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.paddingDisplayCutouts -> {
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
when (preference.key) {
|
|
||||||
"customPageKey" -> PageKeyDialog(requireContext()).show()
|
|
||||||
"clickRegionalConfig" -> {
|
|
||||||
(activity as? ReadBookActivity)?.showClickRegionalConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.menuAlpha -> {
|
|
||||||
NumberPickerDialog(requireContext())
|
|
||||||
.setTitle(getString(R.string.menu_alpha))
|
|
||||||
.setMaxValue(100)
|
|
||||||
.setMinValue(0)
|
|
||||||
.setValue(AppConfig.menuAlpha)
|
|
||||||
.show {
|
|
||||||
AppConfig.menuAlpha = it
|
|
||||||
upPreferenceSummary(PreferKey.menuAlpha, it.toString())
|
|
||||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.pageTouchSlop -> {
|
|
||||||
NumberPickerDialog(requireContext())
|
|
||||||
.setTitle(getString(R.string.page_touch_slop_dialog_title))
|
|
||||||
.setMaxValue(9999)
|
|
||||||
.setMinValue(0)
|
|
||||||
.setValue(AppConfig.pageTouchSlop)
|
|
||||||
.show {
|
|
||||||
AppConfig.pageTouchSlop = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(4))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
|
||||||
private fun upPreferenceSummary(preferenceKey: String, value: String?) {
|
|
||||||
val preference = findPreference<Preference>(preferenceKey) ?: return
|
|
||||||
when (preferenceKey) {
|
|
||||||
PreferKey.menuAlpha -> preference.summary =
|
|
||||||
getString(R.string.menu_alpha_sum, AppConfig.menuAlpha)
|
|
||||||
PreferKey.pageTouchSlop -> preference.summary =
|
|
||||||
getString(R.string.page_touch_slop_summary, value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,119 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogReadPaddingBinding
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.setLayout
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class PaddingConfigDialog : BaseDialogFragment(R.layout.dialog_read_padding) {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogReadPaddingBinding::bind)
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
initData()
|
|
||||||
initView()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
ReadBookConfig.save()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
if (resources.configuration.smallestScreenWidthDp > 600)
|
|
||||||
setLayout(0.6f, 0.6f)
|
|
||||||
else
|
|
||||||
setLayout(0.9f, 0.8f)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() = binding.run {
|
|
||||||
//正文
|
|
||||||
dsbPaddingTop.progress = ReadBookConfig.paddingTop
|
|
||||||
dsbPaddingBottom.progress = ReadBookConfig.paddingBottom
|
|
||||||
dsbPaddingLeft.progress = ReadBookConfig.paddingLeft
|
|
||||||
dsbPaddingRight.progress = ReadBookConfig.paddingRight
|
|
||||||
//页眉
|
|
||||||
dsbHeaderPaddingTop.progress = ReadBookConfig.headerPaddingTop
|
|
||||||
dsbHeaderPaddingBottom.progress = ReadBookConfig.headerPaddingBottom
|
|
||||||
dsbHeaderPaddingLeft.progress = ReadBookConfig.headerPaddingLeft
|
|
||||||
dsbHeaderPaddingRight.progress = ReadBookConfig.headerPaddingRight
|
|
||||||
//页脚
|
|
||||||
dsbFooterPaddingTop.progress = ReadBookConfig.footerPaddingTop
|
|
||||||
dsbFooterPaddingBottom.progress = ReadBookConfig.footerPaddingBottom
|
|
||||||
dsbFooterPaddingLeft.progress = ReadBookConfig.footerPaddingLeft
|
|
||||||
dsbFooterPaddingRight.progress = ReadBookConfig.footerPaddingRight
|
|
||||||
cbShowTopLine.isChecked = ReadBookConfig.showHeaderLine
|
|
||||||
cbShowBottomLine.isChecked = ReadBookConfig.showFooterLine
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
//正文
|
|
||||||
dsbPaddingTop.onChanged = {
|
|
||||||
ReadBookConfig.paddingTop = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
|
|
||||||
}
|
|
||||||
dsbPaddingBottom.onChanged = {
|
|
||||||
ReadBookConfig.paddingBottom = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
|
|
||||||
}
|
|
||||||
dsbPaddingLeft.onChanged = {
|
|
||||||
ReadBookConfig.paddingLeft = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
|
|
||||||
}
|
|
||||||
dsbPaddingRight.onChanged = {
|
|
||||||
ReadBookConfig.paddingRight = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(10, 5))
|
|
||||||
}
|
|
||||||
//页眉
|
|
||||||
dsbHeaderPaddingTop.onChanged = {
|
|
||||||
ReadBookConfig.headerPaddingTop = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbHeaderPaddingBottom.onChanged = {
|
|
||||||
ReadBookConfig.headerPaddingBottom = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbHeaderPaddingLeft.onChanged = {
|
|
||||||
ReadBookConfig.headerPaddingLeft = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbHeaderPaddingRight.onChanged = {
|
|
||||||
ReadBookConfig.headerPaddingRight = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
//页脚
|
|
||||||
dsbFooterPaddingTop.onChanged = {
|
|
||||||
ReadBookConfig.footerPaddingTop = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbFooterPaddingBottom.onChanged = {
|
|
||||||
ReadBookConfig.footerPaddingBottom = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbFooterPaddingLeft.onChanged = {
|
|
||||||
ReadBookConfig.footerPaddingLeft = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
dsbFooterPaddingRight.onChanged = {
|
|
||||||
ReadBookConfig.footerPaddingRight = it
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
cbShowTopLine.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
ReadBookConfig.showHeaderLine = isChecked
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
cbShowBottomLine.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
ReadBookConfig.showFooterLine = isChecked
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.content.Context
|
|
||||||
import android.view.KeyEvent
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.databinding.DialogPageKeyBinding
|
|
||||||
//import io.legado.app.lib.theme.backgroundColor
|
|
||||||
import io.legado.app.utils.getPrefString
|
|
||||||
import io.legado.app.utils.hideSoftInput
|
|
||||||
import io.legado.app.utils.putPrefString
|
|
||||||
import io.legado.app.utils.setLayout
|
|
||||||
import splitties.views.onClick
|
|
||||||
|
|
||||||
|
|
||||||
class PageKeyDialog(context: Context) : Dialog(context) {
|
|
||||||
|
|
||||||
private val binding = DialogPageKeyBinding.inflate(layoutInflater)
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
setLayout(0.9f, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
setContentView(binding.root)
|
|
||||||
binding.run {
|
|
||||||
//contentView.setBackgroundColor(context.backgroundColor)
|
|
||||||
etPrev.setText(context.getPrefString(PreferKey.prevKeys))
|
|
||||||
etNext.setText(context.getPrefString(PreferKey.nextKeys))
|
|
||||||
tvReset.onClick {
|
|
||||||
etPrev.setText("")
|
|
||||||
etNext.setText("")
|
|
||||||
}
|
|
||||||
tvOk.setOnClickListener {
|
|
||||||
context.putPrefString(PreferKey.prevKeys, etPrev.text?.toString())
|
|
||||||
context.putPrefString(PreferKey.nextKeys, etNext.text?.toString())
|
|
||||||
dismiss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onKeyDown(keyCode: Int, event: KeyEvent): Boolean {
|
|
||||||
if (keyCode != KeyEvent.KEYCODE_BACK && keyCode != KeyEvent.KEYCODE_DEL) {
|
|
||||||
if (binding.etPrev.hasFocus()) {
|
|
||||||
val editableText = binding.etPrev.editableText
|
|
||||||
if (editableText.isEmpty() or editableText.endsWith(",")) {
|
|
||||||
editableText.append(keyCode.toString())
|
|
||||||
} else {
|
|
||||||
editableText.append(",").append(keyCode.toString())
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
} else if (binding.etNext.hasFocus()) {
|
|
||||||
val editableText = binding.etNext.editableText
|
|
||||||
if (editableText.isEmpty() or editableText.endsWith(",")) {
|
|
||||||
editableText.append(keyCode.toString())
|
|
||||||
} else {
|
|
||||||
editableText.append(",").append(keyCode.toString())
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return super.onKeyDown(keyCode, event)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun dismiss() {
|
|
||||||
super.dismiss()
|
|
||||||
currentFocus?.hideSoftInput()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,211 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.backgroundColor
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
// 【新增引用】为了显示清理成功的提示
|
|
||||||
import android.content.SharedPreferences
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.LinearLayout
|
|
||||||
import androidx.preference.ListPreference
|
|
||||||
import androidx.preference.Preference
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BasePrefDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.constant.PreferKey
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.help.IntentHelp
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
|
||||||
import io.legado.app.lib.prefs.SwitchPreference
|
|
||||||
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
|
||||||
import io.legado.app.model.ReadAloud
|
|
||||||
import io.legado.app.service.BaseReadAloudService
|
|
||||||
import io.legado.app.ui.widget.number.NumberPickerDialog
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.StringUtils
|
|
||||||
import io.legado.app.utils.TTSCacheUtils
|
|
||||||
import io.legado.app.utils.fromJsonObject
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.putPrefInt
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
|
|
||||||
class ReadAloudConfigDialog : BasePrefDialogFragment() {
|
|
||||||
private val readAloudPreferTag = "readAloudPreferTag"
|
|
||||||
|
|
||||||
override fun onCreateView(
|
|
||||||
inflater: LayoutInflater,
|
|
||||||
container: ViewGroup?,
|
|
||||||
savedInstanceState: Bundle?
|
|
||||||
): View {
|
|
||||||
val view = LinearLayout(requireContext())
|
|
||||||
//view.setBackgroundColor(requireContext().backgroundColor)
|
|
||||||
view.id = R.id.tag1
|
|
||||||
container?.addView(view)
|
|
||||||
return view
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
var preferenceFragment = childFragmentManager.findFragmentByTag(readAloudPreferTag)
|
|
||||||
if (preferenceFragment == null) preferenceFragment = ReadAloudPreferenceFragment()
|
|
||||||
childFragmentManager.beginTransaction()
|
|
||||||
.replace(view.id, preferenceFragment, readAloudPreferTag)
|
|
||||||
.commit()
|
|
||||||
}
|
|
||||||
|
|
||||||
class ReadAloudPreferenceFragment : PreferenceFragment(),
|
|
||||||
SpeakEngineDialog.CallBack,
|
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
|
||||||
|
|
||||||
private val speakEngineSummary: String
|
|
||||||
get() {
|
|
||||||
val ttsEngine = ReadAloud.ttsEngine
|
|
||||||
?: return getString(R.string.system_tts)
|
|
||||||
if (StringUtils.isNumeric(ttsEngine)) {
|
|
||||||
return appDb.httpTTSDao.getName(ttsEngine.toLong())
|
|
||||||
?: getString(R.string.system_tts)
|
|
||||||
}
|
|
||||||
return GSON.fromJsonObject<SelectItem<String>>(ttsEngine).getOrNull()?.title
|
|
||||||
?: getString(R.string.system_tts)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
|
||||||
addPreferencesFromResource(R.xml.pref_config_aloud)
|
|
||||||
upSpeakEngineSummary()
|
|
||||||
upPreferenceSummary(PreferKey.audioPreDownloadNum)
|
|
||||||
upPreferenceSummary(PreferKey.audioCacheCleanTime)
|
|
||||||
findPreference<SwitchPreference>(PreferKey.pauseReadAloudWhilePhoneCalls)?.let {
|
|
||||||
it.isEnabled = AppConfig.ignoreAudioFocus
|
|
||||||
}
|
|
||||||
|
|
||||||
findPreference<Preference>("clear_cache")?.let {
|
|
||||||
it.summary = getString(R.string.clear_cache)
|
|
||||||
it.setOnPreferenceClickListener {
|
|
||||||
TTSCacheUtils.clearTtsCache()
|
|
||||||
toastOnUi("音频缓存已清理")
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
super.onViewCreated(view, savedInstanceState)
|
|
||||||
//listView.setEdgeEffectColor(primaryColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onResume() {
|
|
||||||
super.onResume()
|
|
||||||
preferenceManager.sharedPreferences?.registerOnSharedPreferenceChangeListener(this)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPause() {
|
|
||||||
preferenceManager.sharedPreferences?.unregisterOnSharedPreferenceChangeListener(this)
|
|
||||||
super.onPause()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPreferenceTreeClick(preference: Preference): Boolean {
|
|
||||||
when (preference.key) {
|
|
||||||
PreferKey.audioPreDownloadNum -> {
|
|
||||||
NumberPickerDialog(requireContext())
|
|
||||||
.setTitle(getString(R.string.read_aloud_preload))
|
|
||||||
.setMaxValue(50)
|
|
||||||
.setMinValue(0)
|
|
||||||
.setValue(10)
|
|
||||||
.setCustomButton((R.string.btn_default_s)) {
|
|
||||||
putPrefInt(PreferKey.audioPreDownloadNum, 10)
|
|
||||||
upPreferenceSummary(PreferKey.audioPreDownloadNum)
|
|
||||||
}
|
|
||||||
.show {
|
|
||||||
putPrefInt(PreferKey.audioPreDownloadNum, it)
|
|
||||||
upPreferenceSummary(PreferKey.audioPreDownloadNum)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.audioCacheCleanTime -> {
|
|
||||||
NumberPickerDialog(requireContext())
|
|
||||||
.setTitle(getString(R.string.audio_cache_clean_time))
|
|
||||||
.setMaxValue(50)
|
|
||||||
.setMinValue(0)
|
|
||||||
.setValue(1)
|
|
||||||
.setCustomButton((R.string.btn_default_s)) {
|
|
||||||
putPrefInt(PreferKey.audioCacheCleanTime, 10)
|
|
||||||
upPreferenceSummary(PreferKey.audioCacheCleanTime)
|
|
||||||
}
|
|
||||||
.show {
|
|
||||||
putPrefInt(PreferKey.audioCacheCleanTime, it)
|
|
||||||
upPreferenceSummary(PreferKey.audioCacheCleanTime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.ttsEngine -> showDialogFragment(SpeakEngineDialog())
|
|
||||||
"sysTtsConfig" -> IntentHelp.openTTSSetting()
|
|
||||||
}
|
|
||||||
return super.onPreferenceTreeClick(preference)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onSharedPreferenceChanged(
|
|
||||||
sharedPreferences: SharedPreferences?,
|
|
||||||
key: String?
|
|
||||||
) {
|
|
||||||
when (key) {
|
|
||||||
PreferKey.readAloudByPage, PreferKey.streamReadAloudAudio -> {
|
|
||||||
if (BaseReadAloudService.isRun) {
|
|
||||||
postEvent(EventBus.MEDIA_BUTTON, false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.ignoreAudioFocus -> {
|
|
||||||
findPreference<SwitchPreference>(PreferKey.pauseReadAloudWhilePhoneCalls)?.let {
|
|
||||||
it.isEnabled = AppConfig.ignoreAudioFocus
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upPreferenceSummary(preference: Preference?, value: String) {
|
|
||||||
when (preference) {
|
|
||||||
is ListPreference -> {
|
|
||||||
val index = preference.findIndexOfValue(value)
|
|
||||||
preference.summary = if (index >= 0) preference.entries[index] else null
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
else -> {
|
|
||||||
preference?.summary = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upPreferenceSummary(preferenceKey: String, value: String? = null) {
|
|
||||||
val preference = findPreference<Preference>(preferenceKey) ?: return
|
|
||||||
when (preferenceKey) {
|
|
||||||
PreferKey.audioPreDownloadNum -> {
|
|
||||||
preference.summary = getString(
|
|
||||||
R.string.read_aloud_preload_summary,
|
|
||||||
AppConfig.audioPreDownloadNum
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
PreferKey.audioCacheCleanTime -> {
|
|
||||||
preference.summary = getString(
|
|
||||||
R.string.audio_cache_clean_time_summary,
|
|
||||||
AppConfig.audioCacheCleanTimeOrgin
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
else -> preference.summary = value
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun upSpeakEngineSummary() {
|
|
||||||
upPreferenceSummary(
|
|
||||||
findPreference(PreferKey.ttsEngine),
|
|
||||||
speakEngineSummary
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.bottomBackground
|
|
||||||
//import io.legado.app.lib.theme.getPrimaryTextColor
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import androidx.core.content.ContextCompat
|
|
||||||
import com.google.android.material.slider.Slider
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogReadAloudBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.selector
|
|
||||||
import io.legado.app.model.ReadAloud
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.service.BaseReadAloudService
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.utils.getPrefBoolean
|
|
||||||
import io.legado.app.utils.observeEvent
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
|
|
||||||
|
|
||||||
class ReadAloudDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_aloud) {
|
|
||||||
private val callBack: CallBack? get() = activity as? CallBack
|
|
||||||
private val binding by viewBinding(DialogReadAloudBinding::bind)
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
// dialog?.window?.run {
|
|
||||||
// clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
|
|
||||||
// setBackgroundDrawableResource(R.color.background)
|
|
||||||
// decorView.setPadding(0, 0, 0, 0)
|
|
||||||
// val attr = attributes
|
|
||||||
// attr.dimAmount = 0.0f
|
|
||||||
// attr.gravity = Gravity.BOTTOM
|
|
||||||
// attributes = attr
|
|
||||||
// setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
|
||||||
// }
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
val bottomDialog = (activity as ReadBookActivity).bottomDialog++
|
|
||||||
if (bottomDialog > 0) {
|
|
||||||
dismiss()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
//val bg = requireContext().bottomBackground
|
|
||||||
//val isLight = ColorUtils.isColorLight(bg)
|
|
||||||
//val textColor = requireContext().getPrimaryTextColor(isLight)
|
|
||||||
binding.run {
|
|
||||||
// rootView.setBackgroundColor(bg)
|
|
||||||
// tvPre.setTextColor(textColor)
|
|
||||||
// tvNext.setTextColor(textColor)
|
|
||||||
// ivPlayPrev.setColorFilter(textColor)
|
|
||||||
// ivPlayPause.setColorFilter(textColor)
|
|
||||||
// ivPlayNext.setColorFilter(textColor)
|
|
||||||
// ivStop.setColorFilter(textColor)
|
|
||||||
// ivTimer.setColorFilter(textColor)
|
|
||||||
// tvTimer.setTextColor(textColor)
|
|
||||||
// ivTtsSpeechReduce.setColorFilter(textColor)
|
|
||||||
// tvTtsSpeed.setTextColor(textColor)
|
|
||||||
// tvTtsSpeedValue.setTextColor(textColor)
|
|
||||||
// ivTtsSpeechAdd.setColorFilter(textColor)
|
|
||||||
// ivCatalog.setColorFilter(textColor)
|
|
||||||
// tvCatalog.setTextColor(textColor)
|
|
||||||
// ivMainMenu.setColorFilter(textColor)
|
|
||||||
// tvMainMenu.setTextColor(textColor)
|
|
||||||
// ivToBackstage.setColorFilter(textColor)
|
|
||||||
// tvToBackstage.setTextColor(textColor)
|
|
||||||
// ivSetting.setColorFilter(textColor)
|
|
||||||
// tvSetting.setTextColor(textColor)
|
|
||||||
// cbTtsFollowSys.setTextColor(textColor)
|
|
||||||
}
|
|
||||||
initData()
|
|
||||||
initEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() = binding.run {
|
|
||||||
upPlayState()
|
|
||||||
upTimerText(BaseReadAloudService.timeMinute)
|
|
||||||
cbTtsFollowSys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true)
|
|
||||||
upTtsSpeechRateEnabled(!cbTtsFollowSys.isChecked)
|
|
||||||
upSeekTimer()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initEvent() = binding.run {
|
|
||||||
ivMainMenu.setOnClickListener {
|
|
||||||
callBack?.showMenuBar()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
ivSetting.setOnClickListener {
|
|
||||||
ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog")
|
|
||||||
}
|
|
||||||
tvPre.setOnClickListener { ReadBook.moveToPrevChapter(upContent = true, toLast = false) }
|
|
||||||
tvNext.setOnClickListener { ReadBook.moveToNextChapter(true) }
|
|
||||||
ivStop.setOnClickListener {
|
|
||||||
ReadAloud.stop(requireContext())
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
ivPlayPause.setOnClickListener { callBack?.onClickReadAloud() }
|
|
||||||
ivPlayPrev.setOnClickListener { ReadAloud.prevParagraph(requireContext()) }
|
|
||||||
ivPlayNext.setOnClickListener { ReadAloud.nextParagraph(requireContext()) }
|
|
||||||
ivCatalog.setOnClickListener { callBack?.openChapterList() }
|
|
||||||
ivToBackstage.setOnClickListener { callBack?.finish() }
|
|
||||||
cbTtsFollowSys.setOnCheckedChangeListener { _, isChecked ->
|
|
||||||
AppConfig.ttsFlowSys = isChecked
|
|
||||||
upTtsSpeechRateEnabled(!isChecked)
|
|
||||||
upTtsSpeechRate()
|
|
||||||
}
|
|
||||||
|
|
||||||
ivTimer.setOnClickListener {
|
|
||||||
AppConfig.ttsTimer = seekTimer.value.toInt()
|
|
||||||
toastOnUi("保存设定时间成功!")
|
|
||||||
}
|
|
||||||
|
|
||||||
// 设置初始值
|
|
||||||
seekTtsSpeechRate.value = AppConfig.ttsSpeechRate.toFloat()
|
|
||||||
seekTimer.value = if (BaseReadAloudService.timeMinute > 0)
|
|
||||||
BaseReadAloudService.timeMinute.toFloat()
|
|
||||||
else AppConfig.ttsTimer.toFloat()
|
|
||||||
|
|
||||||
// 减速按钮逻辑
|
|
||||||
ivTtsSpeechReduce.setOnClickListener {
|
|
||||||
val newValue = (seekTtsSpeechRate.value - 1).coerceAtLeast(seekTtsSpeechRate.valueFrom)
|
|
||||||
seekTtsSpeechRate.value = newValue
|
|
||||||
AppConfig.ttsSpeechRate = newValue.toInt()
|
|
||||||
upTtsSpeechRateText(newValue.toInt())
|
|
||||||
upTtsSpeechRate()
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加速按钮逻辑
|
|
||||||
ivTtsSpeechAdd.setOnClickListener {
|
|
||||||
val newValue = (seekTtsSpeechRate.value + 1).coerceAtMost(seekTtsSpeechRate.valueTo)
|
|
||||||
seekTtsSpeechRate.value = newValue
|
|
||||||
AppConfig.ttsSpeechRate = newValue.toInt()
|
|
||||||
upTtsSpeechRateText(newValue.toInt())
|
|
||||||
upTtsSpeechRate()
|
|
||||||
}
|
|
||||||
|
|
||||||
btnTimer.setOnClickListener {
|
|
||||||
val times = intArrayOf(0, 5, 10, 15, 30, 60, 90, 180)
|
|
||||||
val timeKeys = times.map { "$it 分钟" }
|
|
||||||
context?.selector("设定时间", timeKeys) { _, index ->
|
|
||||||
ReadAloud.setTimer(requireContext(), times[index])
|
|
||||||
upTimerText(times[index])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//设置保存的默认值
|
|
||||||
seekTtsSpeechRate.addOnChangeListener { _, value, fromUser ->
|
|
||||||
if (fromUser) {
|
|
||||||
upTtsSpeechRateText(value.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seekTtsSpeechRate.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {}
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
AppConfig.ttsSpeechRate = slider.value.toInt()
|
|
||||||
upTtsSpeechRate()
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
seekTimer.addOnChangeListener { _, value, fromUser ->
|
|
||||||
if (fromUser) {
|
|
||||||
upTimerText(value.toInt())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
seekTimer.addOnSliderTouchListener(object : Slider.OnSliderTouchListener {
|
|
||||||
override fun onStartTrackingTouch(slider: Slider) {}
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
|
||||||
ReadAloud.setTimer(requireContext(), slider.value.toInt())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTtsSpeechRateEnabled(enabled: Boolean) {
|
|
||||||
binding.run {
|
|
||||||
upTtsSpeechRateText(AppConfig.ttsSpeechRate)
|
|
||||||
tvTtsSpeedValue.visible(enabled)
|
|
||||||
seekTtsSpeechRate.isEnabled = enabled
|
|
||||||
ivTtsSpeechReduce.isEnabled = enabled
|
|
||||||
ivTtsSpeechAdd.isEnabled = enabled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upPlayState() {
|
|
||||||
if (!BaseReadAloudService.pause) {
|
|
||||||
binding.ivPlayPause.icon =
|
|
||||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_pause)
|
|
||||||
binding.ivPlayPause.contentDescription = getString(R.string.pause)
|
|
||||||
} else {
|
|
||||||
binding.ivPlayPause.icon =
|
|
||||||
ContextCompat.getDrawable(requireContext(), R.drawable.ic_play)
|
|
||||||
binding.ivPlayPause.contentDescription = getString(R.string.audio_play)
|
|
||||||
}
|
|
||||||
|
|
||||||
// val bg = requireContext().bottomBackground
|
|
||||||
// val isLight = ColorUtils.isColorLight(bg)
|
|
||||||
// val textColor = requireContext().getPrimaryTextColor(isLight)
|
|
||||||
// binding.ivPlayPause.iconTint = ColorStateList.valueOf(textColor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upSeekTimer() {
|
|
||||||
binding.seekTimer.post {
|
|
||||||
binding.seekTimer.value = if (BaseReadAloudService.timeMinute > 0) {
|
|
||||||
BaseReadAloudService.timeMinute.toFloat()
|
|
||||||
} else {
|
|
||||||
AppConfig.ttsTimer.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTimerText(timeMinute: Int) {
|
|
||||||
if (timeMinute < 0) {
|
|
||||||
binding.btnTimer.text = requireContext().getString(R.string.timer_m, 0)
|
|
||||||
} else {
|
|
||||||
binding.btnTimer.text = requireContext().getString(R.string.timer_m, timeMinute)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("SetTextI18n")
|
|
||||||
private fun upTtsSpeechRateText(value: Int) {
|
|
||||||
binding.tvTtsSpeedValue.text = value.toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTtsSpeechRate() {
|
|
||||||
ReadAloud.upTtsSpeechRate(requireContext())
|
|
||||||
if (!BaseReadAloudService.pause) {
|
|
||||||
ReadAloud.pause(requireContext())
|
|
||||||
ReadAloud.resume(requireContext())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun observeLiveBus() {
|
|
||||||
observeEvent<Int>(EventBus.ALOUD_STATE) { upPlayState() }
|
|
||||||
observeEvent<Int>(EventBus.READ_ALOUD_DS) {
|
|
||||||
val value = it.coerceIn(binding.seekTimer.valueFrom.toInt(), binding.seekTimer.valueTo.toInt())
|
|
||||||
binding.seekTimer.value = value.toFloat()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun showMenuBar()
|
|
||||||
fun openChapterList()
|
|
||||||
fun onClickReadAloud()
|
|
||||||
fun finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.content.DialogInterface
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.Gravity
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.core.view.get
|
|
||||||
import com.github.liuyueyi.quick.transfer.constants.TransType
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogReadBookStyleBinding
|
|
||||||
import io.legado.app.databinding.ItemReadStyleBinding
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.utils.ChineseUtils
|
|
||||||
import io.legado.app.utils.dpToPx
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
class ReadStyleDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_book_style),
|
|
||||||
FontConfigDialog.CallBack {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogReadBookStyleBinding::bind)
|
|
||||||
private val callBack get() = activity as? ReadBookActivity
|
|
||||||
private lateinit var styleAdapter: StyleAdapter
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
(activity as ReadBookActivity).bottomDialog++
|
|
||||||
initView()
|
|
||||||
initData()
|
|
||||||
initViewEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onDismiss(dialog: DialogInterface) {
|
|
||||||
super.onDismiss(dialog)
|
|
||||||
ReadBookConfig.save()
|
|
||||||
(activity as ReadBookActivity).bottomDialog--
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
if (AppConfig.isNightTheme) {
|
|
||||||
tvDayNight.setIconResource(R.drawable.ic_daytime)
|
|
||||||
} else {
|
|
||||||
tvDayNight.setIconResource(R.drawable.ic_brightness)
|
|
||||||
}
|
|
||||||
dsbTextSize.valueFormat = {
|
|
||||||
(it + 5).toString()
|
|
||||||
}
|
|
||||||
|
|
||||||
styleAdapter = StyleAdapter()
|
|
||||||
rvStyle.adapter = styleAdapter
|
|
||||||
styleAdapter.addFooterView {
|
|
||||||
ItemReadStyleBinding.inflate(layoutInflater, it, false).apply {
|
|
||||||
tvStyle.text = ""
|
|
||||||
cdStyle.cardElevation = 0f
|
|
||||||
cdStyle.radius = 8f.dpToPx()
|
|
||||||
cdStyle.strokeWidth = 1.dpToPx()
|
|
||||||
ivStyle.setImageResource(R.drawable.ic_add)
|
|
||||||
ivStyle.setPadding(12.dpToPx(),12.dpToPx(),12.dpToPx(),12.dpToPx())
|
|
||||||
root.setOnClickListener {
|
|
||||||
ReadBookConfig.configList.add(ReadBookConfig.Config())
|
|
||||||
showBgTextConfig(ReadBookConfig.configList.lastIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() {
|
|
||||||
binding.cbShareLayout.isChecked = ReadBookConfig.shareLayout
|
|
||||||
upView()
|
|
||||||
styleAdapter.setItems(ReadBookConfig.configList)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun updateChineseIcon() {
|
|
||||||
val text = when (AppConfig.chineseConverterType) {
|
|
||||||
1 -> " 简"
|
|
||||||
2 -> " 繁"
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
binding.btnChineseConverter.text = text
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
private fun initViewEvent() = binding.run {
|
|
||||||
updateChineseIcon()
|
|
||||||
btnChineseConverter.setOnClickListener {
|
|
||||||
alert(titleResource = R.string.chinese_converter) {
|
|
||||||
items(resources.getStringArray(R.array.chinese_mode).toList()) { _, i ->
|
|
||||||
AppConfig.chineseConverterType = i
|
|
||||||
ChineseUtils.unLoad(*TransType.entries.toTypedArray())
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(5))
|
|
||||||
updateChineseIcon()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tvTextFont.setOnClickListener {
|
|
||||||
callBack?.showFont()
|
|
||||||
}
|
|
||||||
|
|
||||||
tvPadding.setOnClickListener {
|
|
||||||
callBack?.showInfoConfig()
|
|
||||||
}
|
|
||||||
tvTip.setOnClickListener {
|
|
||||||
TipConfigDialog().show(childFragmentManager, "tipConfigDialog")
|
|
||||||
}
|
|
||||||
tvMore.setOnClickListener {
|
|
||||||
showDialogFragment<MoreConfigDialog>()
|
|
||||||
}
|
|
||||||
tvDayNight.setOnClickListener {
|
|
||||||
AppConfig.isNightTheme = !AppConfig.isNightTheme
|
|
||||||
OldThemeConfig.applyDayNight(requireContext())
|
|
||||||
}
|
|
||||||
// rgPageAnim.setOnCheckedChangeListener { _, checkedId ->
|
|
||||||
// ReadBook.book?.setPageAnim(-1)
|
|
||||||
// ReadBookConfig.pageAnim = binding.rgPageAnim.getIndexById(checkedId)
|
|
||||||
// callBack?.upPageAnim()
|
|
||||||
// ReadBook.loadContent(false)
|
|
||||||
// }
|
|
||||||
binding.rgPageAnim.setOnCheckedStateChangeListener { group, checkedIds ->
|
|
||||||
val checkedId = checkedIds.firstOrNull() ?: return@setOnCheckedStateChangeListener
|
|
||||||
ReadBook.book?.setPageAnim(-1)
|
|
||||||
ReadBookConfig.pageAnim = when (checkedId) {
|
|
||||||
R.id.rb_anim0 -> 0 // 覆盖动画
|
|
||||||
R.id.rb_anim1 -> 1 // 滑动动画
|
|
||||||
R.id.rb_simulation_anim -> 2 // 仿真翻页
|
|
||||||
R.id.rb_scroll_anim -> 3 // 滚动动画
|
|
||||||
R.id.rb_fade_anim -> 4
|
|
||||||
R.id.rb_no_anim -> 5 // 无动画
|
|
||||||
else -> 0
|
|
||||||
}
|
|
||||||
callBack?.upPageAnim()
|
|
||||||
ReadBook.loadContent(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
cbShareLayout.addOnCheckedChangeListener { _, isChecked ->
|
|
||||||
ReadBookConfig.shareLayout = isChecked
|
|
||||||
upView()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
||||||
}
|
|
||||||
|
|
||||||
dsbTextSize.onChanged = {
|
|
||||||
ReadBookConfig.textSize = it + 5
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun changeBgTextConfig(index: Int) {
|
|
||||||
val oldIndex = ReadBookConfig.styleSelect
|
|
||||||
if (index != oldIndex) {
|
|
||||||
ReadBookConfig.styleSelect = index
|
|
||||||
upView()
|
|
||||||
styleAdapter.notifyItemChanged(oldIndex)
|
|
||||||
styleAdapter.notifyItemChanged(index)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(1, 2, 5))
|
|
||||||
if (AppConfig.readBarStyleFollowPage) {
|
|
||||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showBgTextConfig(index: Int): Boolean {
|
|
||||||
changeBgTextConfig(index)
|
|
||||||
callBack?.showBgTextConfig()
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upView() = binding.run {
|
|
||||||
ReadBook.pageAnim().let {
|
|
||||||
if (it >= 0 && it < rgPageAnim.childCount) {
|
|
||||||
rgPageAnim.check(rgPageAnim[it].id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ReadBookConfig.let {
|
|
||||||
dsbTextSize.progress = it.textSize - 5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override val curFontPath: String
|
|
||||||
get() = ReadBookConfig.textFont
|
|
||||||
|
|
||||||
override fun selectFont(path: String) {
|
|
||||||
if (path != ReadBookConfig.textFont || path.isEmpty()) {
|
|
||||||
ReadBookConfig.textFont = path
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class StyleAdapter :
|
|
||||||
RecyclerAdapter<ReadBookConfig.Config, ItemReadStyleBinding>(requireContext()) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemReadStyleBinding {
|
|
||||||
return ItemReadStyleBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemReadStyleBinding,
|
|
||||||
item: ReadBookConfig.Config,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.apply {
|
|
||||||
tvStyle.text = item.name.ifBlank { "文字" }
|
|
||||||
tvStyle.setTextColor(item.curTextColor())
|
|
||||||
ivStyle.setImageDrawable(item.curBgDrawable(100, 150))
|
|
||||||
cdStyle.strokeWidth = 1.dpToPx()
|
|
||||||
if (ReadBookConfig.styleSelect == holder.layoutPosition) {
|
|
||||||
llStyle.gravity = Gravity.TOP
|
|
||||||
cdStyle.radius = 32f.dpToPx()
|
|
||||||
//cdStyle.strokeColor = item.curTextColor()
|
|
||||||
//tvStyle.setTextBold(true)
|
|
||||||
} else {
|
|
||||||
cdStyle.radius = 8f.dpToPx()
|
|
||||||
//cdStyle.strokeColor = item.curTextColor()
|
|
||||||
//tvStyle.setTextBold(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemReadStyleBinding) {
|
|
||||||
binding.apply {
|
|
||||||
cdStyle.setOnClickListener {
|
|
||||||
changeBgTextConfig(holder.layoutPosition)
|
|
||||||
}
|
|
||||||
|
|
||||||
cdStyle.setOnLongClickListener {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
showBgTextConfig(holder.layoutPosition)
|
|
||||||
true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.LayoutInflater
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import androidx.recyclerview.widget.RecyclerView
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogRegexColorConfigBinding
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.help.config.RegexColorRule
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
|
||||||
import io.legado.app.ui.book.read.page.provider.TextChapterLayout
|
|
||||||
import io.legado.app.ui.widget.AccentColorButton
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
|
||||||
|
|
||||||
class RegexColorConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_regex_color_config),
|
|
||||||
FontSelectDialog.CallBack {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogRegexColorConfigBinding::bind)
|
|
||||||
private val callBack2 get() = activity as? ReadBookActivity
|
|
||||||
private lateinit var adapter: RegexColorRuleAdapter
|
|
||||||
private var editingRulePosition = -1
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val REGEX_RULE_COLOR = 7900
|
|
||||||
var pendingColorPosition = -1
|
|
||||||
}
|
|
||||||
|
|
||||||
override val curFontPath: String
|
|
||||||
get() = if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
|
||||||
ReadBookConfig.regexColorRules[editingRulePosition].fontPath
|
|
||||||
} else ""
|
|
||||||
|
|
||||||
override fun selectFont(path: String) {
|
|
||||||
if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
|
||||||
ReadBookConfig.regexColorRules[editingRulePosition].fontPath = path
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
adapter = RegexColorRuleAdapter(
|
|
||||||
onDeleteClick = { position -> deleteRule(position) },
|
|
||||||
onColorClick = { position -> showColorPicker(position) },
|
|
||||||
onFontClick = { position -> showFontSelect(position) }
|
|
||||||
)
|
|
||||||
initView()
|
|
||||||
initViewEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(context)
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
adapter.setItems(ReadBookConfig.regexColorRules)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initViewEvent() = binding.run {
|
|
||||||
btnAddRule.setOnClickListener {
|
|
||||||
showAddRuleDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showAddRuleDialog() {
|
|
||||||
val defaultPatterns = listOf(
|
|
||||||
"\u201C匹配内容\u201D" to "\u201C.+?\u201D",
|
|
||||||
"《匹配内容》" to "《.+?》",
|
|
||||||
"\"匹配内容\"" to "\".+?\""
|
|
||||||
)
|
|
||||||
val displayItems = defaultPatterns.map { it.first } + "自定义规则"
|
|
||||||
context?.alert(title = "添加正则规则") {
|
|
||||||
items(displayItems) { _, i ->
|
|
||||||
if (i < defaultPatterns.size) {
|
|
||||||
val (name, pattern) = defaultPatterns[i]
|
|
||||||
addRule(name, pattern)
|
|
||||||
} else {
|
|
||||||
showCustomRuleDialog()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showCustomRuleDialog() {
|
|
||||||
val editText = android.widget.EditText(context).apply {
|
|
||||||
hint = "输入正则表达式,如:\\u201C.+?\\u201D"
|
|
||||||
}
|
|
||||||
context?.alert(title = "自定义正则规则") {
|
|
||||||
customView { editText }
|
|
||||||
okButton {
|
|
||||||
val pattern = editText.text.toString().trim()
|
|
||||||
if (pattern.isNotEmpty()) {
|
|
||||||
addRule(pattern, pattern)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cancelButton()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun addRule(name: String, pattern: String) {
|
|
||||||
val rule = RegexColorRule(name, pattern, ReadBookConfig.durConfig.curTextAccentColor())
|
|
||||||
ReadBookConfig.regexColorRules.add(rule)
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun deleteRule(position: Int) {
|
|
||||||
if (position >= 0 && position < ReadBookConfig.regexColorRules.size) {
|
|
||||||
ReadBookConfig.regexColorRules.removeAt(position)
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showColorPicker(position: Int) {
|
|
||||||
if (position !in ReadBookConfig.regexColorRules.indices) return
|
|
||||||
editingRulePosition = position
|
|
||||||
pendingColorPosition = position
|
|
||||||
val rule = ReadBookConfig.regexColorRules[position]
|
|
||||||
val colorValue = rule.color or 0xFF000000.toInt()
|
|
||||||
ColorPickerDialog.newBuilder()
|
|
||||||
.setColor(colorValue)
|
|
||||||
.setShowAlphaSlider(false)
|
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
|
||||||
.setDialogId(REGEX_RULE_COLOR)
|
|
||||||
.show(requireActivity())
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun showFontSelect(position: Int) {
|
|
||||||
if (position !in ReadBookConfig.regexColorRules.indices) return
|
|
||||||
editingRulePosition = position
|
|
||||||
FontSelectDialog().apply {
|
|
||||||
explicitCallback = this@RegexColorConfigDialog
|
|
||||||
}.show(childFragmentManager, "regexFontSelect")
|
|
||||||
}
|
|
||||||
|
|
||||||
fun onColorSelected(color: Int) {
|
|
||||||
if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
|
||||||
ReadBookConfig.regexColorRules[editingRulePosition].color = color
|
|
||||||
notifyConfigChanged()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun notifyConfigChanged() {
|
|
||||||
ReadBookConfig.saveRegexColorRules()
|
|
||||||
TextChapterLayout.invalidateRegexCache()
|
|
||||||
adapter.setItems(ReadBookConfig.regexColorRules)
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class RegexColorRuleAdapter(
|
|
||||||
private val onDeleteClick: ((Int) -> Unit)? = null,
|
|
||||||
private val onColorClick: ((Int) -> Unit)? = null,
|
|
||||||
private val onFontClick: ((Int) -> Unit)? = null
|
|
||||||
) : RecyclerView.Adapter<RegexColorRuleAdapter.ViewHolder>() {
|
|
||||||
|
|
||||||
private var items: List<RegexColorRule> = emptyList()
|
|
||||||
|
|
||||||
fun setItems(items: List<RegexColorRule>) {
|
|
||||||
this.items = items
|
|
||||||
notifyDataSetChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
|
||||||
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_regex_color_rule, parent, false)
|
|
||||||
return ViewHolder(view)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
|
||||||
val item = items[position]
|
|
||||||
holder.bind(item, position)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun getItemCount(): Int {
|
|
||||||
return items.size
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
|
||||||
val tvRuleName = itemView.findViewById<android.widget.TextView>(R.id.tv_rule_name)
|
|
||||||
val tvRulePattern = itemView.findViewById<android.widget.TextView>(R.id.tv_rule_pattern)
|
|
||||||
val btnSelectFont = itemView.findViewById<com.google.android.material.button.MaterialButton>(R.id.btn_select_font)
|
|
||||||
val btnSelectColor = itemView.findViewById<AccentColorButton>(R.id.btn_select_color)
|
|
||||||
val btnDelete = itemView.findViewById<android.widget.ImageButton>(R.id.btn_delete)
|
|
||||||
|
|
||||||
fun bind(item: RegexColorRule, position: Int) {
|
|
||||||
tvRuleName.text = item.name
|
|
||||||
tvRulePattern.text = item.pattern
|
|
||||||
btnSelectColor.color = item.color or 0xFF000000.toInt()
|
|
||||||
btnSelectFont.setOnClickListener {
|
|
||||||
onFontClick?.invoke(position)
|
|
||||||
}
|
|
||||||
btnSelectColor.setOnClickListener {
|
|
||||||
onColorClick?.invoke(position)
|
|
||||||
}
|
|
||||||
btnDelete.setOnClickListener {
|
|
||||||
onDeleteClick?.invoke(position)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.View
|
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
|
||||||
import io.legado.app.databinding.DialogShadowSetBinding
|
|
||||||
import io.legado.app.utils.postEvent
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 字体选择对话框
|
|
||||||
*/
|
|
||||||
class ShadowSetDialog : BaseBottomSheetDialogFragment(R.layout.dialog_shadow_set) {
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val S_COLOR = 123
|
|
||||||
}
|
|
||||||
private val fontRegex = Regex("(?i).*\\.[ot]tf")
|
|
||||||
private val binding by viewBinding(DialogShadowSetBinding::bind)
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
initView()
|
|
||||||
upView()
|
|
||||||
initViewEvent()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
binding.dsbShadowRadius.valueFormat = { "$it px" }
|
|
||||||
binding.dsbShadowDx.valueFormat = { "$it px" }
|
|
||||||
binding.dsbShadowDy.valueFormat = { "$it px" }
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initViewEvent() = binding.run {
|
|
||||||
binding.dsbShadowRadius.onChanged = {
|
|
||||||
ReadBookConfig.shadowRadius = it.toFloat()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
binding.dsbShadowDx.onChanged = {
|
|
||||||
ReadBookConfig.shadowDx = it.toFloat()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
binding.dsbShadowDy.onChanged = {
|
|
||||||
ReadBookConfig.shadowDy = it.toFloat()
|
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upView() = binding.run {
|
|
||||||
ReadBookConfig.let {
|
|
||||||
binding.dsbShadowRadius.progress = it.shadowRadius.toInt()
|
|
||||||
binding.dsbShadowDx.progress = it.shadowDx.toInt()
|
|
||||||
binding.dsbShadowDy.progress = it.shadowDy.toInt()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val callBack: CallBack?
|
|
||||||
get() = (parentFragment as? CallBack) ?: (activity as? CallBack)
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun selectFont(path: String)
|
|
||||||
val curFontPath: String
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 实线下划线 Span
|
||||||
|
*/
|
||||||
|
class SolidUnderlineSpan(
|
||||||
|
private val textColor: Int,
|
||||||
|
private val underlineColor: Int,
|
||||||
|
private val underlineWidth: Float = 1f,
|
||||||
|
private val underlineOffset: Float = 6f,
|
||||||
|
) : ReplacementSpan() {
|
||||||
|
|
||||||
|
private val offsetPx = underlineOffset.toInt().dpToPx()
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
if (fm != null) {
|
||||||
|
val metrics = paint.fontMetricsInt
|
||||||
|
fm.top = metrics.top
|
||||||
|
fm.ascent = metrics.ascent
|
||||||
|
fm.descent = metrics.descent + offsetPx
|
||||||
|
fm.bottom = metrics.bottom + offsetPx
|
||||||
|
}
|
||||||
|
return paint.measureText(text, start, end).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(
|
||||||
|
canvas: Canvas,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
x: Float,
|
||||||
|
top: Int,
|
||||||
|
y: Int,
|
||||||
|
bottom: Int,
|
||||||
|
paint: Paint
|
||||||
|
) {
|
||||||
|
val textStr = text.subSequence(start, end).toString()
|
||||||
|
paint.color = textColor
|
||||||
|
canvas.drawText(textStr, x, y.toFloat(), paint)
|
||||||
|
|
||||||
|
val width = paint.measureText(text, start, end)
|
||||||
|
val lineY = y + offsetPx
|
||||||
|
val linePaint = Paint(paint).apply {
|
||||||
|
color = underlineColor
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
strokeWidth = underlineWidth.dpToPx()
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
canvas.drawLine(x, lineY.toFloat(), x + width, lineY.toFloat(), linePaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,299 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import android.content.Context
|
|
||||||
import android.os.Bundle
|
|
||||||
import android.view.MenuItem
|
|
||||||
import android.view.View
|
|
||||||
import android.view.ViewGroup
|
|
||||||
import android.widget.RadioButton
|
|
||||||
import androidx.appcompat.widget.Toolbar
|
|
||||||
import androidx.fragment.app.viewModels
|
|
||||||
import androidx.lifecycle.lifecycleScope
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
|
||||||
import io.legado.app.base.adapter.ItemViewHolder
|
|
||||||
import io.legado.app.base.adapter.RecyclerAdapter
|
|
||||||
import io.legado.app.constant.AppLog
|
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.HttpTTS
|
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
|
||||||
import io.legado.app.databinding.DialogRecyclerViewBinding
|
|
||||||
import io.legado.app.databinding.ItemHttpTtsBinding
|
|
||||||
import io.legado.app.help.DirectLinkUpload
|
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
|
||||||
import io.legado.app.lib.dialogs.alert
|
|
||||||
import io.legado.app.model.ReadAloud
|
|
||||||
import io.legado.app.model.ReadBook
|
|
||||||
import io.legado.app.ui.association.ImportHttpTtsDialog
|
|
||||||
import io.legado.app.ui.file.HandleFileContract
|
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
|
||||||
import io.legado.app.utils.ACache
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.TTSCacheUtils
|
|
||||||
import io.legado.app.utils.fromJsonObject
|
|
||||||
import io.legado.app.utils.gone
|
|
||||||
import io.legado.app.utils.isAbsUrl
|
|
||||||
import io.legado.app.utils.isJsonObject
|
|
||||||
import io.legado.app.utils.sendToClip
|
|
||||||
import io.legado.app.utils.showDialogFragment
|
|
||||||
import io.legado.app.utils.splitNotBlank
|
|
||||||
import io.legado.app.utils.startActivity
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
|
||||||
import io.legado.app.utils.visible
|
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
|
||||||
import kotlinx.coroutines.flow.catch
|
|
||||||
import kotlinx.coroutines.flow.conflate
|
|
||||||
import kotlinx.coroutines.flow.flowOn
|
|
||||||
import kotlinx.coroutines.launch
|
|
||||||
|
|
||||||
/**
|
|
||||||
* tts引擎管理
|
|
||||||
*/
|
|
||||||
class SpeakEngineDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycler_view),
|
|
||||||
Toolbar.OnMenuItemClickListener {
|
|
||||||
|
|
||||||
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
|
||||||
private val viewModel: SpeakEngineViewModel by viewModels()
|
|
||||||
private val ttsUrlKey = "ttsUrlKey"
|
|
||||||
private val adapter by lazy { Adapter(requireContext()) }
|
|
||||||
private var ttsEngine: String? = ReadAloud.ttsEngine
|
|
||||||
private val sysTtsViews = arrayListOf<RadioButton>()
|
|
||||||
private val callBack: CallBack? get() = parentFragment as? CallBack
|
|
||||||
private val importDocResult = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
showDialogFragment(ImportHttpTtsDialog(uri.toString()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val exportDirResult = registerForActivityResult(HandleFileContract()) {
|
|
||||||
it.uri?.let { uri ->
|
|
||||||
alert(R.string.export_success) {
|
|
||||||
if (uri.toString().isAbsUrl()) {
|
|
||||||
setMessage(DirectLinkUpload.getSummary())
|
|
||||||
}
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = getString(R.string.path)
|
|
||||||
editView.setText(uri.toString())
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
requireContext().sendToClip(uri.toString())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onStart() {
|
|
||||||
super.onStart()
|
|
||||||
//setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 0.9f)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
|
||||||
initView()
|
|
||||||
initMenu()
|
|
||||||
initData()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initView() = binding.run {
|
|
||||||
//toolBar.setBackgroundColor(primaryColor)
|
|
||||||
toolBar.setTitle(R.string.speak_engine)
|
|
||||||
//recyclerView.setEdgeEffectColor(primaryColor)
|
|
||||||
recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
|
||||||
recyclerView.adapter = adapter
|
|
||||||
adapter.addHeaderView {
|
|
||||||
ItemHttpTtsBinding.inflate(layoutInflater, recyclerView, false).apply {
|
|
||||||
sysTtsViews.add(cbName)
|
|
||||||
ivEdit.gone()
|
|
||||||
ivMenuDelete.gone()
|
|
||||||
labelSys.visible()
|
|
||||||
cbName.text = "系统默认"
|
|
||||||
cbName.tag = ""
|
|
||||||
cbName.isChecked = ttsEngine == null || ttsEngine!!.isJsonObject()
|
|
||||||
&& GSON.fromJsonObject<SelectItem<String>>(ttsEngine)
|
|
||||||
.getOrNull()?.value.isNullOrEmpty()
|
|
||||||
cbName.setOnClickListener {
|
|
||||||
upTts(GSON.toJson(SelectItem("系统默认", "")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
viewModel.sysEngines.forEach { engine ->
|
|
||||||
adapter.addHeaderView {
|
|
||||||
ItemHttpTtsBinding.inflate(layoutInflater, recyclerView, false).apply {
|
|
||||||
sysTtsViews.add(cbName)
|
|
||||||
ivEdit.gone()
|
|
||||||
ivMenuDelete.gone()
|
|
||||||
labelSys.visible()
|
|
||||||
cbName.text = engine.label
|
|
||||||
cbName.tag = engine.name
|
|
||||||
cbName.isChecked = GSON.fromJsonObject<SelectItem<String>>(ttsEngine)
|
|
||||||
.getOrNull()?.value == cbName.tag
|
|
||||||
cbName.setOnClickListener {
|
|
||||||
upTts(GSON.toJson(SelectItem(engine.label, engine.name)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tvFooterLeft.setText(R.string.book)
|
|
||||||
tvFooterLeft.visible()
|
|
||||||
tvFooterLeft.setOnClickListener {
|
|
||||||
ReadBook.book?.setTtsEngine(ttsEngine)
|
|
||||||
callBack?.upSpeakEngineSummary()
|
|
||||||
ReadAloud.upReadAloudClass()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
tvOk.setText(R.string.general)
|
|
||||||
tvOk.visible()
|
|
||||||
tvOk.setOnClickListener {
|
|
||||||
ReadBook.book?.setTtsEngine(null)
|
|
||||||
AppConfig.ttsEngine = ttsEngine
|
|
||||||
callBack?.upSpeakEngineSummary()
|
|
||||||
ReadAloud.upReadAloudClass()
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
tvCancel.visible()
|
|
||||||
tvCancel.setOnClickListener {
|
|
||||||
dismissAllowingStateLoss()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initMenu() = binding.run {
|
|
||||||
toolBar.inflateMenu(R.menu.speak_engine)
|
|
||||||
//toolBar.menu.applyTint(requireContext())
|
|
||||||
toolBar.setOnMenuItemClickListener(this@SpeakEngineDialog)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun initData() {
|
|
||||||
lifecycleScope.launch {
|
|
||||||
appDb.httpTTSDao.flowAll().catch {
|
|
||||||
AppLog.put("朗读引擎界面获取数据失败\n${it.localizedMessage}", it)
|
|
||||||
}.flowOn(IO).conflate().collect {
|
|
||||||
adapter.setItems(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onMenuItemClick(item: MenuItem?): Boolean {
|
|
||||||
when (item?.itemId) {
|
|
||||||
R.id.menu_clear -> clearCache()
|
|
||||||
R.id.menu_add -> showDialogFragment<HttpTtsEditDialog>()
|
|
||||||
R.id.menu_default -> viewModel.importDefault()
|
|
||||||
R.id.menu_import_local -> importDocResult.launch {
|
|
||||||
mode = HandleFileContract.FILE
|
|
||||||
allowExtensions = arrayOf("txt", "json")
|
|
||||||
}
|
|
||||||
|
|
||||||
R.id.menu_import_onLine -> importAlert()
|
|
||||||
R.id.menu_export -> exportDirResult.launch {
|
|
||||||
mode = HandleFileContract.EXPORT
|
|
||||||
fileData = HandleFileContract.FileData(
|
|
||||||
"httpTts.json",
|
|
||||||
GSON.toJson(adapter.getItems()).toByteArray(),
|
|
||||||
"application/json"
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun importAlert() {
|
|
||||||
val aCache = ACache.get(cacheDir = false)
|
|
||||||
val cacheUrls: MutableList<String> = aCache
|
|
||||||
.getAsString(ttsUrlKey)
|
|
||||||
?.splitNotBlank(",")
|
|
||||||
?.toMutableList() ?: mutableListOf()
|
|
||||||
alert(R.string.import_on_line) {
|
|
||||||
val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply {
|
|
||||||
editView.hint = "url"
|
|
||||||
editView.setFilterValues(cacheUrls)
|
|
||||||
editView.delCallBack = {
|
|
||||||
cacheUrls.remove(it)
|
|
||||||
aCache.put(ttsUrlKey, cacheUrls.joinToString(","))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
customView { alertBinding.root }
|
|
||||||
okButton {
|
|
||||||
alertBinding.editView.text?.toString()?.let { url ->
|
|
||||||
if (url.isAbsUrl() && !cacheUrls.contains(url)) {
|
|
||||||
cacheUrls.add(0, url)
|
|
||||||
aCache.put(ttsUrlKey, cacheUrls.joinToString(","))
|
|
||||||
}
|
|
||||||
showDialogFragment(ImportHttpTtsDialog(url))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun upTts(tts: String) {
|
|
||||||
ttsEngine = tts
|
|
||||||
sysTtsViews.forEach {
|
|
||||||
it.isChecked = GSON.fromJsonObject<SelectItem<String>>(ttsEngine)
|
|
||||||
.getOrNull()?.value == it.tag
|
|
||||||
}
|
|
||||||
adapter.notifyItemRangeChanged(adapter.getHeaderCount(), adapter.itemCount)
|
|
||||||
}
|
|
||||||
|
|
||||||
fun clearCache() {
|
|
||||||
execute {
|
|
||||||
ReadAloud.upReadAloudClass()
|
|
||||||
TTSCacheUtils.clearTtsCache()
|
|
||||||
toastOnUi(R.string.clear_cache_success)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inner class Adapter(context: Context) :
|
|
||||||
RecyclerAdapter<HttpTTS, ItemHttpTtsBinding>(context) {
|
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemHttpTtsBinding {
|
|
||||||
return ItemHttpTtsBinding.inflate(inflater, parent, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
|
||||||
holder: ItemViewHolder,
|
|
||||||
binding: ItemHttpTtsBinding,
|
|
||||||
item: HttpTTS,
|
|
||||||
payloads: MutableList<Any>
|
|
||||||
) {
|
|
||||||
binding.apply {
|
|
||||||
cbName.text = item.name
|
|
||||||
cbName.isChecked = item.id.toString() == ttsEngine
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun registerListener(holder: ItemViewHolder, binding: ItemHttpTtsBinding) {
|
|
||||||
binding.run {
|
|
||||||
cbName.setOnClickListener {
|
|
||||||
getItemByLayoutPosition(holder.layoutPosition)?.let { httpTTS ->
|
|
||||||
val id = httpTTS.id.toString()
|
|
||||||
upTts(id)
|
|
||||||
if (!httpTTS.loginUrl.isNullOrBlank()
|
|
||||||
&& httpTTS.getLoginInfo().isNullOrBlank()
|
|
||||||
) {
|
|
||||||
startActivity<SourceLoginActivity> {
|
|
||||||
putExtra("type", "httpTts")
|
|
||||||
putExtra("key", id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ivEdit.setOnClickListener {
|
|
||||||
val id = getItemByLayoutPosition(holder.layoutPosition)!!.id
|
|
||||||
showDialogFragment(HttpTtsEditDialog(id))
|
|
||||||
}
|
|
||||||
ivMenuDelete.setOnClickListener {
|
|
||||||
getItemByLayoutPosition(holder.layoutPosition)?.let { httpTTS ->
|
|
||||||
appDb.httpTTSDao.delete(httpTTS)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
interface CallBack {
|
|
||||||
fun upSpeakEngineSummary()
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
package io.legado.app.ui.book.read.config
|
|
||||||
|
|
||||||
import android.app.Application
|
|
||||||
import android.speech.tts.TextToSpeech
|
|
||||||
import io.legado.app.base.BaseViewModel
|
|
||||||
import io.legado.app.help.DefaultData
|
|
||||||
|
|
||||||
class SpeakEngineViewModel(application: Application) : BaseViewModel(application) {
|
|
||||||
|
|
||||||
val sysEngines: List<TextToSpeech.EngineInfo> by lazy {
|
|
||||||
val tts = TextToSpeech(context, null)
|
|
||||||
val engines = tts.engines
|
|
||||||
tts.shutdown()
|
|
||||||
engines
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importDefault() {
|
|
||||||
execute {
|
|
||||||
DefaultData.importDefaultHttpTTS()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,410 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Path
|
||||||
|
import kotlin.math.abs
|
||||||
|
import kotlin.math.cos
|
||||||
|
import kotlin.math.sin
|
||||||
|
|
||||||
|
object SvgPathParser {
|
||||||
|
|
||||||
|
private val cache = android.util.LruCache<String, Path>(32)
|
||||||
|
|
||||||
|
fun parse(svgPath: String): Path? {
|
||||||
|
if (svgPath.isBlank()) return null
|
||||||
|
cache.get(svgPath)?.let { return it }
|
||||||
|
val path = parseInternal(svgPath) ?: return null
|
||||||
|
cache.put(svgPath, path)
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseInternal(svgPath: String): Path? {
|
||||||
|
if (svgPath.isBlank()) return null
|
||||||
|
|
||||||
|
val path = Path()
|
||||||
|
val tokens = tokenize(svgPath)
|
||||||
|
if (tokens.isEmpty()) return null
|
||||||
|
|
||||||
|
var currentX = 0f
|
||||||
|
var currentY = 0f
|
||||||
|
var startX = 0f
|
||||||
|
var startY = 0f
|
||||||
|
var lastControlX = 0f
|
||||||
|
var lastControlY = 0f
|
||||||
|
var lastCommand = ""
|
||||||
|
var index = 0
|
||||||
|
|
||||||
|
while (index < tokens.size) {
|
||||||
|
val token = tokens[index]
|
||||||
|
|
||||||
|
when (token) {
|
||||||
|
"M" -> {
|
||||||
|
index++
|
||||||
|
if (index + 1 < tokens.size) {
|
||||||
|
currentX = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
currentY = tokens[index + 1].toFloatOrNull() ?: currentY
|
||||||
|
startX = currentX
|
||||||
|
startY = currentY
|
||||||
|
path.moveTo(currentX, currentY)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
lastCommand = "M"
|
||||||
|
}
|
||||||
|
"m" -> {
|
||||||
|
index++
|
||||||
|
if (index + 1 < tokens.size) {
|
||||||
|
val dx = tokens[index].toFloatOrNull() ?: 0f
|
||||||
|
val dy = tokens[index + 1].toFloatOrNull() ?: 0f
|
||||||
|
currentX += dx
|
||||||
|
currentY += dy
|
||||||
|
startX = currentX
|
||||||
|
startY = currentY
|
||||||
|
path.moveTo(currentX, currentY)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
lastCommand = "m"
|
||||||
|
}
|
||||||
|
"L" -> {
|
||||||
|
index++
|
||||||
|
while (index + 1 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
currentX = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
currentY = tokens[index + 1].toFloatOrNull() ?: currentY
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
lastCommand = "L"
|
||||||
|
}
|
||||||
|
"l" -> {
|
||||||
|
index++
|
||||||
|
while (index + 1 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val dx = tokens[index].toFloatOrNull() ?: 0f
|
||||||
|
val dy = tokens[index + 1].toFloatOrNull() ?: 0f
|
||||||
|
currentX += dx
|
||||||
|
currentY += dy
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index += 2
|
||||||
|
}
|
||||||
|
lastCommand = "l"
|
||||||
|
}
|
||||||
|
"H" -> {
|
||||||
|
index++
|
||||||
|
while (index < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
currentX = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
lastCommand = "H"
|
||||||
|
}
|
||||||
|
"h" -> {
|
||||||
|
index++
|
||||||
|
while (index < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val dx = tokens[index].toFloatOrNull() ?: 0f
|
||||||
|
currentX += dx
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
lastCommand = "h"
|
||||||
|
}
|
||||||
|
"V" -> {
|
||||||
|
index++
|
||||||
|
while (index < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
currentY = tokens[index].toFloatOrNull() ?: currentY
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
lastCommand = "V"
|
||||||
|
}
|
||||||
|
"v" -> {
|
||||||
|
index++
|
||||||
|
while (index < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val dy = tokens[index].toFloatOrNull() ?: 0f
|
||||||
|
currentY += dy
|
||||||
|
path.lineTo(currentX, currentY)
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
lastCommand = "v"
|
||||||
|
}
|
||||||
|
"C" -> {
|
||||||
|
index++
|
||||||
|
while (index + 5 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x1 = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
val y1 = tokens[index + 1].toFloatOrNull() ?: currentY
|
||||||
|
val x2 = tokens[index + 2].toFloatOrNull() ?: currentX
|
||||||
|
val y2 = tokens[index + 3].toFloatOrNull() ?: currentY
|
||||||
|
val x = tokens[index + 4].toFloatOrNull() ?: currentX
|
||||||
|
val y = tokens[index + 5].toFloatOrNull() ?: currentY
|
||||||
|
path.cubicTo(x1, y1, x2, y2, x, y)
|
||||||
|
lastControlX = x2
|
||||||
|
lastControlY = y2
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 6
|
||||||
|
}
|
||||||
|
lastCommand = "C"
|
||||||
|
}
|
||||||
|
"c" -> {
|
||||||
|
index++
|
||||||
|
while (index + 5 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x1 = currentX + (tokens[index].toFloatOrNull() ?: 0f)
|
||||||
|
val y1 = currentY + (tokens[index + 1].toFloatOrNull() ?: 0f)
|
||||||
|
val x2 = currentX + (tokens[index + 2].toFloatOrNull() ?: 0f)
|
||||||
|
val y2 = currentY + (tokens[index + 3].toFloatOrNull() ?: 0f)
|
||||||
|
val x = currentX + (tokens[index + 4].toFloatOrNull() ?: 0f)
|
||||||
|
val y = currentY + (tokens[index + 5].toFloatOrNull() ?: 0f)
|
||||||
|
path.cubicTo(x1, y1, x2, y2, x, y)
|
||||||
|
lastControlX = x2
|
||||||
|
lastControlY = y2
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 6
|
||||||
|
}
|
||||||
|
lastCommand = "c"
|
||||||
|
}
|
||||||
|
"S" -> {
|
||||||
|
index++
|
||||||
|
while (index + 3 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x2 = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
val y2 = tokens[index + 1].toFloatOrNull() ?: currentY
|
||||||
|
val x = tokens[index + 2].toFloatOrNull() ?: currentX
|
||||||
|
val y = tokens[index + 3].toFloatOrNull() ?: currentY
|
||||||
|
val x1 = if (lastCommand == "C" || lastCommand == "c" || lastCommand == "S" || lastCommand == "s") {
|
||||||
|
2 * currentX - lastControlX
|
||||||
|
} else {
|
||||||
|
currentX
|
||||||
|
}
|
||||||
|
val y1 = if (lastCommand == "C" || lastCommand == "c" || lastCommand == "S" || lastCommand == "s") {
|
||||||
|
2 * currentY - lastControlY
|
||||||
|
} else {
|
||||||
|
currentY
|
||||||
|
}
|
||||||
|
path.cubicTo(x1, y1, x2, y2, x, y)
|
||||||
|
lastControlX = x2
|
||||||
|
lastControlY = y2
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 4
|
||||||
|
}
|
||||||
|
lastCommand = "S"
|
||||||
|
}
|
||||||
|
"s" -> {
|
||||||
|
index++
|
||||||
|
while (index + 3 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x2 = currentX + (tokens[index].toFloatOrNull() ?: 0f)
|
||||||
|
val y2 = currentY + (tokens[index + 1].toFloatOrNull() ?: 0f)
|
||||||
|
val x = currentX + (tokens[index + 2].toFloatOrNull() ?: 0f)
|
||||||
|
val y = currentY + (tokens[index + 3].toFloatOrNull() ?: 0f)
|
||||||
|
val x1 = if (lastCommand == "C" || lastCommand == "c" || lastCommand == "S" || lastCommand == "s") {
|
||||||
|
2 * currentX - lastControlX
|
||||||
|
} else {
|
||||||
|
currentX
|
||||||
|
}
|
||||||
|
val y1 = if (lastCommand == "C" || lastCommand == "c" || lastCommand == "S" || lastCommand == "s") {
|
||||||
|
2 * currentY - lastControlY
|
||||||
|
} else {
|
||||||
|
currentY
|
||||||
|
}
|
||||||
|
path.cubicTo(x1, y1, x2, y2, x, y)
|
||||||
|
lastControlX = x2
|
||||||
|
lastControlY = y2
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 4
|
||||||
|
}
|
||||||
|
lastCommand = "s"
|
||||||
|
}
|
||||||
|
"Q" -> {
|
||||||
|
index++
|
||||||
|
while (index + 3 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x1 = tokens[index].toFloatOrNull() ?: currentX
|
||||||
|
val y1 = tokens[index + 1].toFloatOrNull() ?: currentY
|
||||||
|
val x = tokens[index + 2].toFloatOrNull() ?: currentX
|
||||||
|
val y = tokens[index + 3].toFloatOrNull() ?: currentY
|
||||||
|
path.quadTo(x1, y1, x, y)
|
||||||
|
lastControlX = x1
|
||||||
|
lastControlY = y1
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 4
|
||||||
|
}
|
||||||
|
lastCommand = "Q"
|
||||||
|
}
|
||||||
|
"q" -> {
|
||||||
|
index++
|
||||||
|
while (index + 3 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val x1 = currentX + (tokens[index].toFloatOrNull() ?: 0f)
|
||||||
|
val y1 = currentY + (tokens[index + 1].toFloatOrNull() ?: 0f)
|
||||||
|
val x = currentX + (tokens[index + 2].toFloatOrNull() ?: 0f)
|
||||||
|
val y = currentY + (tokens[index + 3].toFloatOrNull() ?: 0f)
|
||||||
|
path.quadTo(x1, y1, x, y)
|
||||||
|
lastControlX = x1
|
||||||
|
lastControlY = y1
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 4
|
||||||
|
}
|
||||||
|
lastCommand = "q"
|
||||||
|
}
|
||||||
|
"A", "a" -> {
|
||||||
|
val isRelative = token == "a"
|
||||||
|
index++
|
||||||
|
while (index + 6 < tokens.size && !isCommand(tokens[index])) {
|
||||||
|
val arcRx = abs(tokens[index].toFloatOrNull() ?: 0f)
|
||||||
|
val arcRy = abs(tokens[index + 1].toFloatOrNull() ?: 0f)
|
||||||
|
val xAxisRotation = tokens[index + 2].toFloatOrNull() ?: 0f
|
||||||
|
val largeArcFlag = tokens[index + 3].toIntOrNull() ?: 0
|
||||||
|
val sweepFlag = tokens[index + 4].toIntOrNull() ?: 0
|
||||||
|
val x = if (isRelative) currentX + (tokens[index + 5].toFloatOrNull() ?: 0f) else tokens[index + 5].toFloatOrNull() ?: currentX
|
||||||
|
val y = if (isRelative) currentY + (tokens[index + 6].toFloatOrNull() ?: 0f) else tokens[index + 6].toFloatOrNull() ?: currentY
|
||||||
|
|
||||||
|
drawArc(path, currentX, currentY, x, y, arcRx, arcRy, xAxisRotation, largeArcFlag == 1, sweepFlag == 1)
|
||||||
|
currentX = x
|
||||||
|
currentY = y
|
||||||
|
index += 7
|
||||||
|
}
|
||||||
|
lastCommand = token
|
||||||
|
}
|
||||||
|
"Z", "z" -> {
|
||||||
|
path.close()
|
||||||
|
currentX = startX
|
||||||
|
currentY = startY
|
||||||
|
index++
|
||||||
|
lastCommand = token
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
index++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun tokenize(svgPath: String): List<String> {
|
||||||
|
val tokens = mutableListOf<String>()
|
||||||
|
val sb = StringBuilder()
|
||||||
|
var i = 0
|
||||||
|
|
||||||
|
while (i < svgPath.length) {
|
||||||
|
val c = svgPath[i]
|
||||||
|
|
||||||
|
when {
|
||||||
|
c.isWhitespace() || c == ',' -> {
|
||||||
|
if (sb.isNotEmpty()) {
|
||||||
|
tokens.add(sb.toString())
|
||||||
|
sb.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.isLetter() -> {
|
||||||
|
if (sb.isNotEmpty()) {
|
||||||
|
tokens.add(sb.toString())
|
||||||
|
sb.clear()
|
||||||
|
}
|
||||||
|
tokens.add(c.toString())
|
||||||
|
}
|
||||||
|
c == '-' -> {
|
||||||
|
if (sb.isNotEmpty() && !sb.endsWith('e', ignoreCase = true)) {
|
||||||
|
tokens.add(sb.toString())
|
||||||
|
sb.clear()
|
||||||
|
}
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
c == '.' -> {
|
||||||
|
if (sb.contains('.')) {
|
||||||
|
tokens.add(sb.toString())
|
||||||
|
sb.clear()
|
||||||
|
}
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
sb.append(c)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sb.isNotEmpty()) {
|
||||||
|
tokens.add(sb.toString())
|
||||||
|
}
|
||||||
|
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun isCommand(token: String): Boolean {
|
||||||
|
return token.length == 1 && token[0].isLetter()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawArc(
|
||||||
|
path: Path,
|
||||||
|
x1: Float, y1: Float,
|
||||||
|
x2: Float, y2: Float,
|
||||||
|
arcRx: Float, arcRy: Float,
|
||||||
|
phi: Float,
|
||||||
|
largeArc: Boolean,
|
||||||
|
sweep: Boolean
|
||||||
|
) {
|
||||||
|
if (arcRx == 0f || arcRy == 0f) {
|
||||||
|
path.lineTo(x2, y2)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var localRx = arcRx
|
||||||
|
var localRy = arcRy
|
||||||
|
|
||||||
|
val phiRad = Math.toRadians(phi.toDouble())
|
||||||
|
val cosPhi = cos(phiRad).toFloat()
|
||||||
|
val sinPhi = sin(phiRad).toFloat()
|
||||||
|
|
||||||
|
val dx = (x1 - x2) / 2f
|
||||||
|
val dy = (y1 - y2) / 2f
|
||||||
|
|
||||||
|
val x1p = cosPhi * dx + sinPhi * dy
|
||||||
|
val y1p = -sinPhi * dx + cosPhi * dy
|
||||||
|
|
||||||
|
var rxSq = localRx * localRx
|
||||||
|
var rySq = localRy * localRy
|
||||||
|
val x1pSq = x1p * x1p
|
||||||
|
val y1pSq = y1p * y1p
|
||||||
|
|
||||||
|
var cr = x1pSq / rxSq + y1pSq / rySq
|
||||||
|
if (cr > 1f) {
|
||||||
|
val sqrtCr = kotlin.math.sqrt(cr.toDouble()).toFloat()
|
||||||
|
localRx *= sqrtCr
|
||||||
|
localRy *= sqrtCr
|
||||||
|
rxSq = localRx * localRx
|
||||||
|
rySq = localRy * localRy
|
||||||
|
}
|
||||||
|
|
||||||
|
val rq = rxSq * rySq - rxSq * y1pSq - rySq * x1pSq
|
||||||
|
val cq = rxSq * y1pSq + rySq * x1pSq
|
||||||
|
val sqrtVal = kotlin.math.sqrt(kotlin.math.max(0.0, rq.toDouble()) / kotlin.math.max(1e-10, cq.toDouble())).toFloat()
|
||||||
|
val sign = if (largeArc != sweep) 1f else -1f
|
||||||
|
val cxp = sign * sqrtVal * (localRx * y1p / localRy)
|
||||||
|
val cyp = -sign * sqrtVal * (localRy * x1p / localRx)
|
||||||
|
|
||||||
|
val cx = cosPhi * cxp - sinPhi * cyp + (x1 + x2) / 2f
|
||||||
|
val cy = sinPhi * cxp + cosPhi * cyp + (y1 + y2) / 2f
|
||||||
|
|
||||||
|
val theta1 = angle(1f, 0f, (x1p - cxp) / localRx, (y1p - cyp) / localRy)
|
||||||
|
var dtheta = angle(
|
||||||
|
(x1p - cxp) / localRx, (y1p - cyp) / localRy,
|
||||||
|
(-x1p - cxp) / localRx, (-y1p - cyp) / localRy
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!sweep && dtheta > 0) dtheta -= 360f
|
||||||
|
if (sweep && dtheta < 0) dtheta += 360f
|
||||||
|
|
||||||
|
val sweepAngle = dtheta
|
||||||
|
path.arcTo(
|
||||||
|
android.graphics.RectF(cx - localRx, cy - localRy, cx + localRx, cy + localRy),
|
||||||
|
theta1,
|
||||||
|
sweepAngle,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun angle(ux: Float, uy: Float, vx: Float, vy: Float): Float {
|
||||||
|
val n = kotlin.math.sqrt((ux * ux + uy * uy).toDouble()) * kotlin.math.sqrt((vx * vx + vy * vy).toDouble())
|
||||||
|
val c = (ux * vx + uy * vy) / kotlin.math.max(n, 1e-10)
|
||||||
|
val angle = Math.toDegrees(kotlin.math.acos(c.coerceIn(-1.0, 1.0)))
|
||||||
|
return if (ux * vy - uy * vx < 0) -angle.toFloat() else angle.toFloat()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Paint
|
||||||
|
import android.text.style.ReplacementSpan
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
|
|
||||||
|
/**
|
||||||
|
* SVG 路径下划线 Span
|
||||||
|
*/
|
||||||
|
class SvgUnderlineSpan(
|
||||||
|
private val textColor: Int,
|
||||||
|
private val underlineColor: Int,
|
||||||
|
private val underlineWidth: Float = 1f,
|
||||||
|
private val svgPath: String,
|
||||||
|
) : ReplacementSpan() {
|
||||||
|
|
||||||
|
override fun getSize(
|
||||||
|
paint: Paint,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
fm: Paint.FontMetricsInt?
|
||||||
|
): Int {
|
||||||
|
if (fm != null) {
|
||||||
|
val metrics = paint.fontMetricsInt
|
||||||
|
fm.top = metrics.top
|
||||||
|
fm.ascent = metrics.ascent
|
||||||
|
fm.descent = metrics.descent
|
||||||
|
fm.bottom = metrics.bottom
|
||||||
|
}
|
||||||
|
return paint.measureText(text, start, end).toInt()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun draw(
|
||||||
|
canvas: Canvas,
|
||||||
|
text: CharSequence,
|
||||||
|
start: Int,
|
||||||
|
end: Int,
|
||||||
|
x: Float,
|
||||||
|
top: Int,
|
||||||
|
y: Int,
|
||||||
|
bottom: Int,
|
||||||
|
paint: Paint
|
||||||
|
) {
|
||||||
|
val textStr = text.subSequence(start, end).toString()
|
||||||
|
val textPaint = Paint(paint).apply {
|
||||||
|
color = textColor
|
||||||
|
}
|
||||||
|
canvas.drawText(textStr, x, y.toFloat(), textPaint)
|
||||||
|
|
||||||
|
if (svgPath.isNotBlank()) {
|
||||||
|
val path = SvgPathParser.parse(svgPath)
|
||||||
|
if (path != null) {
|
||||||
|
val textWidth = paint.measureText(textStr)
|
||||||
|
val baseWidth = 100f
|
||||||
|
val baseY = 50f
|
||||||
|
val lineY = y + 6.dpToPx()
|
||||||
|
|
||||||
|
val underlinePaint = Paint(paint).apply {
|
||||||
|
color = underlineColor
|
||||||
|
strokeWidth = underlineWidth.dpToPx()
|
||||||
|
style = Paint.Style.STROKE
|
||||||
|
isAntiAlias = true
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.save()
|
||||||
|
canvas.translate(x, lineY - baseY)
|
||||||
|
canvas.scale(textWidth / baseWidth, 1f)
|
||||||
|
canvas.drawPath(path, underlinePaint)
|
||||||
|
canvas.restore()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loaded 100 of 209 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user