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:
@@ -109,10 +109,10 @@
|
||||
"Bash(node *)",
|
||||
"Bash(Select-String -Pattern \"BUILD|error|Error\")",
|
||||
"Bash(Select-Object -First 10)",
|
||||
"Bash(git push *)",
|
||||
"Bash(gh run *)",
|
||||
"WebFetch(domain:ktor.io)",
|
||||
"WebFetch(domain:api.ktor.io)"
|
||||
"Bash(Select-Object -Last 10)",
|
||||
"Bash(dir /s /b \"%USERPROFILE%\\\\.gradle\\\\caches\\\\*kyant*\")",
|
||||
"Bash(Select-String -Pattern \"\\(BUILD|error:|FAILED\\)\")",
|
||||
"PowerShell(grep *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -139,22 +139,6 @@
|
||||
android:enableOnBackInvokedCallback="true"
|
||||
android:exported="false"
|
||||
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
|
||||
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.book.BookHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.OldThemeConfig
|
||||
import io.legado.app.help.config.OldThemeConfig.applyDayNightInit
|
||||
import io.legado.app.help.config.ThemeConfigStore
|
||||
import io.legado.app.help.config.ThemeConfigStore.applyDayNightInit
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
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.lib.theme.primaryColor
|
||||
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.FirebaseManager
|
||||
import io.legado.app.utils.LogUtils
|
||||
@@ -161,7 +162,7 @@ class App : Application(), ImageLoaderFactory {
|
||||
BookHelp.clearInvalidCache()
|
||||
Backup.clearCache()
|
||||
ReadBookConfig.clearBgAndCache()
|
||||
OldThemeConfig.clearBg()
|
||||
ThemeConfigStore.clearBg()
|
||||
//初始化简繁转换引擎
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> {
|
||||
@@ -189,6 +190,11 @@ class App : Application(), ImageLoaderFactory {
|
||||
// oldConfig = Configuration(newConfig)
|
||||
// }
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
super.onTrimMemory(level)
|
||||
TextLine.trimCaches(level)
|
||||
}
|
||||
|
||||
/**
|
||||
* 尝试在安装了GMS的设备上(GMS或者MicroG)使用GMS内置的Conscrypt
|
||||
* 作为首选JCE提供程序,而使Okhttp在低版本Android上
|
||||
|
||||
@@ -7,7 +7,6 @@ import androidx.core.content.pm.ShortcutManagerCompat
|
||||
import androidx.core.graphics.drawable.IconCompat
|
||||
import io.legado.app.R
|
||||
import io.legado.app.receiver.SharedReceiverActivity
|
||||
import io.legado.app.ui.book.read.ReadBookActivity
|
||||
import io.legado.app.ui.main.MainActivity
|
||||
|
||||
object ShortCuts {
|
||||
@@ -34,7 +33,9 @@ object ShortCuts {
|
||||
val bookShelfIntent = MainActivity.createHomeIntent(context).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
val readBookIntent = buildIntent<ReadBookActivity>(context)
|
||||
val readBookIntent = MainActivity.createReadBookIntent(context).apply {
|
||||
action = Intent.ACTION_VIEW
|
||||
}
|
||||
return ShortcutInfoCompat.Builder(context, "lastRead")
|
||||
.setShortLabel(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.Theme
|
||||
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.utils.applyOpenTint
|
||||
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.hideSoftInput
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.setNavigationBarColorAuto
|
||||
import io.legado.app.utils.setStatusBarColorAuto
|
||||
import io.legado.app.utils.themeColor
|
||||
import io.legado.app.utils.toastOnUi
|
||||
@@ -101,7 +100,6 @@ abstract class BaseActivity<VB : ViewBinding>(
|
||||
else{
|
||||
setupSystemBar()
|
||||
}
|
||||
window.setNavigationBarColorAuto(themeColor(com.google.android.material.R.attr.colorSurface))
|
||||
//setupSystemBar()
|
||||
setContentView(binding.root)
|
||||
upBackgroundImage()
|
||||
@@ -231,7 +229,7 @@ abstract class BaseActivity<VB : ViewBinding>(
|
||||
open fun upBackgroundImage() {
|
||||
if (imageBg) {
|
||||
try {
|
||||
OldThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
||||
ThemeConfigStore.getBgImage(this, windowManager.windowSize)?.let {
|
||||
window.decorView.background = it.toDrawable(resources)
|
||||
}
|
||||
} catch (e: OutOfMemoryError) {
|
||||
@@ -261,4 +259,4 @@ abstract class BaseActivity<VB : ViewBinding>(
|
||||
currentFocus?.hideSoftInput()
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,8 +12,6 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
|
||||
import io.legado.app.constant.AppLog
|
||||
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.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
@@ -30,7 +28,6 @@ abstract class BaseBottomSheetDialogFragment(
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
dialog?.window?.setNavigationBarColorAuto(requireContext().themeColor(com.google.android.material.R.attr.colorSurfaceContainer))
|
||||
if (resources.configuration.orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
val screenHeight = resources.displayMetrics.heightPixels
|
||||
(dialog as? BottomSheetDialog)?.behavior?.apply {
|
||||
@@ -70,4 +67,4 @@ abstract class BaseBottomSheetDialogFragment(
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.os.Build
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
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.Theme
|
||||
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.utils.disableAutoFill
|
||||
import io.legado.app.utils.fullScreen
|
||||
@@ -34,6 +36,10 @@ abstract class BaseComposeActivity(
|
||||
AppContextWrapper.applyLocaleAndFont(this)
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
enableEdgeToEdge()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
window.isNavigationBarContrastEnforced = false
|
||||
}
|
||||
|
||||
setupSystemBar()
|
||||
// Compose 入口
|
||||
@@ -66,7 +72,7 @@ abstract class BaseComposeActivity(
|
||||
|
||||
open fun upBackgroundImage() {
|
||||
try {
|
||||
OldThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
|
||||
ThemeConfigStore.getBgImage(this, windowManager.windowSize)?.let {
|
||||
window.setBackgroundDrawable(it.toDrawable(resources))
|
||||
}
|
||||
} catch (_: Exception) {}
|
||||
|
||||
@@ -103,6 +103,10 @@ object PreferKey {
|
||||
const val showBrightnessView = "showBrightnessView"
|
||||
const val useUnderline = "useUnderline"
|
||||
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 autoClearExpired = "autoClearExpired"
|
||||
const val autoChangeSource = "autoChangeSource"
|
||||
@@ -288,6 +292,35 @@ object PreferKey {
|
||||
const val isPredictiveBackEnabled = "isPredictiveBackEnabled"
|
||||
const val replaceSortMode = "desc"
|
||||
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 selectText = "selectText"
|
||||
//我在干什么
|
||||
@@ -351,3 +384,14 @@ object PreferKey {
|
||||
const val eyeProtectionStartTime = "eyeProtectionStartTime"
|
||||
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.CookieDao
|
||||
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.HomepageModuleDao
|
||||
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.DictRule
|
||||
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.HttpTTS
|
||||
import io.legado.app.data.entities.KeyboardAssist
|
||||
@@ -73,7 +75,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 89,
|
||||
version = 90,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -81,7 +83,8 @@ val appDb by lazy {
|
||||
RssReadRecord::class, ReadRecordDetail::class, ReadRecordSession::class,
|
||||
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::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],
|
||||
autoMigrations = [
|
||||
AutoMigration(from = 43, to = 44),
|
||||
@@ -129,7 +132,8 @@ val appDb by lazy {
|
||||
AutoMigration(from = 85, to = 86),
|
||||
AutoMigration(from = 86, to = 87),
|
||||
AutoMigration(from = 87, to = 88),
|
||||
AutoMigration(from = 88, to = 89)
|
||||
AutoMigration(from = 88, to = 89),
|
||||
AutoMigration(from = 89, to = 90)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
@@ -158,6 +162,7 @@ abstract class AppDatabase : RoomDatabase() {
|
||||
abstract val searchContentHistoryDao: SearchContentHistoryDao
|
||||
abstract val homepageModuleDao: HomepageModuleDao
|
||||
abstract val homepageCustomSetDao: HomepageCustomSetDao
|
||||
abstract val highlightRuleDao: HighlightRuleDao
|
||||
|
||||
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) =
|
||||
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 类型的快捷访问
|
||||
fun getInt(key: String, defaultValue: Int = 0): Flow<Int> =
|
||||
getPreference(intPreferencesKey(key), defaultValue)
|
||||
|
||||
@@ -38,4 +38,5 @@ val appDatabaseModule = module {
|
||||
factory<ServerDao> { get<AppDatabase>().serverDao }
|
||||
factory<HomepageModuleDao> { get<AppDatabase>().homepageModuleDao }
|
||||
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.LlmTranslateRepositoryImpl
|
||||
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.ReadSettingsRepository
|
||||
import io.legado.app.data.repository.RemoteBookRepository
|
||||
import io.legado.app.data.repository.RssRepository
|
||||
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.CacheBookChaptersUseCase
|
||||
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.DeleteBooksUseCase
|
||||
import io.legado.app.domain.usecase.ExploreBooksUseCase
|
||||
import io.legado.app.domain.usecase.ExploreKindUiUseCase
|
||||
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.ImportBookshelfUseCase
|
||||
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.changesource.ChangeBookSourceComposeViewModel
|
||||
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.group.GroupViewModel
|
||||
import io.legado.app.ui.book.import.local.ImportBookViewModel
|
||||
@@ -147,6 +154,10 @@ val appModule = module {
|
||||
singleOf(::SearchContentRepository)
|
||||
singleOf(::RemoteBookRepository)
|
||||
singleOf(::SettingsRepository)
|
||||
singleOf(::ReadSettingsRepository)
|
||||
singleOf(::ReadAloudSettingsRepository)
|
||||
singleOf(::ReadBookStyleConfigRepository)
|
||||
singleOf(::MangaSettingsRepository)
|
||||
singleOf(::LocalPreferencesRepository)
|
||||
singleOf(::ExploreBooksUseCase)
|
||||
singleOf(::ExploreKindUiUseCase)
|
||||
@@ -194,6 +205,8 @@ val appModule = module {
|
||||
single<SearchRepository> { get<SearchRepositoryImpl>() }
|
||||
single<BookSearchGateway> { get<SearchRepositoryImpl>() }
|
||||
singleOf(::SearchBooksUseCase)
|
||||
singleOf(::ChangeSourceSearchUseCase)
|
||||
singleOf(::GetChapterContentUseCase)
|
||||
single<LlmGateway> { LlmTranslateRepositoryImpl() }
|
||||
single<DictionaryGateway> { DictionaryRepositoryImpl() }
|
||||
singleOf(::TranslateChapterUseCase)
|
||||
@@ -252,12 +265,16 @@ val appModule = module {
|
||||
application = get(),
|
||||
getReadingProgressUseCase = get(),
|
||||
uploadReadingProgressUseCase = get(),
|
||||
translateChapterUseCase = get()
|
||||
translateChapterUseCase = get(),
|
||||
readSettingsRepository = get(),
|
||||
readBookStyleConfigRepository = get(),
|
||||
readAloudSettingsRepository = get()
|
||||
)
|
||||
}
|
||||
viewModelOf(::ChangeCoverViewModel)
|
||||
viewModelOf(::ChangeBookSourceComposeViewModel)
|
||||
viewModelOf(::ChangeBookSourceViewModel)
|
||||
viewModelOf(::ChangeChapterSourceViewModel)
|
||||
viewModelOf(::ExploreViewModel)
|
||||
viewModelOf(::RssViewModel)
|
||||
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.TxtTocRule
|
||||
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.coroutine.Coroutine
|
||||
import io.legado.app.model.BookCover
|
||||
@@ -70,12 +70,12 @@ object DefaultData {
|
||||
GSON.fromJsonArray<TxtTocRule>(json).getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
val themeConfigs: List<OldThemeConfig.Config> by lazy {
|
||||
val themeConfigs: List<ThemeConfigStore.Config> by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}${OldThemeConfig.configFileName}")
|
||||
appCtx.assets.open("defaultData${File.separator}${ThemeConfigStore.configFileName}")
|
||||
.readBytes()
|
||||
)
|
||||
GSON.fromJsonArray<OldThemeConfig.Config>(json).getOrNull() ?: emptyList()
|
||||
GSON.fromJsonArray<ThemeConfigStore.Config>(json).getOrNull() ?: emptyList()
|
||||
}
|
||||
|
||||
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.exception.NoStackTraceException
|
||||
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.http.BackstageWebView
|
||||
import io.legado.app.help.http.CookieManager.cookieJarHeader
|
||||
@@ -1204,12 +1204,12 @@ interface JsExtensions : JsEncodeUtils {
|
||||
*/
|
||||
@JavascriptInterface
|
||||
fun getThemeConfig(): String {
|
||||
val themeConfig = OldThemeConfig.getDurConfig(appCtx)
|
||||
val themeConfig = ThemeConfigStore.getDurConfig(appCtx)
|
||||
return GSON.toJson(themeConfig)
|
||||
}
|
||||
|
||||
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.constant.PreferKey
|
||||
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.utils.canvasrecorder.CanvasRecorderFactory
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
@@ -64,8 +65,134 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
var adaptSpecialStyle = appCtx.getPrefBoolean(PreferKey.adaptSpecialStyle, true)
|
||||
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?) {
|
||||
syncReadPreferenceFromSharedPreferences(key)
|
||||
when (key) {
|
||||
|
||||
PreferKey.adaptSpecialStyle -> adaptSpecialStyle =
|
||||
@@ -230,7 +357,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
// get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
|
||||
|
||||
val screenOrientation: String?
|
||||
get() = appCtx.getPrefString(PreferKey.screenOrientation)
|
||||
get() = screenOrientationValue
|
||||
|
||||
var bookGroupStyle: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0)
|
||||
@@ -351,7 +478,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
val noAnimScrollPage: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.noAnimScrollPage, false)
|
||||
get() = noAnimScrollPageValue
|
||||
|
||||
const val defaultSpeechRate = 5
|
||||
|
||||
@@ -468,14 +595,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var tocUiUseReplace: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.tocUiUseReplace)
|
||||
get() = tocUiUseReplaceValue
|
||||
set(value) {
|
||||
tocUiUseReplaceValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.tocUiUseReplace, value)
|
||||
}
|
||||
|
||||
var tocCountWords: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.tocCountWords, true)
|
||||
get() = tocCountWordsValue
|
||||
set(value) {
|
||||
tocCountWordsValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.tocCountWords, value)
|
||||
}
|
||||
|
||||
@@ -486,7 +615,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
val autoChangeSource: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
|
||||
get() = autoChangeSourceValue
|
||||
|
||||
var changeSourceLoadInfo: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo)
|
||||
@@ -545,7 +674,7 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
val clickImgWay: String?
|
||||
get() = appCtx.getPrefString(PreferKey.clickImgWay)
|
||||
get() = clickImgWayValue
|
||||
var preDownloadNum
|
||||
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
||||
set(value) {
|
||||
@@ -590,25 +719,25 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val streamReadAloudAudio get() = appCtx.getPrefBoolean(PreferKey.streamReadAloudAudio, false)
|
||||
|
||||
val doublePageHorizontal: String?
|
||||
get() = appCtx.getPrefString(PreferKey.doublePageHorizontal)
|
||||
get() = doublePageHorizontalValue
|
||||
|
||||
val progressBarBehavior: String?
|
||||
get() = appCtx.getPrefString(PreferKey.progressBarBehavior, "page")
|
||||
get() = progressBarBehaviorValue
|
||||
|
||||
val keyPageOnLongPress
|
||||
get() = appCtx.getPrefBoolean(PreferKey.keyPageOnLongPress, false)
|
||||
get() = keyPageOnLongPressValue
|
||||
|
||||
val volumeKeyPage
|
||||
get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPage, true)
|
||||
get() = volumeKeyPageValue
|
||||
|
||||
val volumeKeyPageOnPlay
|
||||
get() = appCtx.getPrefBoolean(PreferKey.volumeKeyPageOnPlay, true)
|
||||
get() = volumeKeyPageOnPlayValue
|
||||
|
||||
val mouseWheelPage
|
||||
get() = appCtx.getPrefBoolean(PreferKey.mouseWheelPage, true)
|
||||
get() = mouseWheelPageValue
|
||||
|
||||
val paddingDisplayCutouts
|
||||
get() = appCtx.getPrefBoolean(PreferKey.paddingDisplayCutouts, false)
|
||||
get() = paddingDisplayCutoutsValue
|
||||
|
||||
var searchScope: String
|
||||
get() = appCtx.getPrefString("searchScope") ?: ""
|
||||
@@ -623,8 +752,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var pageTouchSlop: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.pageTouchSlop, 0)
|
||||
get() = pageTouchSlopValue
|
||||
set(value) {
|
||||
pageTouchSlopValue = value
|
||||
appCtx.putPrefInt(PreferKey.pageTouchSlop, value)
|
||||
}
|
||||
|
||||
@@ -666,21 +796,24 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var showReadTitleBarAddition: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showReadTitleAddition, true)
|
||||
get() = showReadTitleBarAdditionValue
|
||||
set(value) {
|
||||
showReadTitleBarAdditionValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.showReadTitleAddition, value)
|
||||
}
|
||||
|
||||
var readBarStyleFollowPage: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.readBarStyleFollowPage, false)
|
||||
get() = readBarStyleFollowPageValue
|
||||
set(value) {
|
||||
readBarStyleFollowPageValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.readBarStyleFollowPage, value)
|
||||
}
|
||||
|
||||
var readBarStyle: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.readBarStyle, 0)
|
||||
get() = readBarStyleValue
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.readBarStyle, value)
|
||||
readBarStyleValue = value.coerceIn(0, 2)
|
||||
appCtx.putPrefInt(PreferKey.readBarStyle, readBarStyleValue)
|
||||
}
|
||||
|
||||
var sourceEditMaxLine: Int
|
||||
@@ -757,8 +890,9 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var titleBarMode
|
||||
get() = appCtx.getPrefString(PreferKey.titleBarMode, "1")
|
||||
get() = titleBarModeValue
|
||||
set(value) {
|
||||
titleBarModeValue = value
|
||||
appCtx.putPrefString(PreferKey.titleBarMode, value)
|
||||
}
|
||||
|
||||
@@ -903,14 +1037,16 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
appCtx.putPrefBoolean(PreferKey.enableBlur, value)
|
||||
}
|
||||
var menuAlpha: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.menuAlpha, 100)
|
||||
get() = menuAlphaValue
|
||||
set(value) {
|
||||
menuAlphaValue = value
|
||||
appCtx.putPrefInt(PreferKey.menuAlpha, value)
|
||||
}
|
||||
|
||||
var readSliderMode
|
||||
get() = appCtx.getPrefString(PreferKey.readSliderMode, "0")
|
||||
get() = readSliderModeValue
|
||||
set(value) {
|
||||
readSliderModeValue = value
|
||||
appCtx.putPrefString(PreferKey.readSliderMode, value)
|
||||
}
|
||||
|
||||
@@ -945,20 +1081,23 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
}
|
||||
|
||||
var defaultSourceChangeAll: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.defaultSourceChangeAll, true)
|
||||
get() = defaultSourceChangeAllValue
|
||||
set(value) {
|
||||
defaultSourceChangeAllValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.defaultSourceChangeAll, value)
|
||||
}
|
||||
|
||||
var sliderVibrator: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.sliderVibrator, false)
|
||||
get() = sliderVibratorValue
|
||||
set(value) {
|
||||
sliderVibratorValue = value
|
||||
appCtx.putPrefBoolean(PreferKey.sliderVibrator, value)
|
||||
}
|
||||
|
||||
var selectVibrator: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.selectVibrator, false)
|
||||
get() = selectVibratorValue
|
||||
set(value) {
|
||||
selectVibratorValue = 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.util.DisplayMetrics
|
||||
import androidx.annotation.Keep
|
||||
import androidx.appcompat.app.AppCompatDelegate
|
||||
import androidx.core.graphics.toColorInt
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.help.DefaultData
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.utils.BitmapUtils
|
||||
import io.legado.app.utils.FileUtils
|
||||
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.postEvent
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import io.legado.app.utils.putPrefInt
|
||||
import io.legado.app.utils.stackBlur
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
@Keep
|
||||
object OldThemeConfig {
|
||||
object ThemeConfigStore {
|
||||
const val configFileName = "themeConfig.json"
|
||||
val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
|
||||
|
||||
@@ -46,13 +43,8 @@ object OldThemeConfig {
|
||||
else -> Theme.Light
|
||||
}
|
||||
|
||||
fun isDarkTheme(): Boolean {
|
||||
return getTheme() == Theme.Dark
|
||||
}
|
||||
|
||||
fun applyDayNight(context: Context) {
|
||||
initNightMode()
|
||||
BookCover.upDefaultCover()
|
||||
postEvent(EventBus.RECREATE, "")
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||
}
|
||||
@@ -62,17 +54,7 @@ object OldThemeConfig {
|
||||
}
|
||||
|
||||
private fun initNightMode() {
|
||||
when (appCtx.getPrefString(PreferKey.themeMode, "0")) {
|
||||
"1" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
|
||||
}
|
||||
"2" -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
|
||||
}
|
||||
else -> {
|
||||
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
|
||||
}
|
||||
}
|
||||
ThemeConfig.initNightMode()
|
||||
}
|
||||
|
||||
fun getBgImage(context: Context, metrics: DisplayMetrics): Bitmap? {
|
||||
@@ -110,11 +92,6 @@ object OldThemeConfig {
|
||||
FileUtils.createFileIfNotExist(configFilePath).writeText(json)
|
||||
}
|
||||
|
||||
fun delConfig(index: Int) {
|
||||
configList.removeAt(index)
|
||||
save()
|
||||
}
|
||||
|
||||
fun addConfig(json: String): Boolean {
|
||||
GSON.fromJsonObject<Config>(json.trim { it < ' ' }).getOrNull()
|
||||
?.let {
|
||||
@@ -165,33 +142,8 @@ object OldThemeConfig {
|
||||
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() {
|
||||
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.config.AppConfig
|
||||
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.coroutine.Coroutine
|
||||
import io.legado.app.model.BookCover
|
||||
@@ -77,7 +77,7 @@ object Backup {
|
||||
DirectLinkUpload.ruleFileName,
|
||||
ReadBookConfig.configFileName,
|
||||
ReadBookConfig.shareConfigFileName,
|
||||
OldThemeConfig.configFileName,
|
||||
ThemeConfigStore.configFileName,
|
||||
BookCover.configFileName,
|
||||
"config.xml"
|
||||
)
|
||||
@@ -164,8 +164,8 @@ object Backup {
|
||||
FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.shareConfigFileName)
|
||||
.writeText(it)
|
||||
}
|
||||
GSON.toJson(OldThemeConfig.configList).let {
|
||||
FileUtils.createFileIfNotExist(backupPath + File.separator + OldThemeConfig.configFileName)
|
||||
GSON.toJson(ThemeConfigStore.configList).let {
|
||||
FileUtils.createFileIfNotExist(backupPath + File.separator + ThemeConfigStore.configFileName)
|
||||
.writeText(it)
|
||||
}
|
||||
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.upType
|
||||
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.model.BookCover
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
@@ -275,12 +275,12 @@ object Restore : KoinComponent {
|
||||
}
|
||||
//恢复主题配置
|
||||
if (!BackupConfig.ignoreThemeConfig) {
|
||||
File(path, OldThemeConfig.configFileName).takeIf {
|
||||
File(path, ThemeConfigStore.configFileName).takeIf {
|
||||
it.exists()
|
||||
}?.runCatching {
|
||||
FileUtils.delete(OldThemeConfig.configFilePath)
|
||||
copyTo(File(OldThemeConfig.configFilePath))
|
||||
OldThemeConfig.upConfig()
|
||||
FileUtils.delete(ThemeConfigStore.configFilePath)
|
||||
copyTo(File(ThemeConfigStore.configFilePath))
|
||||
ThemeConfigStore.upConfig()
|
||||
}?.onFailure {
|
||||
AppLog.put("恢复主题出错\n${it.localizedMessage}", it)
|
||||
}
|
||||
@@ -341,7 +341,7 @@ object Restore : KoinComponent {
|
||||
if (!BuildConfig.DEBUG) {
|
||||
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 com.google.android.material.button.MaterialButtonToggleGroup
|
||||
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) {
|
||||
@@ -59,7 +59,7 @@ class ThemeModePreference(context: Context, attrs: AttributeSet) : Preference(co
|
||||
persistString(newValue)
|
||||
callChangeListener(newValue)
|
||||
Handler(Looper.getMainLooper()).postDelayed({
|
||||
OldThemeConfig.applyDayNight(context)
|
||||
ThemeConfigStore.applyDayNight(context)
|
||||
}, 300)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -63,9 +63,6 @@ object BookCover {
|
||||
}.getOrDefault(appCtx.resources.getDrawable(R.drawable.image_cover_default, null))
|
||||
}
|
||||
|
||||
// 兼容旧代码,空实现
|
||||
fun upDefaultCover() {}
|
||||
|
||||
fun getRandomDefaultPath(
|
||||
seed: Any? = null,
|
||||
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.BaseReadAloudService
|
||||
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.getPrefBoolean
|
||||
import io.legado.app.utils.postEvent
|
||||
@@ -94,7 +94,7 @@ class MediaButtonReceiver : BroadcastReceiver() {
|
||||
// break
|
||||
}
|
||||
|
||||
LifecycleHelp.isExistActivity(ReadBookActivity::class.java) ->
|
||||
MainActivity.hasActiveReadBookRoute ->
|
||||
postEvent(EventBus.MEDIA_BUTTON, true)
|
||||
|
||||
LifecycleHelp.isExistActivity(AudioPlayActivity::class.java) ->
|
||||
|
||||
@@ -17,12 +17,10 @@ import splitties.systemservices.connectivityManager
|
||||
class NetworkChangedListener(private val context: Context) {
|
||||
|
||||
var onNetworkChanged: (() -> Unit)? = null
|
||||
private var registered = false
|
||||
|
||||
private val receiver: NetworkChangedReceiver? by lazy {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
|
||||
NetworkChangedReceiver()
|
||||
}
|
||||
return@lazy null
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) NetworkChangedReceiver() else null
|
||||
}
|
||||
|
||||
private val networkCallback: ConnectivityManager.NetworkCallback? by lazy {
|
||||
@@ -38,25 +36,31 @@ class NetworkChangedListener(private val context: Context) {
|
||||
|
||||
@SuppressLint("MissingPermission", "UnspecifiedRegisterReceiverFlag")
|
||||
fun register() {
|
||||
if (registered) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
networkCallback?.let {
|
||||
connectivityManager.registerDefaultNetworkCallback(it)
|
||||
registered = true
|
||||
}
|
||||
} else {
|
||||
receiver?.let {
|
||||
context.registerReceiver(it, it.filter)
|
||||
registered = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun unRegister() {
|
||||
if (!registered) return
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
networkCallback?.let {
|
||||
connectivityManager.unregisterNetworkCallback(it)
|
||||
registered = false
|
||||
}
|
||||
} else {
|
||||
receiver?.let {
|
||||
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.ReadBook
|
||||
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.main.MainActivity
|
||||
import io.legado.app.utils.LogUtils
|
||||
import io.legado.app.utils.activityPendingIntent
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
@@ -639,7 +639,10 @@ abstract class BaseReadAloudService : BaseService(),
|
||||
.setContentTitle(nTitle)
|
||||
.setContentText(nSubtitle)
|
||||
.setContentIntent(
|
||||
activityPendingIntent<ReadBookActivity>("activity")
|
||||
activityPendingIntent(
|
||||
MainActivity.createReadBookIntent(this, readAloud = true),
|
||||
"activity"
|
||||
)
|
||||
)
|
||||
.setVibrate(null)
|
||||
.setSound(null)
|
||||
|
||||
@@ -35,6 +35,8 @@ class InteractiveHighlight(
|
||||
Animatable(Offset.Zero, Offset.VectorConverter, Offset.VisibilityThreshold)
|
||||
|
||||
private var startPosition = Offset.Zero
|
||||
val pressProgress: Float get() = pressProgressAnimation.value
|
||||
val offset: Offset get() = positionAnimation.value
|
||||
|
||||
@Language("AGSL")
|
||||
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.databinding.DialogRecyclerViewBinding
|
||||
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.ui.widget.dialog.CodeDialog
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
@@ -126,7 +126,7 @@ class ImportThemeDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
|
||||
}
|
||||
|
||||
inner class SourcesAdapter(context: Context) :
|
||||
RecyclerAdapter<OldThemeConfig.Config, ItemSourceImportBinding>(context) {
|
||||
RecyclerAdapter<ThemeConfigStore.Config, ItemSourceImportBinding>(context) {
|
||||
|
||||
override fun getViewBinding(parent: ViewGroup): ItemSourceImportBinding {
|
||||
return ItemSourceImportBinding.inflate(inflater, parent, false)
|
||||
@@ -135,7 +135,7 @@ class ImportThemeDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_recycl
|
||||
override fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemSourceImportBinding,
|
||||
item: OldThemeConfig.Config,
|
||||
item: ThemeConfigStore.Config,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
binding.apply {
|
||||
|
||||
@@ -8,7 +8,7 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
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.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
@@ -28,8 +28,8 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
||||
val errorLiveData = MutableLiveData<String>()
|
||||
val successLiveData = MutableLiveData<Int>()
|
||||
|
||||
val allSources = arrayListOf<OldThemeConfig.Config>()
|
||||
val checkSources = arrayListOf<OldThemeConfig.Config?>()
|
||||
val allSources = arrayListOf<ThemeConfigStore.Config>()
|
||||
val checkSources = arrayListOf<ThemeConfigStore.Config?>()
|
||||
val selectStatus = arrayListOf<Boolean>()
|
||||
|
||||
val isSelectAll: Boolean
|
||||
@@ -57,7 +57,7 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
||||
execute {
|
||||
selectStatus.forEachIndexed { index, b ->
|
||||
if (b) {
|
||||
OldThemeConfig.addConfig(allSources[index])
|
||||
ThemeConfigStore.addConfig(allSources[index])
|
||||
}
|
||||
}
|
||||
}.onFinally {
|
||||
@@ -79,12 +79,12 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
||||
private suspend fun importSourceAwait(text: String) {
|
||||
when {
|
||||
text.isJsonObject() -> {
|
||||
GSON.fromJsonObject<OldThemeConfig.Config>(text).getOrThrow().let {
|
||||
GSON.fromJsonObject<ThemeConfigStore.Config>(text).getOrThrow().let {
|
||||
allSources.add(it)
|
||||
}
|
||||
}
|
||||
|
||||
text.isJsonArray() -> GSON.fromJsonArray<OldThemeConfig.Config>(text).getOrThrow()
|
||||
text.isJsonArray() -> GSON.fromJsonArray<ThemeConfigStore.Config>(text).getOrThrow()
|
||||
.let { items ->
|
||||
allSources.addAll(items)
|
||||
}
|
||||
@@ -114,7 +114,7 @@ class ImportThemeViewModel(app: Application) : BaseViewModel(app) {
|
||||
private fun comparisonSource() {
|
||||
execute {
|
||||
allSources.forEach { config ->
|
||||
val source = OldThemeConfig.configList.find {
|
||||
val source = ThemeConfigStore.configList.find {
|
||||
it.themeName == config.themeName
|
||||
}
|
||||
checkSources.add(source)
|
||||
|
||||
+227
-29
@@ -1,25 +1,35 @@
|
||||
package io.legado.app.ui.book.changesource
|
||||
|
||||
import android.app.Application
|
||||
import io.legado.app.data.appDb
|
||||
import androidx.lifecycle.ViewModel
|
||||
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.SearchBook
|
||||
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 kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class ChangeBookSourceComposeViewModel(
|
||||
application: Application,
|
||||
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
||||
private val getChapterContentUseCase: GetChapterContentUseCase,
|
||||
private val searchRepository: SearchRepository,
|
||||
) : ChangeBookSourceViewModel(application) {
|
||||
) : ViewModel() {
|
||||
|
||||
// Public state for the sheet
|
||||
val enabledGroups = searchRepository.enabledGroups
|
||||
val enabledSources = searchRepository.enabledSources
|
||||
|
||||
val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
||||
private val searchScope = SearchScope(ChangeSourceConfig.searchScope)
|
||||
|
||||
data class ScopeUiState(
|
||||
val isAll: Boolean,
|
||||
@@ -38,18 +48,164 @@ class ChangeBookSourceComposeViewModel(
|
||||
)
|
||||
val scopeUiState = _scopeUiState.asStateFlow()
|
||||
|
||||
val checkAuthor: Boolean
|
||||
get() = ChangeSourceConfig.checkAuthor
|
||||
private val _isSearching = MutableStateFlow(false)
|
||||
val isSearching = _isSearching.asStateFlow()
|
||||
|
||||
val loadInfo: Boolean
|
||||
get() = ChangeSourceConfig.loadInfo
|
||||
private val _changeSourceProgress = MutableStateFlow(0 to "")
|
||||
val changeSourceProgress = _changeSourceProgress.asStateFlow()
|
||||
|
||||
val loadToc: Boolean
|
||||
get() = ChangeSourceConfig.loadToc
|
||||
private val _searchDataFlow = MutableStateFlow<List<SearchBook>>(emptyList())
|
||||
val searchDataFlow: StateFlow<List<SearchBook>> = _searchDataFlow.asStateFlow()
|
||||
|
||||
val loadWordCount: Boolean
|
||||
get() = ChangeSourceConfig.loadWordCount
|
||||
val totalSourceCount: Int
|
||||
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) {
|
||||
if (ChangeSourceConfig.checkAuthor == enabled) return
|
||||
ChangeSourceConfig.checkAuthor = enabled
|
||||
@@ -70,21 +226,67 @@ class ChangeBookSourceComposeViewModel(
|
||||
if (ChangeSourceConfig.loadWordCount == enabled) return
|
||||
ChangeSourceConfig.loadWordCount = enabled
|
||||
if (enabled) {
|
||||
onLoadWordCountChecked(true)
|
||||
startSearch()
|
||||
} else {
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
fun bookScoreFlow(searchBook: SearchBook): StateFlow<Int> {
|
||||
return ObservableSourceConfig.bookScoreFlow(searchBook)
|
||||
fun refresh() {
|
||||
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) {
|
||||
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() {
|
||||
searchScope.update("")
|
||||
saveScope()
|
||||
@@ -110,17 +312,15 @@ class ChangeBookSourceComposeViewModel(
|
||||
} else {
|
||||
mutableSetOf()
|
||||
}
|
||||
|
||||
if (selectedUrls.contains(source.bookSourceUrl)) {
|
||||
selectedUrls.remove(source.bookSourceUrl)
|
||||
} else {
|
||||
selectedUrls.add(source.bookSourceUrl)
|
||||
}
|
||||
|
||||
if (selectedUrls.isEmpty()) {
|
||||
searchScope.update("")
|
||||
} else {
|
||||
val selectedSources = appDb.bookSourceDao.allEnabledPart.filter {
|
||||
val selectedSources = io.legado.app.data.appDb.bookSourceDao.allEnabledPart.filter {
|
||||
selectedUrls.contains(it.bookSourceUrl)
|
||||
}
|
||||
searchScope.updateSources(selectedSources)
|
||||
@@ -130,14 +330,12 @@ class ChangeBookSourceComposeViewModel(
|
||||
|
||||
private fun saveScope() {
|
||||
ChangeSourceConfig.searchScope = searchScope.toString()
|
||||
_scopeUiState.update {
|
||||
ScopeUiState(
|
||||
isAll = searchScope.isAll(),
|
||||
isSource = searchScope.isSource(),
|
||||
displayNames = searchScope.displayNames,
|
||||
sourceUrls = searchScope.sourceUrls
|
||||
)
|
||||
}
|
||||
_scopeUiState.value = ScopeUiState(
|
||||
isAll = searchScope.isAll(),
|
||||
isSource = searchScope.isSource(),
|
||||
displayNames = searchScope.displayNames,
|
||||
sourceUrls = searchScope.sourceUrls
|
||||
)
|
||||
refresh()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,10 +32,10 @@ import io.legado.app.databinding.DialogBookChangeSourceBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
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.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.main.MainActivity
|
||||
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||
import io.legado.app.ui.widget.recycler.VerticalDivider
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
@@ -103,7 +103,7 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
//binding.toolBar.setBackgroundColor(primaryColor)
|
||||
viewModel.initData(arguments, callBack?.oldBook, activity is ReadBookActivity)
|
||||
viewModel.initData(arguments, callBack?.oldBook, MainActivity.hasActiveReadBookRoute)
|
||||
showTitle()
|
||||
initMenu()
|
||||
initRecyclerView()
|
||||
@@ -547,4 +547,4 @@ class ChangeBookSourceDialog() : BaseBottomSheetDialogFragment(R.layout.dialog_b
|
||||
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
|
||||
|
||||
import android.app.Application
|
||||
import android.os.Bundle
|
||||
import io.legado.app.data.appDb
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
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(application: Application) :
|
||||
ChangeBookSourceViewModel(application) {
|
||||
class ChangeChapterSourceViewModel(
|
||||
private val changeSourceSearchUseCase: ChangeSourceSearchUseCase,
|
||||
private val getChapterContentUseCase: GetChapterContentUseCase,
|
||||
private val searchRepository: SearchRepository,
|
||||
) : ViewModel() {
|
||||
|
||||
var chapterIndex: Int = 0
|
||||
var chapterTitle: String = ""
|
||||
private val _uiState = MutableStateFlow(ChangeChapterSourceUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
override fun initData(arguments: Bundle?, book: Book?, fromReadBookActivity: Boolean) {
|
||||
super.initData(arguments, book, fromReadBookActivity)
|
||||
arguments?.let { bundle ->
|
||||
bundle.getString("chapterTitle")?.let {
|
||||
chapterTitle = it
|
||||
private val _effects = MutableSharedFlow<ChangeChapterSourceEffect>(extraBufferCapacity = 16)
|
||||
val effects = _effects.asSharedFlow()
|
||||
|
||||
// Internal state
|
||||
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(
|
||||
book: Book,
|
||||
chapter: BookChapter,
|
||||
nextChapterUrl: String?,
|
||||
success: (content: String) -> Unit,
|
||||
error: (msg: String) -> Unit
|
||||
) {
|
||||
execute {
|
||||
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: throw NoStackTraceException("书源不存在")
|
||||
WebBook.getContentAwait(bookSource, book, chapter, nextChapterUrl, false)
|
||||
}.onSuccess {
|
||||
success.invoke(it)
|
||||
}.onError {
|
||||
error.invoke(it.localizedMessage ?: "获取正文出错")
|
||||
fun initData(book: Book, chapterIndex: Int, chapterTitle: String) {
|
||||
this.oldBook = book
|
||||
this.chapterIndex = chapterIndex
|
||||
this.chapterTitle = chapterTitle
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
showToc = false,
|
||||
tocItems = persistentListOf(),
|
||||
isLoadingToc = false,
|
||||
selectedSourceName = "",
|
||||
)
|
||||
}
|
||||
startSearch()
|
||||
}
|
||||
|
||||
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 ->
|
||||
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 ->
|
||||
startActivity<BookInfoActivity> {
|
||||
putExtra("bookUrl", bookUrl)
|
||||
|
||||
@@ -10,6 +10,8 @@ import io.legado.app.domain.usecase.ChangeSourceMigrationOptions
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
import kotlinx.collections.immutable.persistentListOf
|
||||
|
||||
const val READER_RESULT_DELETED = 100
|
||||
|
||||
data class BookInfoUiState(
|
||||
val book: Book? = null,
|
||||
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.info.edit.BookInfoEditActivity
|
||||
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.toc.TocActivityResult
|
||||
import io.legado.app.ui.config.otherConfig.OtherConfig
|
||||
@@ -48,6 +47,7 @@ fun BookInfoRouteScreen(
|
||||
onBack: () -> Unit,
|
||||
onFinish: (resultCode: Int?, afterTransition: Boolean) -> Unit,
|
||||
onOpenSearch: (String) -> Unit,
|
||||
onOpenReader: (bookUrl: String, inBookshelf: Boolean, chapterChanged: Boolean) -> Unit = { _, _, _ -> },
|
||||
onNavigateToBookInfo: (name: String?, author: String?, bookUrl: String, origin: String?, coverPath: String?) -> Unit = { _, _, _, _, _ -> },
|
||||
onNavigateToExploreShow: (title: String?, sourceUrl: String, exploreUrl: String?) -> Unit = { _, _, _ -> },
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
@@ -123,7 +123,15 @@ fun BookInfoRouteScreen(
|
||||
ReadMangaActivity::class.java
|
||||
}
|
||||
|
||||
else -> ReadBookActivity::class.java
|
||||
else -> null
|
||||
}
|
||||
if (cls == null) {
|
||||
onOpenReader(
|
||||
effect.book.bookUrl,
|
||||
effect.inBookshelf,
|
||||
effect.chapterChanged,
|
||||
)
|
||||
return@collectLatest
|
||||
}
|
||||
readBookResult.launch(
|
||||
Intent(activity, cls).apply {
|
||||
|
||||
@@ -576,7 +576,7 @@ fun ChangeSourceSheet(
|
||||
|
||||
val performAction: (SearchBook, Boolean) -> Unit = { searchBook, replace ->
|
||||
loadingAction = true
|
||||
val book = viewModel.bookMap[searchBook.primaryStr()] ?: searchBook.toBook()
|
||||
val book = viewModel.getBookFromMap(searchBook.primaryStr()) ?: searchBook.toBook()
|
||||
viewModel.getToc(
|
||||
book,
|
||||
onSuccess = { toc, source ->
|
||||
|
||||
@@ -11,7 +11,6 @@ import coil.ImageLoader
|
||||
import coil.request.SuccessResult
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
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.utils.ArchiveUtils
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.ImageSaveUtils
|
||||
import io.legado.app.utils.UrlUtil
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -76,7 +76,6 @@ import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.collections.immutable.toImmutableList
|
||||
import java.io.ByteArrayOutputStream
|
||||
|
||||
class BookInfoViewModel(
|
||||
@@ -346,7 +345,7 @@ class BookInfoViewModel(
|
||||
syncUiState()
|
||||
}
|
||||
|
||||
io.legado.app.ui.book.read.ReadBookActivity.RESULT_DELETED -> {
|
||||
READER_RESULT_DELETED -> {
|
||||
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.ui.book.changesource.ChangeBookSourceDialog
|
||||
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.MangaClickActionConfigDialog
|
||||
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.MangaMenu
|
||||
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.toc.TocActivityResult
|
||||
import io.legado.app.ui.browser.WebViewActivity
|
||||
@@ -185,7 +185,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
private val bookInfoActivity =
|
||||
registerForActivityResult(StartActivityContract(BookInfoActivity::class.java)) {
|
||||
if (it.resultCode == RESULT_OK) {
|
||||
setResult(RESULT_DELETED)
|
||||
setResult(READER_RESULT_DELETED)
|
||||
super.finish()
|
||||
} else {
|
||||
ReadManga.loadOrUpContent()
|
||||
@@ -498,7 +498,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
override fun onColorSelected(dialogId: Int, color: Int){
|
||||
if (dialogId == MANGA_B)
|
||||
{
|
||||
AppConfig.mangaBackground = color
|
||||
viewModel.setMangaBackground(color)
|
||||
setBackground()
|
||||
}
|
||||
}
|
||||
@@ -703,7 +703,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
getString(R.string.pre_download),
|
||||
AppConfig.mangaPreDownloadNum
|
||||
) {
|
||||
AppConfig.mangaPreDownloadNum = it
|
||||
viewModel.setMangaPreDownloadNum(it)
|
||||
item.title = getString(R.string.pre_download_m, it)
|
||||
setRecyclerViewPreloader(it)
|
||||
}
|
||||
@@ -757,20 +757,20 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
|
||||
//点击滑动
|
||||
override fun onClickScrollDisabledChanged(disabled: Boolean) {
|
||||
AppConfig.disableClickScroll = disabled
|
||||
viewModel.setDisableClickScroll(disabled)
|
||||
}
|
||||
|
||||
override fun onScrollAniDisabledChanged(disabled: Boolean) {
|
||||
AppConfig.disableMangaScrollAnimation = disabled
|
||||
viewModel.setDisableMangaScrollAnimation(disabled)
|
||||
}
|
||||
|
||||
override fun onCrossFadeDisabledChanged(disabled: Boolean) {
|
||||
AppConfig.disableMangaCrossFade = disabled
|
||||
viewModel.setDisableMangaCrossFade(disabled)
|
||||
}
|
||||
|
||||
//双击缩放
|
||||
override fun onMangaScaleDisabledChanged(disabled: Boolean) {
|
||||
AppConfig.disableMangaScale = disabled
|
||||
viewModel.setDisableMangaScale(disabled)
|
||||
setDisableMangaScale(disabled)
|
||||
}
|
||||
|
||||
@@ -784,16 +784,13 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
|
||||
//墨水屏
|
||||
override fun updateEpaperMode(enabled: Boolean, threshold: Int) {
|
||||
AppConfig.enableMangaEInk = enabled
|
||||
AppConfig.enableMangaGray = false
|
||||
AppConfig.mangaEInkThreshold = threshold
|
||||
viewModel.setEInkMode(enabled, threshold)
|
||||
mAdapter.enableMangaEInk(enabled, threshold)
|
||||
}
|
||||
|
||||
//灰度
|
||||
override fun updateGrayMode(enabled: Boolean) {
|
||||
AppConfig.enableMangaGray = enabled
|
||||
AppConfig.enableMangaEInk = false
|
||||
viewModel.setGrayMode(enabled)
|
||||
mAdapter.enableGray(enabled)
|
||||
}
|
||||
|
||||
@@ -811,7 +808,7 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
//自动翻页速度
|
||||
override fun onAutoPageSpeedChanged(speed: Int) {
|
||||
setAutoReadEnabled(false)
|
||||
AppConfig.mangaAutoPageSpeed = speed
|
||||
viewModel.setMangaAutoPageSpeed(speed)
|
||||
mScrollTimer.setSpeed(speed)
|
||||
setAutoReadEnabled(enableScroll)
|
||||
// if (enableAutoScrollPage) {
|
||||
@@ -820,19 +817,19 @@ class ReadMangaActivity : VMBaseActivity<ActivityMangaBinding, ReadMangaViewMode
|
||||
}
|
||||
|
||||
override fun onMangaLongClickChanged(checked: Boolean) {
|
||||
AppConfig.mangaLongClick = checked
|
||||
viewModel.setMangaLongClick(checked)
|
||||
}
|
||||
|
||||
override fun onVolumeKeyPageChanged(enable: Boolean) {
|
||||
AppConfig.MangaVolumeKeyPage = enable
|
||||
viewModel.setMangaVolumeKeyPage(enable)
|
||||
}
|
||||
|
||||
override fun onReverseVolumeKeyPageChanged(enable: Boolean) {
|
||||
AppConfig.reverseVolumeKeyPage = enable
|
||||
viewModel.setReverseVolumeKeyPage(enable)
|
||||
}
|
||||
|
||||
override fun onHideMangaTitleChanged(hide: Boolean) {
|
||||
AppConfig.hideMangaTitle = hide
|
||||
viewModel.setHideMangaTitle(hide)
|
||||
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.BookChapter
|
||||
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.usecase.GetReadingProgressUseCase
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -50,7 +51,8 @@ import java.io.ByteArrayOutputStream
|
||||
|
||||
class ReadMangaViewModel(
|
||||
application: Application,
|
||||
private val getReadingProgressUseCase: GetReadingProgressUseCase
|
||||
private val getReadingProgressUseCase: GetReadingProgressUseCase,
|
||||
private val mangaSettingsRepository: MangaSettingsRepository
|
||||
) : BaseViewModel(application) {
|
||||
|
||||
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() {
|
||||
super.onCleared()
|
||||
changeSourceCoroutine?.cancel()
|
||||
|
||||
+43
-11
@@ -8,15 +8,17 @@ 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.data.repository.MangaSettingsRepository
|
||||
import io.legado.app.databinding.DialogClickActionConfigBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.lib.dialogs.selector
|
||||
import io.legado.app.utils.getCompatColor
|
||||
import io.legado.app.utils.putPrefInt
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_click_action_config) {
|
||||
private val binding by viewBinding(DialogClickActionConfigBinding::bind)
|
||||
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||
|
||||
private val actions by lazy {
|
||||
linkedMapOf(
|
||||
@@ -66,68 +68,85 @@ class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_c
|
||||
|
||||
binding.tvTopLeft.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionTL, action)
|
||||
setClickAction(PreferKey.mangaClickActionTL, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvTopCenter.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionTC, action)
|
||||
setClickAction(PreferKey.mangaClickActionTC, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvTopRight.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionTR, action)
|
||||
setClickAction(PreferKey.mangaClickActionTR, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvMiddleLeft.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionML, action)
|
||||
setClickAction(PreferKey.mangaClickActionML, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvMiddleCenter.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionMC, action)
|
||||
setClickAction(PreferKey.mangaClickActionMC, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvMiddleRight.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionMR, action)
|
||||
setClickAction(PreferKey.mangaClickActionMR, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvBottomLeft.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionBL, action)
|
||||
setClickAction(PreferKey.mangaClickActionBL, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvBottomCenter.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionBC, action)
|
||||
setClickAction(PreferKey.mangaClickActionBC, action)
|
||||
(it as? TextView)?.text = actions[action]
|
||||
}
|
||||
}
|
||||
|
||||
binding.tvBottomRight.setOnClickListener {
|
||||
selectAction { action ->
|
||||
putPrefInt(PreferKey.mangaClickActionBR, action)
|
||||
setClickAction(PreferKey.mangaClickActionBR, 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) {
|
||||
context?.selector(
|
||||
getString(R.string.select_action),
|
||||
@@ -138,7 +157,20 @@ class MangaClickActionConfigDialog : BaseOverlayDialogFragment(R.layout.dialog_c
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
if (!hasMenuClickArea()) {
|
||||
AppConfig.detectMangaClickArea()
|
||||
execute {
|
||||
mangaSettingsRepository.setMangaClickAction(PreferKey.mangaClickActionMC, 0)
|
||||
}
|
||||
}
|
||||
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 io.legado.app.R
|
||||
import io.legado.app.base.BaseBottomSheetDialogFragment
|
||||
import io.legado.app.data.repository.MangaSettingsRepository
|
||||
import io.legado.app.databinding.DialogMangaColorFilterBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
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.viewbindingdelegate.viewBinding
|
||||
import io.legado.app.utils.visible
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_manga_color_filter) {
|
||||
private val binding by viewBinding(DialogMangaColorFilterBinding::bind)
|
||||
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||
private val mConfig =
|
||||
GSON.fromJsonObject<MangaColorFilterConfig>(AppConfig.mangaColorFilter).getOrNull()
|
||||
?: MangaColorFilterConfig()
|
||||
@@ -114,8 +117,13 @@ class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_man
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
AppConfig.mangaColorFilter = mConfig.toJson()
|
||||
val colorFilter = mConfig.toJson()
|
||||
AppConfig.mangaColorFilter = colorFilter
|
||||
AppConfig.mangaEInkThreshold = mMangaEInkThreshold
|
||||
execute {
|
||||
mangaSettingsRepository.setMangaAutoColorFilter(colorFilter)
|
||||
mangaSettingsRepository.setMangaEInkThreshold(mMangaEInkThreshold)
|
||||
}
|
||||
}
|
||||
|
||||
interface Callback {
|
||||
@@ -124,4 +132,4 @@ class MangaColorFilterDialog : BaseBottomSheetDialogFragment(R.layout.dialog_man
|
||||
fun updateGrayMode(enabled: Boolean)
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ 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.data.repository.MangaSettingsRepository
|
||||
import io.legado.app.databinding.DialogMangaFooterSettingBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
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.toastOnUi
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
class MangaFooterSettingDialog :
|
||||
BaseBottomSheetDialogFragment(R.layout.dialog_manga_footer_setting) {
|
||||
@@ -37,6 +39,7 @@ class MangaFooterSettingDialog :
|
||||
var callback: Callback? = null
|
||||
|
||||
private val binding by viewBinding(DialogMangaFooterSettingBinding::bind)
|
||||
private val mangaSettingsRepository by inject<MangaSettingsRepository>()
|
||||
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
@@ -255,7 +258,11 @@ class MangaFooterSettingDialog :
|
||||
|
||||
override fun onDismiss(dialog: DialogInterface) {
|
||||
super.onDismiss(dialog)
|
||||
AppConfig.mangaFooterConfig = GSON.toJson(config)
|
||||
val footerConfig = GSON.toJson(config)
|
||||
AppConfig.mangaFooterConfig = footerConfig
|
||||
execute {
|
||||
mangaSettingsRepository.setMangaFooterConfig(footerConfig)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateChapterText() {
|
||||
@@ -311,4 +318,4 @@ class MangaFooterSettingDialog :
|
||||
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.ReadManga
|
||||
import io.legado.app.ui.browser.WebViewActivity
|
||||
import io.legado.app.utils.ConstraintModify
|
||||
import io.legado.app.utils.activity
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
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.startActivity
|
||||
import io.legado.app.utils.visible
|
||||
@@ -125,27 +123,12 @@ class MangaMenu @JvmOverloads constructor(
|
||||
} else {
|
||||
titleBarAddition.gone()
|
||||
}
|
||||
upBrightnessVwPos()
|
||||
/**
|
||||
* 确保视图不被导航栏遮挡
|
||||
*/
|
||||
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() {
|
||||
menuTopIn.setAnimationListener(menuInListener)
|
||||
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.base.adapter.ItemViewHolder
|
||||
import io.legado.app.base.adapter.RecyclerAdapter
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.databinding.ItemTextBinding
|
||||
import io.legado.app.databinding.PopupActionMenuBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.isAbsUrl
|
||||
import io.legado.app.utils.printOnDebug
|
||||
@@ -34,7 +32,11 @@ import androidx.core.net.toUri
|
||||
import io.legado.app.constant.AppLog
|
||||
|
||||
@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) {
|
||||
|
||||
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 visibleMenuItems = arrayListOf<MenuItemImpl>()
|
||||
private val moreMenuItems = arrayListOf<MenuItemImpl>()
|
||||
private val expandTextMenu get() = context.getPrefBoolean(PreferKey.expandTextMenu)
|
||||
|
||||
init {
|
||||
@SuppressLint("InflateParams")
|
||||
@@ -66,7 +67,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerViewMore.adapter = adapter
|
||||
setOnDismissListener {
|
||||
if (!context.getPrefBoolean(PreferKey.expandTextMenu)) {
|
||||
if (!expandTextMenu()) {
|
||||
binding.ivMenuMore.setImageResource(R.drawable.ic_more_vert)
|
||||
binding.recyclerViewMore.gone()
|
||||
adapter.setItems(visibleMenuItems)
|
||||
@@ -90,7 +91,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
||||
}
|
||||
|
||||
fun upMenu() {
|
||||
if (expandTextMenu) {
|
||||
if (expandTextMenu()) {
|
||||
adapter.setItems(menuItems)
|
||||
binding.ivMenuMore.gone()
|
||||
} else {
|
||||
@@ -108,7 +109,7 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
||||
endX: Int,
|
||||
endBottomY: Int
|
||||
) {
|
||||
if (expandTextMenu) {
|
||||
if (expandTextMenu()) {
|
||||
when {
|
||||
startTopY > 500 -> {
|
||||
showAtLocation(
|
||||
@@ -272,4 +273,4 @@ class TextActionMenu(private val context: Context, private val callBack: CallBac
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user