更多自定义设置 @325506 (#994)
* 第一次上传 * 新增深度个性化设置-可以给主题选择自定义字体了 * 新增深度个性化设置-存在背景图片时,应用栏和导航栏会对背景图片进行模糊处理 * 新增深度个性化设置-主题颜色高度自定义 * 新增深度个性化设置-新增边框设置 * 合并后bug修复 # Conflicts: # app/build.gradle.kts # app/google-services.json # app/src/main/java/io/legado/app/constant/PreferKey.kt # app/src/main/java/io/legado/app/di/appModule.kt # app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt # app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt # app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt # app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt # app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt # app/src/main/java/io/legado/app/ui/book/manga/config/MangaClickActionConfigDialog.kt # app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt # app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt # app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt # app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt # app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt # app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt # app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt # app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt # app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt # app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt # app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt # app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt # app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt # app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt # app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt # app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt # app/src/main/java/io/legado/app/ui/main/MainActivity.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt # app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt # app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt # app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt # app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt # app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt # app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt # app/src/main/java/io/legado/app/ui/theme/AppBackground.kt # app/src/main/java/io/legado/app/ui/theme/AppTheme.kt # app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt # app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt # app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt # app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt # app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt # app/src/main/java/io/legado/app/ui/theme/Typography.kt # app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt # app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt # app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt # app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt # app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt # app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt # app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt # app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt # app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt # app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt # app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/SettingCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt # app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt # app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt # app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt # app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt # app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt # app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt # app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt # app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt # app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassMediumFlexibleTopAppBar.kt # app/src/main/res/drawable/ic_add.xml # app/src/main/res/drawable/ic_add_online.xml # app/src/main/res/drawable/ic_arrange.xml # app/src/main/res/drawable/ic_arrow_down.xml # app/src/main/res/drawable/ic_arrow_right.xml # app/src/main/res/drawable/ic_cfg_about.xml # app/src/main/res/drawable/ic_cfg_backup.xml # app/src/main/res/drawable/ic_cfg_other.xml # app/src/main/res/drawable/ic_cfg_replace.xml # app/src/main/res/drawable/ic_cfg_source.xml # app/src/main/res/drawable/ic_cfg_theme.xml # app/src/main/res/drawable/ic_cfg_web.xml # app/src/main/res/drawable/ic_groups.xml # app/src/main/res/drawable/ic_help.xml # app/src/main/res/drawable/ic_more.xml # app/src/main/res/drawable/ic_search.xml # app/src/main/res/drawable/ic_view_quilt.xml # app/src/main/res/layout/dialog_tip_config.xml # app/src/main/res/layout/view_preference.xml # app/src/main/res/layout/view_preference_category.xml # app/src/main/res/values-zh-rCN/strings.xml # app/src/main/res/values-zh-rHK/strings.xml # app/src/main/res/values-zh-rTW/strings.xml # app/src/main/res/values/arrays.xml # app/src/main/res/values/strings.xml # app/src/main/res/xml/pref_main.xml # avd.sh # gradle.properties # gradle/libs.versions.toml # gradle/wrapper/gradle-wrapper.properties # gradlew # modules/web/index.html * 边框设置bug修复 # Conflicts: # app/build.gradle.kts # app/google-services.json # app/src/main/java/io/legado/app/constant/PreferKey.kt # app/src/main/java/io/legado/app/di/appModule.kt # app/src/main/java/io/legado/app/ui/book/bookmark/AllBookmarkScreen.kt # app/src/main/java/io/legado/app/ui/book/explore/ExploreShowScreen.kt # app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt # app/src/main/java/io/legado/app/ui/book/import/local/ImportBookScreen.kt # app/src/main/java/io/legado/app/ui/book/import/remote/RemoteBookScreen.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoContract.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoScreen.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoSheets.kt # app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt # app/src/main/java/io/legado/app/ui/book/manga/config/MangaClickActionConfigDialog.kt # app/src/main/java/io/legado/app/ui/book/read/config/ClickActionConfigDialog.kt # app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordScreen.kt # app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt # app/src/main/java/io/legado/app/ui/book/search/SearchScope.kt # app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt # app/src/main/java/io/legado/app/ui/book/searchContent/SearchContentScreen.kt # app/src/main/java/io/legado/app/ui/book/toc/TocScreen.kt # app/src/main/java/io/legado/app/ui/book/toc/rule/TxtRuleScreen.kt # app/src/main/java/io/legado/app/ui/config/ConfigNavScreen.kt # app/src/main/java/io/legado/app/ui/config/coverConfig/CoverConfigScreen.kt # app/src/main/java/io/legado/app/ui/config/otherConfig/CheckSourceBottomSheet.kt # app/src/main/java/io/legado/app/ui/config/otherConfig/DirectLinkUploadBottomSheet.kt # app/src/main/java/io/legado/app/ui/config/readConfig/PageKeySheet.kt # app/src/main/java/io/legado/app/ui/config/themeConfig/BackgroundImageManageSheet.kt # app/src/main/java/io/legado/app/ui/config/themeConfig/ThemeConfig.kt # app/src/main/java/io/legado/app/ui/dict/rule/DictRuleScreen.kt # app/src/main/java/io/legado/app/ui/main/MainActivity.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt # app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageSheet.kt # app/src/main/java/io/legado/app/ui/main/explore/ExploreScreen.kt # app/src/main/java/io/legado/app/ui/main/my/MyScreen.kt # app/src/main/java/io/legado/app/ui/replace/edit/ReplaceEditScreen.kt # app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesScreen.kt # app/src/main/java/io/legado/app/ui/rss/subscription/RuleSubScreen.kt # app/src/main/java/io/legado/app/ui/theme/AdaptivePadding.kt # app/src/main/java/io/legado/app/ui/theme/AppBackground.kt # app/src/main/java/io/legado/app/ui/theme/AppTheme.kt # app/src/main/java/io/legado/app/ui/theme/HazeStyle.kt # app/src/main/java/io/legado/app/ui/theme/ImageSeedColorExtractor.kt # app/src/main/java/io/legado/app/ui/theme/LegadoTheme.kt # app/src/main/java/io/legado/app/ui/theme/ThemeColorSchemeOverride.kt # app/src/main/java/io/legado/app/ui/theme/ThemeOverride.kt # app/src/main/java/io/legado/app/ui/theme/Typography.kt # app/src/main/java/io/legado/app/ui/theme/hazeStyle/HazeLegado.kt # app/src/main/java/io/legado/app/ui/widget/components/AppNavigationBar.kt # app/src/main/java/io/legado/app/ui/widget/components/AppScaffold.kt # app/src/main/java/io/legado/app/ui/widget/components/AppTextField.kt # app/src/main/java/io/legado/app/ui/widget/components/SearchBarSection.kt # app/src/main/java/io/legado/app/ui/widget/components/SplicedColumnGroup.kt # app/src/main/java/io/legado/app/ui/widget/components/alert/AppAlertDialog.kt # app/src/main/java/io/legado/app/ui/widget/components/button/SmallIconButton.kt # app/src/main/java/io/legado/app/ui/widget/components/button/SmallTextButton.kt # app/src/main/java/io/legado/app/ui/widget/components/button/ToggleChip.kt # app/src/main/java/io/legado/app/ui/widget/components/card/GlassCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/SelectionItemCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/SettingCard.kt # app/src/main/java/io/legado/app/ui/widget/components/card/TextCard.kt # app/src/main/java/io/legado/app/ui/widget/components/checkBox/CheckboxItem.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/BookCover.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/BookshelfCover.kt # app/src/main/java/io/legado/app/ui/widget/components/cover/Cover.kt # app/src/main/java/io/legado/app/ui/widget/components/icon/AppIcons.kt # app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenu.kt # app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt # app/src/main/java/io/legado/app/ui/widget/components/rules/RuleListScaffold.kt # app/src/main/java/io/legado/app/ui/widget/components/settingItem/ClickableSettingItem.kt # app/src/main/java/io/legado/app/ui/widget/components/settingItem/SliderSettingItem.kt # app/src/main/java/io/legado/app/ui/widget/components/topbar/DynamicTopAppBar.kt # app/src/main/java/io/legado/app/ui/widget/components/topbar/GlassMediumFlexibleTopAppBar.kt # app/src/main/res/drawable/ic_add.xml # app/src/main/res/drawable/ic_add_online.xml # app/src/main/res/drawable/ic_arrange.xml # app/src/main/res/drawable/ic_arrow_down.xml # app/src/main/res/drawable/ic_arrow_right.xml # app/src/main/res/drawable/ic_cfg_about.xml # app/src/main/res/drawable/ic_cfg_backup.xml # app/src/main/res/drawable/ic_cfg_other.xml # app/src/main/res/drawable/ic_cfg_replace.xml # app/src/main/res/drawable/ic_cfg_source.xml # app/src/main/res/drawable/ic_cfg_theme.xml # app/src/main/res/drawable/ic_cfg_web.xml # app/src/main/res/drawable/ic_groups.xml # app/src/main/res/drawable/ic_help.xml # app/src/main/res/drawable/ic_more.xml # app/src/main/res/drawable/ic_search.xml # app/src/main/res/drawable/ic_view_quilt.xml # app/src/main/res/layout/dialog_tip_config.xml # app/src/main/res/layout/view_preference.xml # app/src/main/res/layout/view_preference_category.xml # app/src/main/res/values-zh-rCN/strings.xml # app/src/main/res/values-zh-rHK/strings.xml # app/src/main/res/values-zh-rTW/strings.xml # app/src/main/res/values/arrays.xml # app/src/main/res/values/strings.xml # app/src/main/res/xml/pref_main.xml # avd.sh # gradle.properties # gradle/libs.versions.toml # gradle/wrapper/gradle-wrapper.properties # gradlew # modules/web/index.html * 实现播放音频显示歌词,但是布局需要调整 * 优化音频显示布局 * 合并代码 * 合并主题色设置,新增主题保存 * 修改一下颜色设置 * 边框bug修复 * 悬浮胶囊主题颜色修复 * 给发现和订阅项增加边框 * 内置浏览器设置非全屏 * 实现正则表达式匹配字段高亮 * 增加自定义正则表达式匹配颜色 * 正则表达式配置持久化保存 * 更新下载路径 * 修复png透明图片 * 修改当前版本号 * 同步lyc版本订阅源功能 * 同步lyc版本订阅源功能 * 合并后修复 * 书架样式调整,但有bug * 合并后提交 * bug修复 * 订阅源日志修复 * 合并后修复 * 正文标题可以自定义字体 * 列表模式可以显示更多内容 * 实现自定义标签 * 主题背景和封面背景支持动图 * 自定义标签颜色 * 自定义标签颜色 * 主题保存bug修复 * 实现个性化主题设置的备份与恢复 * 页眉页脚可自定义字号,颜色,字体 * 边距设置 * bug修复 * 主题应用bug修复 * 虚线延展 * 模糊效果设置 * 字体页面新增搜索 * 描述性文字添加 * 列表简介内容显示 * 主题导入bug修复 * 标题可以自定义字体颜色 * 匹配自定义正则表达式规则文字的字体和颜色 * 匹配自定义正则表达式规则文字的字体和颜色bug修复 * 自定义底栏图标 * 自定义底栏图标bug修复 * 实现主题设置统一管理 * 实现主题设置统一管理界面美化 * 文件夹列表模式显示美化 * 底栏bug修复 * 底栏bug修复 * 底栏液化玻璃参数提取 * 主题管理删除当前阅读背景保存 * 主题管理bug修复 * 使用自定义封面时不动态取色 * 规范性更改 * 正确的数据库迁移schema * 移动设置项 * 书架优化 * 书架优化 * 删除 EmptyMessageView.kt — 已有 EmptyMessage 完全覆盖且功能更全,该文件为无效重复 EmptyMessageView 与 EmptyMessage 实现了完全相同的空状态 UI(Kaomoji 动画、加载指示器、消息文字),但 EmptyMessageView 缺少 buttonText/buttonImageVector/onButtonClick 参数,无任何调用者。 * 消除重复代码:提取共享的 loadFontFiles、JsonObject 扩展函数,删除无效重复的 EmptyMessageView 和 BookGroupUi 常量 - 删除 EmptyMessageView.kt(已有 EmptyMessage 完全覆盖,零调用者) - 提取 loadFontFiles 到 help/FontLoader.kt(消除 ThemeConfigScreen 和 FontSelectScreen 间的重复) - 提取 JsonObject.getString/getInt/getFloat/getBoolean 到 utils/JsonExtensions.kt(消除 3 个配置类中的三份相同实现) - 移除 BookGroupUi 中与 BookGroup 实体重复的 12 个 Id* 常量,改用直接引用 * 拆分大文件:提取 ThemePackSerializer 和 ThemeAppearanceJson 到独立文件 - 提取 ThemePackSerializer 到 help/config/ThemePackSerializer.kt(106 行),从 ThemePackConfig.kt 移除 - 提取 ThemeAppearanceJson 序列化对象到 help/config/ThemeAppearanceJson.kt(140 行),从 ThemeAppearanceConfig.kt 移除 - ThemePackConfig 从 440 行减少到约 325 行,ThemeAppearanceConfig 从 267 行减少到约 117 行 BookshelfViewModel 的拆分留待后续——涉及状态管理 refactor 需要更深入的并发模型理解。 * 提取共用模式:添加 SplicedColumnDivider 和 secondaryColorOr 共用函数 - SplicedColumnGroup.kt 新增 SplicedColumnDivider() composable,消除 5 个 SettingItem 文件中各 7 行重复的 divider 前导代码 - GlassDefaults.kt 新增 secondaryColorOr() composable,统一处理深度个性化 secondaryThemeColor 覆盖逻辑(该模式曾出现 12 次) TypefaceLoader 提取跳过——三个调用点(PageView/TextColumn/ChapterProvider)在缓存策略和系统字体回退上存在差异,需更仔细的并发分析。 * 修复架构问题:移动 RegexColorRule 和 TagColorGenerator 到正确包,提取 Haze containerColor - 将 RegexColorRule 从 ui/book/read/config 移至 help/config,消除 ReadBookConfig 对 UI 层的反向依赖 - 将 TagColorGenerator 从 help 移至 help/config,消除对 ui.config.themeConfig 的跨层级依赖 - HazeStyle 中 3 处重复的 containerColor 解析改用 GlassDefaults.secondaryColorOr * 书架优化 * 书架优化 * 书架优化 --------- Co-authored-by: HapeLee <63206378+HapeLee@users.noreply.github.com>
This commit is contained in:
1 parent
46c9496bdc
commit
64f7de8120
131 files changed
+8956
-1126
No files matched your search
@@ -14,7 +14,37 @@
|
|||||||
"Bash(cp -r \"D:/AndroidPrj/legado-with-MD3/.codex/skills/legado-compose-review/references\" \"D:/AndroidPrj/legado-with-MD3/.claude/skills/legado-compose-review/\")",
|
"Bash(cp -r \"D:/AndroidPrj/legado-with-MD3/.codex/skills/legado-compose-review/references\" \"D:/AndroidPrj/legado-with-MD3/.claude/skills/legado-compose-review/\")",
|
||||||
"WebSearch",
|
"WebSearch",
|
||||||
"Bash(./gradlew assembleAppDebug)",
|
"Bash(./gradlew assembleAppDebug)",
|
||||||
"Bash(git checkout *)"
|
"Bash(git checkout *)",
|
||||||
|
"Bash(./gradlew :app:compileAppDebugKotlin --no-daemon)",
|
||||||
|
"Bash(Select-Object -Last 50)",
|
||||||
|
"PowerShell(./gradlew :app:compileAppDebugKotlin --no-daemon 2>&1 | Select-Object -Last 50)",
|
||||||
|
"PowerShell(./gradlew :app:compileAppDebugKotlin --rerun-tasks 2>&1 | Select-Object -Last 30)",
|
||||||
|
"PowerShell(./gradlew :app:compileAppDebugKotlin --rerun-tasks 2>&1 | Select-Object -Last 40)",
|
||||||
|
"PowerShell(./gradlew :app:compileAppDebugKotlin --rerun-tasks 2>&1 | Select-Object -Last 15)",
|
||||||
|
"PowerShell(./gradlew :app:compileAppDebugKotlin --rerun-tasks 2>&1 | Select-Object -Last 5)",
|
||||||
|
"Bash(./gradlew assembleAppNoR8)",
|
||||||
|
"Bash(.\\\\gradlew.bat :app:compileAppDebugKotlin)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 30)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 15)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 10)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1 | Select-Object -Last 8)",
|
||||||
|
"Bash(Get-ChildItem \"app/src/main/java/io/legado/app/ui/main/bookshelf/\" -Name)",
|
||||||
|
"Bash(Sort-Object)",
|
||||||
|
"Bash(git rm *)",
|
||||||
|
"Bash(git commit *)",
|
||||||
|
"Bash(Select-Object -Last 30)",
|
||||||
|
"Bash(./gradlew.bat :app:compileAppDebugKotlin)",
|
||||||
|
"Bash(git add *)",
|
||||||
|
"Bash(git mv *)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin 2>&1)",
|
||||||
|
"WebFetch(domain:cs.android.com)",
|
||||||
|
"WebFetch(domain:github.com)",
|
||||||
|
"PowerShell(Get-ChildItem *)",
|
||||||
|
"PowerShell(Get-ChildItem -Path \"D:\\\\Android\\\\.gradle\\\\caches\\\\modules-2\\\\files-2.1\\\\androidx.navigation3\\\\navigation3-runtime-android\\\\1.0.1\" -Recurse | ForEach-Object { $_.FullName + \" \\(\" + $_.Length + \" bytes\\)\" })",
|
||||||
|
"Bash(jar tf *)",
|
||||||
|
"Read(//d/tmp/**)",
|
||||||
|
"Bash(jar xf *)",
|
||||||
|
"PowerShell(.\\\\gradlew.bat :app:compileAppDebugKotlin)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -40,6 +40,9 @@ Transform tasks into verifiable goals:
|
|||||||
## Build / Test / Run
|
## Build / Test / Run
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)
|
||||||
|
.\gradlew.bat :app:compileAppDebugKotlin
|
||||||
|
|
||||||
# Assemble all variants
|
# Assemble all variants
|
||||||
./gradlew assembleAppRelease
|
./gradlew assembleAppRelease
|
||||||
|
|
||||||
|
|||||||
@@ -278,5 +278,6 @@ dependencies {
|
|||||||
implementation(libs.miuix.shapes.android)
|
implementation(libs.miuix.shapes.android)
|
||||||
implementation(libs.capsule)
|
implementation(libs.capsule)
|
||||||
implementation(libs.backdrop)
|
implementation(libs.backdrop)
|
||||||
|
implementation(libs.lyricViewx)
|
||||||
implementation(libs.timber)
|
implementation(libs.timber)
|
||||||
}
|
}
|
||||||
File diff suppressed because it is too large.
Load diff
@@ -4,6 +4,7 @@ package io.legado.app.constant
|
|||||||
object PreferKey {
|
object PreferKey {
|
||||||
const val language = "language"
|
const val language = "language"
|
||||||
const val fontScale = "fontScale"
|
const val fontScale = "fontScale"
|
||||||
|
const val appFontPath = "appFontPath"
|
||||||
const val appTheme = "app_theme"
|
const val appTheme = "app_theme"
|
||||||
const val themeMode = "themeMode"
|
const val themeMode = "themeMode"
|
||||||
const val userAgent = "userAgent"
|
const val userAgent = "userAgent"
|
||||||
@@ -101,6 +102,7 @@ object PreferKey {
|
|||||||
const val replaceEnableDefault = "replaceEnableDefault"
|
const val replaceEnableDefault = "replaceEnableDefault"
|
||||||
const val showBrightnessView = "showBrightnessView"
|
const val showBrightnessView = "showBrightnessView"
|
||||||
const val useUnderline = "useUnderline"
|
const val useUnderline = "useUnderline"
|
||||||
|
const val regexColorRules = "regexColorRules"
|
||||||
const val adaptSpecialStyle = "adaptSpecialStyle"
|
const val adaptSpecialStyle = "adaptSpecialStyle"
|
||||||
const val autoClearExpired = "autoClearExpired"
|
const val autoClearExpired = "autoClearExpired"
|
||||||
const val autoChangeSource = "autoChangeSource"
|
const val autoChangeSource = "autoChangeSource"
|
||||||
@@ -193,9 +195,32 @@ object PreferKey {
|
|||||||
const val bottomBarOpacity = "bottomBarOpacity"
|
const val bottomBarOpacity = "bottomBarOpacity"
|
||||||
const val enableBlur = "enableBlur"
|
const val enableBlur = "enableBlur"
|
||||||
const val enableProgressiveBlur = "enableProgressiveBlur"
|
const val enableProgressiveBlur = "enableProgressiveBlur"
|
||||||
|
const val topBarBlurRadius = "topBarBlurRadius"
|
||||||
|
const val bottomBarBlurRadius = "bottomBarBlurRadius"
|
||||||
|
const val topBarBlurAlpha = "topBarBlurAlpha"
|
||||||
|
const val bottomBarBlurAlpha = "bottomBarBlurAlpha"
|
||||||
|
const val bottomBarLensRadius = "bottomBarLensRadius"
|
||||||
const val useFlexibleTopAppBar = "useFlexibleTopAppBar"
|
const val useFlexibleTopAppBar = "useFlexibleTopAppBar"
|
||||||
const val cBackground = "colorBackground"
|
const val cBackground = "colorBackground"
|
||||||
const val cBBackground = "colorBottomBackground"
|
const val cBBackground = "colorBottomBackground"
|
||||||
|
const val enableDeepPersonalization = "enableDeepPersonalization"
|
||||||
|
const val themeColor = "colorMD3Primary"
|
||||||
|
const val secondaryThemeColor = "colorMD3Secondary"
|
||||||
|
const val primaryTextColor = "colorMD3OnSurface"
|
||||||
|
const val secondaryTextColor = "colorMD3OnPrimaryContainer"
|
||||||
|
const val themeBackgroundColor = "colorMD3Background"
|
||||||
|
const val labelContainerColor = "colorMD3SurfaceContainerLow"
|
||||||
|
const val enableContainerBorder = "enableContainerBorder"
|
||||||
|
const val containerBorderWidth = "containerBorderWidth"
|
||||||
|
const val containerBorderStyle = "containerBorderStyle"
|
||||||
|
const val containerBorderColor = "containerBorderColor"
|
||||||
|
const val containerBorderDashWidth = "containerBorderDashWidth"
|
||||||
|
// 中间单线间隔设置
|
||||||
|
const val enableItemDivider = "enableItemDivider"
|
||||||
|
const val itemDividerWidth = "itemDividerWidth"
|
||||||
|
const val itemDividerLength = "itemDividerLength"
|
||||||
|
const val itemDividerColor = "itemDividerColor"
|
||||||
|
const val bookInfoInputColor = "colorBookInfoInput"
|
||||||
const val bgImage = "backgroundImage"
|
const val bgImage = "backgroundImage"
|
||||||
const val bgImageBlurring = "backgroundImageBlurring"
|
const val bgImageBlurring = "backgroundImageBlurring"
|
||||||
const val cNPrimary = "colorPrimaryNight"
|
const val cNPrimary = "colorPrimaryNight"
|
||||||
@@ -274,8 +299,16 @@ object PreferKey {
|
|||||||
|
|
||||||
const val showTip = "showTip"
|
const val showTip = "showTip"
|
||||||
const val showBookCount = "showBookCount"
|
const val showBookCount = "showBookCount"
|
||||||
|
const val showBookIntro = "showBookIntro"
|
||||||
|
const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines"
|
||||||
const val sliderVibrator = "sliderVibrator"
|
const val sliderVibrator = "sliderVibrator"
|
||||||
const val selectVibrator = "selectVibrator"
|
const val selectVibrator = "selectVibrator"
|
||||||
const val audioCacheCleanTime = "audioCacheCleanTime"
|
const val audioCacheCleanTime = "audioCacheCleanTime"
|
||||||
const val audioPreDownloadNum = "audioPreDownloadNum"
|
const val audioPreDownloadNum = "audioPreDownloadNum"
|
||||||
|
const val customTagColors = "customTagColors"
|
||||||
|
const val enableCustomTagColors = "enableCustomTagColors"
|
||||||
|
const val navIconBookshelf = "navIconBookshelf"
|
||||||
|
const val navIconExplore = "navIconExplore"
|
||||||
|
const val navIconRss = "navIconRss"
|
||||||
|
const val navIconMy = "navIconMy"
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 85,
|
version = 86,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
@@ -121,7 +121,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 81, to = 82),
|
AutoMigration(from = 81, to = 82),
|
||||||
AutoMigration(from = 82, to = 83),
|
AutoMigration(from = 82, to = 83),
|
||||||
AutoMigration(from = 83, to = 84),
|
AutoMigration(from = 83, to = 84),
|
||||||
AutoMigration(from = 84, to = 85)
|
AutoMigration(from = 84, to = 85),
|
||||||
|
AutoMigration(from = 85, to = 86)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ import io.legado.app.ui.main.bookshelf.BookShelfItem
|
|||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
|
|
||||||
|
data class GroupBookCount(
|
||||||
|
val groupId: Long,
|
||||||
|
val count: Int
|
||||||
|
)
|
||||||
|
|
||||||
@Dao
|
@Dao
|
||||||
interface BookDao {
|
interface BookDao {
|
||||||
|
|
||||||
@@ -91,7 +96,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
where type & ${BookType.text} > 0
|
where type & ${BookType.text} > 0
|
||||||
and type & ${BookType.local} = 0
|
and type & ${BookType.local} = 0
|
||||||
@@ -125,7 +132,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
ORDER BY durChapterTime DESC
|
ORDER BY durChapterTime DESC
|
||||||
"""
|
"""
|
||||||
@@ -156,7 +165,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE type & ${BookType.audio} > 0
|
WHERE type & ${BookType.audio} > 0
|
||||||
"""
|
"""
|
||||||
@@ -187,7 +198,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE type & ${BookType.local} > 0
|
WHERE type & ${BookType.local} > 0
|
||||||
"""
|
"""
|
||||||
@@ -223,7 +236,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
|
where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
|
||||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
||||||
@@ -260,7 +275,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
where type & ${BookType.local} > 0
|
where type & ${BookType.local} > 0
|
||||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
||||||
@@ -292,7 +309,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE (`group` & :group) > 0
|
WHERE (`group` & :group) > 0
|
||||||
"""
|
"""
|
||||||
@@ -325,7 +344,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
|
WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
|
||||||
"""
|
"""
|
||||||
@@ -356,7 +377,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
where type & ${BookType.updateError} > 0
|
where type & ${BookType.updateError} > 0
|
||||||
order by durChapterTime desc
|
order by durChapterTime desc
|
||||||
@@ -388,7 +411,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE durChapterIndex = 0 AND durChapterPos = 0
|
WHERE durChapterIndex = 0 AND durChapterPos = 0
|
||||||
"""
|
"""
|
||||||
@@ -419,7 +444,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
|
WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
|
||||||
"""
|
"""
|
||||||
@@ -450,7 +477,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
|
WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
|
||||||
"""
|
"""
|
||||||
@@ -481,7 +510,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE type & ${BookType.image} > 0
|
WHERE type & ${BookType.image} > 0
|
||||||
"""
|
"""
|
||||||
@@ -512,7 +543,9 @@ interface BookDao {
|
|||||||
type,
|
type,
|
||||||
`group`,
|
`group`,
|
||||||
`order`,
|
`order`,
|
||||||
canUpdate
|
canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro,
|
||||||
|
kind
|
||||||
FROM books
|
FROM books
|
||||||
WHERE type & ${BookType.text} > 0
|
WHERE type & ${BookType.text} > 0
|
||||||
"""
|
"""
|
||||||
@@ -627,4 +660,268 @@ interface BookDao {
|
|||||||
|
|
||||||
@Query("delete from books where type & ${BookType.notShelf} > 0")
|
@Query("delete from books where type & ${BookType.notShelf} > 0")
|
||||||
fun deleteNotShelfBook()
|
fun deleteNotShelfBook()
|
||||||
|
|
||||||
|
// ── Group preview / count queries (DB-level, replaces in-memory buildGroupPreviewState) ──
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM books")
|
||||||
|
fun flowAllBookShelfCount(): Flow<Int>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT ${BookGroup.IdAll} AS groupId, COUNT(*) AS count FROM books
|
||||||
|
UNION ALL SELECT ${BookGroup.IdRoot}, COUNT(*) FROM books
|
||||||
|
WHERE type & ${BookType.text} > 0 AND type & ${BookType.local} = 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
AND (SELECT show FROM book_groups WHERE groupId = ${BookGroup.IdNetNone}) != 1
|
||||||
|
UNION ALL SELECT ${BookGroup.IdLocal}, COUNT(*) FROM books WHERE type & ${BookType.local} > 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdAudio}, COUNT(*) FROM books WHERE type & ${BookType.audio} > 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdNetNone}, COUNT(*) FROM books
|
||||||
|
WHERE type & ${BookType.audio} = 0 AND type & ${BookType.local} = 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdLocalNone}, COUNT(*) FROM books
|
||||||
|
WHERE type & ${BookType.local} > 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdManga}, COUNT(*) FROM books WHERE type & ${BookType.image} > 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdText}, COUNT(*) FROM books WHERE type & ${BookType.text} > 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdError}, COUNT(*) FROM books WHERE type & ${BookType.updateError} > 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdUnread}, COUNT(*) FROM books WHERE durChapterIndex = 0 AND durChapterPos = 0
|
||||||
|
UNION ALL SELECT ${BookGroup.IdReading}, COUNT(*) FROM books WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
|
||||||
|
UNION ALL SELECT ${BookGroup.IdReadFinished}, COUNT(*) FROM books WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowSystemGroupCounts(): Flow<List<GroupBookCount>>
|
||||||
|
|
||||||
|
@Query("SELECT COUNT(*) FROM books WHERE (`group` & :groupId) > 0")
|
||||||
|
fun flowUserGroupBookCount(groupId: Long): Flow<Int>
|
||||||
|
|
||||||
|
fun flowGroupPreview(groupId: Long): Flow<List<BookShelfItem>> {
|
||||||
|
return when (groupId) {
|
||||||
|
BookGroup.IdRoot -> flowBookShelfRootPreview()
|
||||||
|
BookGroup.IdAll -> flowBookShelfPreview()
|
||||||
|
BookGroup.IdLocal -> flowBookShelfLocalPreview()
|
||||||
|
BookGroup.IdAudio -> flowBookShelfAudioPreview()
|
||||||
|
BookGroup.IdNetNone -> flowBookShelfNetNoGroupPreview()
|
||||||
|
BookGroup.IdLocalNone -> flowBookShelfLocalNoGroupPreview()
|
||||||
|
BookGroup.IdManga -> flowBookShelfMangaPreview()
|
||||||
|
BookGroup.IdText -> flowBookShelfTextPreview()
|
||||||
|
BookGroup.IdError -> flowBookShelfUpdateErrorPreview()
|
||||||
|
BookGroup.IdUnread -> flowBookShelfUnreadPreview()
|
||||||
|
BookGroup.IdReading -> flowBookShelfReadingPreview()
|
||||||
|
BookGroup.IdReadFinished -> flowBookShelfReadFinishedPreview()
|
||||||
|
else -> flowBookShelfPreviewByUserGroup(groupId)
|
||||||
|
}.map { list ->
|
||||||
|
list.filterNot { it.isNotShelf }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.text} > 0 AND type & ${BookType.local} = 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
AND (SELECT show FROM book_groups WHERE groupId = ${BookGroup.IdNetNone}) != 1
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfRootPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.local} > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfLocalPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.audio} > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfAudioPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.audio} = 0 AND type & ${BookType.local} = 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfNetNoGroupPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.local} > 0
|
||||||
|
AND ((SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0) & `group`) = 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfLocalNoGroupPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.image} > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfMangaPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.text} > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfTextPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE type & ${BookType.updateError} > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfUpdateErrorPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE durChapterIndex = 0 AND durChapterPos = 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfUnreadPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfReadingPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfReadFinishedPreview(): Flow<List<BookShelfItem>>
|
||||||
|
|
||||||
|
@Query(
|
||||||
|
"""
|
||||||
|
SELECT bookUrl, name, author, origin, originName,
|
||||||
|
coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
|
||||||
|
durChapterPos, latestChapterTitle, latestChapterTime,
|
||||||
|
lastCheckCount, totalChapterNum, durChapterIndex,
|
||||||
|
type, `group`, `order`, canUpdate,
|
||||||
|
ifnull(customIntro, intro) as intro, kind
|
||||||
|
FROM books
|
||||||
|
WHERE (`group` & :groupId) > 0
|
||||||
|
ORDER BY CASE WHEN (coverUrl IS NOT NULL OR customCoverUrl IS NOT NULL) THEN 0 ELSE 1 END, durChapterTime DESC
|
||||||
|
LIMIT 4
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
fun flowBookShelfPreviewByUserGroup(groupId: Long): Flow<List<BookShelfItem>>
|
||||||
}
|
}
|
||||||
@@ -86,12 +86,32 @@ data class RssSource(
|
|||||||
var loadWithBaseUrl: Boolean = true,
|
var loadWithBaseUrl: Boolean = true,
|
||||||
/**注入js**/
|
/**注入js**/
|
||||||
var injectJs: String? = null,
|
var injectJs: String? = null,
|
||||||
|
/**提前预注入js**/
|
||||||
|
var preloadJs: String? = null,
|
||||||
|
/**web形式起始页**/
|
||||||
|
var startHtml: String? = null,
|
||||||
|
var startStyle: String? = null,
|
||||||
|
var startJs: String? = null,
|
||||||
|
/**是否输出web网页日志**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var showWebLog: Boolean = false,
|
||||||
/*其它规则*/
|
/*其它规则*/
|
||||||
/**最后更新时间,用于排序**/
|
/**最后更新时间,用于排序**/
|
||||||
@ColumnInfo(defaultValue = "0")
|
@ColumnInfo(defaultValue = "0")
|
||||||
var lastUpdateTime: Long = 0,
|
var lastUpdateTime: Long = 0,
|
||||||
@ColumnInfo(defaultValue = "0")
|
@ColumnInfo(defaultValue = "0")
|
||||||
var customOrder: Int = 0,
|
var customOrder: Int = 0,
|
||||||
|
/**类型 0网页,1图片,2视频**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var type: Int = 0,
|
||||||
|
/**是否启用预加载**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var preload: Boolean = false,
|
||||||
|
/**是否优先加载缓存**/
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
var cacheFirst: Boolean = false,
|
||||||
|
/**搜索url**/
|
||||||
|
var searchUrl: String? = null,
|
||||||
//用户控制的重定向
|
//用户控制的重定向
|
||||||
@ColumnInfo(defaultValue = "ASK_CROSS_ORIGIN")
|
@ColumnInfo(defaultValue = "ASK_CROSS_ORIGIN")
|
||||||
var redirectPolicy: String = "ASK_CROSS_ORIGIN"
|
var redirectPolicy: String = "ASK_CROSS_ORIGIN"
|
||||||
@@ -143,6 +163,15 @@ data class RssSource(
|
|||||||
&& equal(variableComment, source.variableComment)
|
&& equal(variableComment, source.variableComment)
|
||||||
&& equal(style, source.style)
|
&& equal(style, source.style)
|
||||||
&& equal(injectJs, source.injectJs)
|
&& equal(injectJs, source.injectJs)
|
||||||
|
&& equal(preloadJs, source.preloadJs)
|
||||||
|
&& equal(startHtml, source.startHtml)
|
||||||
|
&& equal(startStyle, source.startStyle)
|
||||||
|
&& equal(startJs, source.startJs)
|
||||||
|
&& showWebLog == source.showWebLog
|
||||||
|
&& type == source.type
|
||||||
|
&& preload == source.preload
|
||||||
|
&& cacheFirst == source.cacheFirst
|
||||||
|
&& equal(searchUrl, source.searchUrl)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun equal(a: String?, b: String?): Boolean {
|
private fun equal(a: String?, b: String?): Boolean {
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package io.legado.app.help
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.documentfile.provider.DocumentFile
|
||||||
|
import io.legado.app.utils.FileDoc
|
||||||
|
import io.legado.app.utils.listFileDocs
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
fun loadFontFiles(context: Context, folderUri: Uri?): List<FileDoc> {
|
||||||
|
val fontRegex = Regex("(?i).*\\.[ot]tf")
|
||||||
|
if (folderUri != null) {
|
||||||
|
val documentFile = DocumentFile.fromTreeUri(context, folderUri)
|
||||||
|
return documentFile?.let { docFile ->
|
||||||
|
val fontFiles = mutableListOf<FileDoc>()
|
||||||
|
docFile.listFiles()?.forEach { file ->
|
||||||
|
if (file.isFile && file.name?.matches(fontRegex) == true) {
|
||||||
|
fontFiles.add(
|
||||||
|
FileDoc(
|
||||||
|
name = file.name ?: "",
|
||||||
|
isDir = false,
|
||||||
|
size = file.length(),
|
||||||
|
lastModified = file.lastModified(),
|
||||||
|
uri = file.uri
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontFiles
|
||||||
|
} ?: emptyList()
|
||||||
|
} else {
|
||||||
|
val fontPath = "${context.getExternalFilesDir(null)?.absolutePath}/font"
|
||||||
|
return File(fontPath).listFileDocs { it.name.matches(fontRegex) }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1105,7 +1105,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* 弹窗提示
|
* 弹窗提示
|
||||||
*/
|
*/
|
||||||
fun toast(msg: Any?) {
|
fun toast(msg: Any?) {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
appCtx.toastForJs("${getSource()?.getTag()}: ${msg.toString()}")
|
appCtx.toastForJs("${getSource()?.getTag()}: ${msg.toString()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1113,7 +1113,7 @@ interface JsExtensions : JsEncodeUtils {
|
|||||||
* 弹窗提示 停留时间较长
|
* 弹窗提示 停留时间较长
|
||||||
*/
|
*/
|
||||||
fun longToast(msg: Any?) {
|
fun longToast(msg: Any?) {
|
||||||
rhinoContext.ensureActive()
|
rhinoContextOrNull?.ensureActive()
|
||||||
appCtx.longToastForJs("${getSource()?.getTag()}: ${msg.toString()}")
|
appCtx.longToastForJs("${getSource()?.getTag()}: ${msg.toString()}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import com.google.gson.JsonDeserializationContext
|
||||||
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonSerializationContext
|
||||||
|
import com.google.gson.JsonSerializer
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.getString
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
object PersonalizationThemeConfig {
|
||||||
|
const val configFileName = "personalizationThemeConfig.json"
|
||||||
|
val configFilePath = FileUtils.getPath(appCtx.filesDir, configFileName)
|
||||||
|
|
||||||
|
private val THEME_GSON: Gson by lazy {
|
||||||
|
GsonBuilder()
|
||||||
|
.registerTypeAdapter(Config::class.java, ConfigSerializer())
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _configList: ArrayList<Config> by lazy {
|
||||||
|
ArrayList(getConfigs() ?: emptyList())
|
||||||
|
}
|
||||||
|
|
||||||
|
val configList: List<Config> get() = _configList
|
||||||
|
|
||||||
|
fun save() {
|
||||||
|
val json = THEME_GSON.toJson(_configList)
|
||||||
|
FileUtils.delete(configFilePath)
|
||||||
|
FileUtils.createFileIfNotExist(configFilePath).writeText(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJson(config: Config): String {
|
||||||
|
return THEME_GSON.toJson(config)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toJson(): String {
|
||||||
|
return THEME_GSON.toJson(_configList)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun fromJson(json: String): Boolean {
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val configs = THEME_GSON.fromJson(json, Array<Config>::class.java)
|
||||||
|
_configList.clear()
|
||||||
|
_configList.addAll(configs)
|
||||||
|
save()
|
||||||
|
true
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun delConfig(index: Int) {
|
||||||
|
if (index in _configList.indices) {
|
||||||
|
_configList.removeAt(index)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addConfig(json: String): Boolean {
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val config = THEME_GSON.fromJson(json, Config::class.java)
|
||||||
|
addConfig(config)
|
||||||
|
true
|
||||||
|
}.getOrDefault(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun addConfig(newConfig: Config) {
|
||||||
|
_configList.forEachIndexed { index, config ->
|
||||||
|
if (newConfig.themeName == config.themeName) {
|
||||||
|
_configList[index] = newConfig
|
||||||
|
save()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_configList.add(newConfig)
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getConfigs(): List<Config>? {
|
||||||
|
val configFile = File(configFilePath)
|
||||||
|
if (configFile.exists()) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val json = configFile.readText()
|
||||||
|
return THEME_GSON.fromJson(json, Array<Config>::class.java).toList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyConfig(config: Config) {
|
||||||
|
config.appearance.applyToThemeConfig()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePersonalizationTheme(name: String): Config {
|
||||||
|
val config = Config(
|
||||||
|
themeName = name,
|
||||||
|
appearance = ThemeAppearanceConfig.fromCurrent()
|
||||||
|
)
|
||||||
|
addConfig(config)
|
||||||
|
return config
|
||||||
|
}
|
||||||
|
|
||||||
|
data class Config(
|
||||||
|
var themeName: String = "",
|
||||||
|
var appearance: ThemeAppearanceConfig = ThemeAppearanceConfig()
|
||||||
|
) {
|
||||||
|
val themeColor: Int get() = appearance.themeColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class ConfigSerializer : JsonSerializer<PersonalizationThemeConfig.Config>,
|
||||||
|
JsonDeserializer<PersonalizationThemeConfig.Config> {
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
src: PersonalizationThemeConfig.Config,
|
||||||
|
typeOfSrc: Type,
|
||||||
|
context: JsonSerializationContext
|
||||||
|
): JsonElement {
|
||||||
|
val obj = JsonObject()
|
||||||
|
obj.addProperty("themeName", src.themeName)
|
||||||
|
ThemeAppearanceJson.writeTo(obj, src.appearance)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): PersonalizationThemeConfig.Config {
|
||||||
|
val obj = json.asJsonObject
|
||||||
|
return PersonalizationThemeConfig.Config(
|
||||||
|
themeName = obj.getString("themeName", "") ?: "",
|
||||||
|
appearance = ThemeAppearanceJson.readFrom(obj)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -26,10 +26,12 @@ import io.legado.app.utils.getFile
|
|||||||
import io.legado.app.utils.getMeanColor
|
import io.legado.app.utils.getMeanColor
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
import io.legado.app.utils.getPrefInt
|
import io.legado.app.utils.getPrefInt
|
||||||
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.hexString
|
import io.legado.app.utils.hexString
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
import io.legado.app.utils.putPrefBoolean
|
import io.legado.app.utils.putPrefBoolean
|
||||||
import io.legado.app.utils.putPrefInt
|
import io.legado.app.utils.putPrefInt
|
||||||
|
import io.legado.app.utils.putPrefString
|
||||||
import io.legado.app.utils.resizeAndRecycle
|
import io.legado.app.utils.resizeAndRecycle
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
@@ -227,6 +229,12 @@ object ReadBookConfig {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val regexColorRules: ArrayList<RegexColorRule> get() = durConfig.regexColorRules
|
||||||
|
|
||||||
|
fun saveRegexColorRules() {
|
||||||
|
save()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 两端对齐
|
* 两端对齐
|
||||||
*/
|
*/
|
||||||
@@ -260,6 +268,36 @@ object ReadBookConfig {
|
|||||||
config.textFont = value
|
config.textFont = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var titleFont: String
|
||||||
|
get() = config.titleFont
|
||||||
|
set(value) {
|
||||||
|
config.titleFont = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerFont: String
|
||||||
|
get() = config.headerFont
|
||||||
|
set(value) {
|
||||||
|
config.headerFont = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerFont: String
|
||||||
|
get() = config.footerFont
|
||||||
|
set(value) {
|
||||||
|
config.footerFont = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var headerFontSize: Int
|
||||||
|
get() = config.headerFontSize.takeIf { it > 0 } ?: 12
|
||||||
|
set(value) {
|
||||||
|
config.headerFontSize = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var footerFontSize: Int
|
||||||
|
get() = config.footerFontSize.takeIf { it > 0 } ?: 12
|
||||||
|
set(value) {
|
||||||
|
config.footerFontSize = value
|
||||||
|
}
|
||||||
|
|
||||||
var textBold: Int
|
var textBold: Int
|
||||||
get() = config.textBold
|
get() = config.textBold
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -393,6 +431,12 @@ object ReadBookConfig {
|
|||||||
config.titleBottomSpacing = value
|
config.titleBottomSpacing = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var titleColor: Int
|
||||||
|
get() = config.titleColor
|
||||||
|
set(value) {
|
||||||
|
config.titleColor = value
|
||||||
|
}
|
||||||
|
|
||||||
var paragraphIndent: String
|
var paragraphIndent: String
|
||||||
get() = config.paragraphIndent
|
get() = config.paragraphIndent
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -417,6 +461,12 @@ object ReadBookConfig {
|
|||||||
config.underlinePadding = value
|
config.underlinePadding = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var underlineExtend: Boolean
|
||||||
|
get() = config.underlineExtend
|
||||||
|
set(value) {
|
||||||
|
config.underlineExtend = value
|
||||||
|
}
|
||||||
|
|
||||||
var dottedLine: Boolean
|
var dottedLine: Boolean
|
||||||
get() = config.dottedLine
|
get() = config.dottedLine
|
||||||
set(value) {
|
set(value) {
|
||||||
@@ -544,9 +594,14 @@ object ReadBookConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun getExportConfig(): Config {
|
fun getExportConfig(): Config {
|
||||||
val exportConfig = durConfig.copy()
|
val exportConfig = durConfig.copy(regexColorRules = ArrayList(durConfig.regexColorRules.map { it.copy() }))
|
||||||
if (shareLayout) {
|
if (shareLayout) {
|
||||||
exportConfig.textFont = shareConfig.textFont
|
exportConfig.textFont = shareConfig.textFont
|
||||||
|
exportConfig.titleFont = shareConfig.titleFont
|
||||||
|
exportConfig.headerFont = shareConfig.headerFont
|
||||||
|
exportConfig.footerFont = shareConfig.footerFont
|
||||||
|
exportConfig.headerFontSize = shareConfig.headerFontSize
|
||||||
|
exportConfig.footerFontSize = shareConfig.footerFontSize
|
||||||
exportConfig.textBold = shareConfig.textBold
|
exportConfig.textBold = shareConfig.textBold
|
||||||
exportConfig.textSize = shareConfig.textSize
|
exportConfig.textSize = shareConfig.textSize
|
||||||
exportConfig.letterSpacing = shareConfig.letterSpacing
|
exportConfig.letterSpacing = shareConfig.letterSpacing
|
||||||
@@ -556,6 +611,7 @@ object ReadBookConfig {
|
|||||||
exportConfig.titleSize = shareConfig.titleSize
|
exportConfig.titleSize = shareConfig.titleSize
|
||||||
exportConfig.titleTopSpacing = shareConfig.titleTopSpacing
|
exportConfig.titleTopSpacing = shareConfig.titleTopSpacing
|
||||||
exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing
|
exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing
|
||||||
|
exportConfig.titleColor = shareConfig.titleColor
|
||||||
exportConfig.paddingBottom = shareConfig.paddingBottom
|
exportConfig.paddingBottom = shareConfig.paddingBottom
|
||||||
exportConfig.paddingLeft = shareConfig.paddingLeft
|
exportConfig.paddingLeft = shareConfig.paddingLeft
|
||||||
exportConfig.paddingRight = shareConfig.paddingRight
|
exportConfig.paddingRight = shareConfig.paddingRight
|
||||||
@@ -576,7 +632,8 @@ object ReadBookConfig {
|
|||||||
exportConfig.tipFooterLeft = shareConfig.tipFooterLeft
|
exportConfig.tipFooterLeft = shareConfig.tipFooterLeft
|
||||||
exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle
|
exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle
|
||||||
exportConfig.tipFooterRight = shareConfig.tipFooterRight
|
exportConfig.tipFooterRight = shareConfig.tipFooterRight
|
||||||
exportConfig.tipColor = shareConfig.tipColor
|
exportConfig.tipHeaderColor = shareConfig.tipHeaderColor
|
||||||
|
exportConfig.tipFooterColor = shareConfig.tipFooterColor
|
||||||
exportConfig.headerMode = shareConfig.headerMode
|
exportConfig.headerMode = shareConfig.headerMode
|
||||||
// MD3专有属性
|
// MD3专有属性
|
||||||
exportConfig.footerMode = shareConfig.footerMode
|
exportConfig.footerMode = shareConfig.footerMode
|
||||||
@@ -628,6 +685,20 @@ object ReadBookConfig {
|
|||||||
config.textFont = ""
|
config.textFont = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (config.titleFont.isNotEmpty()) {
|
||||||
|
val fontName = config.titleFont
|
||||||
|
val fontPath =
|
||||||
|
FileUtils.getPath(appCtx.externalFiles, "font", fontName)
|
||||||
|
val fontFile = configDir.getFile(fontName)
|
||||||
|
if (fontFile.exists()) {
|
||||||
|
if (!FileUtils.exist(fontPath)) {
|
||||||
|
fontFile.copyTo(File(fontPath))
|
||||||
|
}
|
||||||
|
config.titleFont = fontPath
|
||||||
|
} else {
|
||||||
|
config.titleFont = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
if (config.bgType == 2) {
|
if (config.bgType == 2) {
|
||||||
val bgName = FileUtils.getName(config.bgStr)
|
val bgName = FileUtils.getName(config.bgStr)
|
||||||
config.bgStr = bgName
|
config.bgStr = bgName
|
||||||
@@ -688,6 +759,11 @@ object ReadBookConfig {
|
|||||||
private var pageAnim: Int = 0,//翻页动画
|
private var pageAnim: Int = 0,//翻页动画
|
||||||
private var pageAnimEInk: Int = 4,
|
private var pageAnimEInk: Int = 4,
|
||||||
var textFont: String = "",//字体
|
var textFont: String = "",//字体
|
||||||
|
var titleFont: String = "",//标题字体
|
||||||
|
var headerFont: String = "",//页眉字体
|
||||||
|
var footerFont: String = "",//页脚字体
|
||||||
|
var headerFontSize: Int = 12,//页眉字号
|
||||||
|
var footerFontSize: Int = 12,//页脚字号
|
||||||
var textBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
|
var textBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
|
||||||
var textSize: Int = 20,//文字大小
|
var textSize: Int = 20,//文字大小
|
||||||
var textItalic: Boolean = false,// 是否启用斜体
|
var textItalic: Boolean = false,// 是否启用斜体
|
||||||
@@ -704,6 +780,7 @@ object ReadBookConfig {
|
|||||||
var titleSize: Int = 0,
|
var titleSize: Int = 0,
|
||||||
var titleTopSpacing: Int = 0,
|
var titleTopSpacing: Int = 0,
|
||||||
var titleBottomSpacing: Int = 0,
|
var titleBottomSpacing: Int = 0,
|
||||||
|
var titleColor: Int = 0,
|
||||||
var titleBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
|
var titleBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
|
||||||
var titleLineSpacingExtra: Int = 12,
|
var titleLineSpacingExtra: Int = 12,
|
||||||
var titleLineSpacingSub: Int = 12,
|
var titleLineSpacingSub: Int = 12,
|
||||||
@@ -715,6 +792,7 @@ object ReadBookConfig {
|
|||||||
var underline: Boolean = false, //下划线
|
var underline: Boolean = false, //下划线
|
||||||
var underlinePadding: Int = 10,
|
var underlinePadding: Int = 10,
|
||||||
var underlineHeight: Int = 1,
|
var underlineHeight: Int = 1,
|
||||||
|
var underlineExtend: Boolean = false, //下划线延伸
|
||||||
var underlineColor: String = "#3E3D3B",
|
var underlineColor: String = "#3E3D3B",
|
||||||
var underlineColorNight: String = "#ADADAD",
|
var underlineColorNight: String = "#ADADAD",
|
||||||
var dottedLine: Boolean = false, //虚线
|
var dottedLine: Boolean = false, //虚线
|
||||||
@@ -740,10 +818,12 @@ object ReadBookConfig {
|
|||||||
var tipFooterLeft: Int = ReadTipConfig.chapterTitle,
|
var tipFooterLeft: Int = ReadTipConfig.chapterTitle,
|
||||||
var tipFooterMiddle: Int = ReadTipConfig.none,
|
var tipFooterMiddle: Int = ReadTipConfig.none,
|
||||||
var tipFooterRight: Int = ReadTipConfig.pageAndTotal,
|
var tipFooterRight: Int = ReadTipConfig.pageAndTotal,
|
||||||
var tipColor: Int = 0,
|
var tipHeaderColor: Int = 0,
|
||||||
|
var tipFooterColor: Int = 0,
|
||||||
var tipDividerColor: Int = -1,
|
var tipDividerColor: Int = -1,
|
||||||
var headerMode: Int = 0,
|
var headerMode: Int = 0,
|
||||||
var footerMode: Int = 0
|
var footerMode: Int = 0,
|
||||||
|
var regexColorRules: ArrayList<RegexColorRule> = arrayListOf()
|
||||||
) {
|
) {
|
||||||
|
|
||||||
@Transient
|
@Transient
|
||||||
@@ -821,6 +901,11 @@ object ReadBookConfig {
|
|||||||
"pageAnim" to pageAnim,
|
"pageAnim" to pageAnim,
|
||||||
"pageAnimEInk" to pageAnimEInk,
|
"pageAnimEInk" to pageAnimEInk,
|
||||||
"textFont" to textFont,
|
"textFont" to textFont,
|
||||||
|
"titleFont" to titleFont,
|
||||||
|
"headerFont" to headerFont,
|
||||||
|
"footerFont" to footerFont,
|
||||||
|
"headerFontSize" to headerFontSize,
|
||||||
|
"footerFontSize" to footerFontSize,
|
||||||
"textBold" to textBold,
|
"textBold" to textBold,
|
||||||
"textSize" to textSize,
|
"textSize" to textSize,
|
||||||
"letterSpacing" to letterSpacing,
|
"letterSpacing" to letterSpacing,
|
||||||
@@ -830,6 +915,7 @@ object ReadBookConfig {
|
|||||||
"titleSize" to titleSize,
|
"titleSize" to titleSize,
|
||||||
"titleTopSpacing" to titleTopSpacing,
|
"titleTopSpacing" to titleTopSpacing,
|
||||||
"titleBottomSpacing" to titleBottomSpacing,
|
"titleBottomSpacing" to titleBottomSpacing,
|
||||||
|
"titleColor" to titleColor,
|
||||||
"paragraphIndent" to paragraphIndent,
|
"paragraphIndent" to paragraphIndent,
|
||||||
"paddingBottom" to paddingBottom,
|
"paddingBottom" to paddingBottom,
|
||||||
"paddingLeft" to paddingLeft,
|
"paddingLeft" to paddingLeft,
|
||||||
@@ -851,10 +937,12 @@ object ReadBookConfig {
|
|||||||
"tipFooterLeft" to tipFooterLeft,
|
"tipFooterLeft" to tipFooterLeft,
|
||||||
"tipFooterMiddle" to tipFooterMiddle,
|
"tipFooterMiddle" to tipFooterMiddle,
|
||||||
"tipFooterRight" to tipFooterRight,
|
"tipFooterRight" to tipFooterRight,
|
||||||
"tipColor" to tipColor,
|
"tipHeaderColor" to tipHeaderColor,
|
||||||
|
"tipFooterColor" to tipFooterColor,
|
||||||
"tipDividerColor" to tipDividerColor,
|
"tipDividerColor" to tipDividerColor,
|
||||||
"headerMode" to headerMode,
|
"headerMode" to headerMode,
|
||||||
"footerMode" to footerMode
|
"footerMode" to footerMode,
|
||||||
|
"regexColorRules" to regexColorRules.map { mapOf("name" to it.name, "pattern" to it.pattern, "color" to it.color, "fontPath" to it.fontPath) }
|
||||||
)
|
)
|
||||||
|
|
||||||
fun getBgPath(bgIndex: Int): String? {
|
fun getBgPath(bgIndex: Int): String? {
|
||||||
|
|||||||
@@ -85,10 +85,16 @@ object ReadTipConfig {
|
|||||||
ReadBookConfig.config.footerMode = value
|
ReadBookConfig.config.footerMode = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var tipColor: Int
|
var tipHeaderColor: Int
|
||||||
get() = ReadBookConfig.config.tipColor
|
get() = ReadBookConfig.config.tipHeaderColor
|
||||||
set(value) {
|
set(value) {
|
||||||
ReadBookConfig.config.tipColor = value
|
ReadBookConfig.config.tipHeaderColor = value
|
||||||
|
}
|
||||||
|
|
||||||
|
var tipFooterColor: Int
|
||||||
|
get() = ReadBookConfig.config.tipFooterColor
|
||||||
|
set(value) {
|
||||||
|
ReadBookConfig.config.tipFooterColor = value
|
||||||
}
|
}
|
||||||
|
|
||||||
var tipDividerColor: Int
|
var tipDividerColor: Int
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
data class RegexColorRule(
|
||||||
|
var name: String,
|
||||||
|
var pattern: String,
|
||||||
|
var color: Int,
|
||||||
|
var fontPath: String = ""
|
||||||
|
)
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import io.legado.app.ui.config.themeConfig.TagColorPair
|
||||||
|
|
||||||
|
object TagColorGenerator {
|
||||||
|
|
||||||
|
fun generateTagColors(baseColor: Color): List<TagColorPair> {
|
||||||
|
val baseHsl = FloatArray(3)
|
||||||
|
ColorUtils.colorToHSL(baseColor.toArgb(), baseHsl)
|
||||||
|
val baseHue = baseHsl[0]
|
||||||
|
|
||||||
|
val result = mutableListOf<TagColorPair>()
|
||||||
|
|
||||||
|
val hueOffsets = listOf(0, 45, 90, 135, 180, 225, 270, 315)
|
||||||
|
|
||||||
|
hueOffsets.forEach { offset ->
|
||||||
|
val newHue = (baseHue + offset) % 360
|
||||||
|
|
||||||
|
val textHsl = floatArrayOf(newHue, 0.75f, 0.45f)
|
||||||
|
val textColor = Color.hsl(textHsl[0], textHsl[1], textHsl[2]).toArgb()
|
||||||
|
|
||||||
|
val bgHsl = floatArrayOf(newHue, 0.35f, 0.90f)
|
||||||
|
val bgColor = Color.hsl(bgHsl[0], bgHsl[1], bgHsl[2]).toArgb()
|
||||||
|
|
||||||
|
result.add(TagColorPair(textColor, bgColor))
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
|
||||||
|
data class ThemeAppearanceConfig(
|
||||||
|
var enableDeepPersonalization: Boolean = false,
|
||||||
|
var themeColor: Int = 0,
|
||||||
|
var secondaryThemeColor: Int = 0,
|
||||||
|
var primaryTextColor: Int = 0,
|
||||||
|
var secondaryTextColor: Int = 0,
|
||||||
|
var backgroundColor: Int = 0,
|
||||||
|
var labelContainerColor: Int = 0,
|
||||||
|
var bookInfoInputColor: Int = 0,
|
||||||
|
var appFontPath: String? = null,
|
||||||
|
var enableContainerBorder: Boolean = false,
|
||||||
|
var containerBorderWidth: Float = 1f,
|
||||||
|
var containerBorderStyle: String = "solid",
|
||||||
|
var containerBorderDashWidth: Float = 4f,
|
||||||
|
var containerBorderColor: Int = 0,
|
||||||
|
var enableItemDivider: Boolean = true,
|
||||||
|
var itemDividerWidth: Float = 1f,
|
||||||
|
var itemDividerLength: Float = 80f,
|
||||||
|
var itemDividerColor: Int = 0,
|
||||||
|
var enableCustomTagColors: Boolean = false,
|
||||||
|
var customTagColorsJson: String? = null,
|
||||||
|
var enableBlur: Boolean = false,
|
||||||
|
var topBarBlurRadius: Int = 24,
|
||||||
|
var bottomBarBlurRadius: Int = 8,
|
||||||
|
var topBarBlurAlpha: Int = 73,
|
||||||
|
var bottomBarBlurAlpha: Int = 40,
|
||||||
|
var bottomBarLensRadius: Float = 24f
|
||||||
|
) {
|
||||||
|
|
||||||
|
val hasDeepColorOverrides: Boolean
|
||||||
|
get() = themeColor != 0 ||
|
||||||
|
secondaryThemeColor != 0 ||
|
||||||
|
primaryTextColor != 0 ||
|
||||||
|
secondaryTextColor != 0 ||
|
||||||
|
backgroundColor != 0 ||
|
||||||
|
labelContainerColor != 0
|
||||||
|
|
||||||
|
fun applyToThemeConfig(appFontPathOverride: String? = appFontPath) {
|
||||||
|
ThemeConfig.enableDeepPersonalization = enableDeepPersonalization
|
||||||
|
ThemeConfig.themeColor = themeColor
|
||||||
|
ThemeConfig.secondaryThemeColor = secondaryThemeColor
|
||||||
|
ThemeConfig.primaryTextColor = primaryTextColor
|
||||||
|
ThemeConfig.secondaryTextColor = secondaryTextColor
|
||||||
|
ThemeConfig.themeBackgroundColor = backgroundColor
|
||||||
|
ThemeConfig.labelContainerColor = labelContainerColor
|
||||||
|
|
||||||
|
ThemeConfig.bookInfoInputColor = bookInfoInputColor
|
||||||
|
|
||||||
|
ThemeConfig.appFontPath = appFontPathOverride
|
||||||
|
|
||||||
|
ThemeConfig.enableContainerBorder = enableContainerBorder
|
||||||
|
ThemeConfig.containerBorderWidth = containerBorderWidth
|
||||||
|
ThemeConfig.containerBorderStyle = containerBorderStyle.ifBlank { "solid" }
|
||||||
|
ThemeConfig.containerBorderDashWidth = containerBorderDashWidth
|
||||||
|
ThemeConfig.containerBorderColor = containerBorderColor
|
||||||
|
|
||||||
|
ThemeConfig.enableItemDivider = enableItemDivider
|
||||||
|
ThemeConfig.itemDividerWidth = itemDividerWidth
|
||||||
|
ThemeConfig.itemDividerLength = itemDividerLength
|
||||||
|
ThemeConfig.itemDividerColor = itemDividerColor
|
||||||
|
|
||||||
|
ThemeConfig.enableCustomTagColors = enableCustomTagColors
|
||||||
|
ThemeConfig.customTagColorsJson = ThemeAppearanceJson.convertTagColorsFromHex(customTagColorsJson)
|
||||||
|
|
||||||
|
ThemeConfig.enableBlur = enableBlur
|
||||||
|
ThemeConfig.topBarBlurRadius = topBarBlurRadius
|
||||||
|
ThemeConfig.bottomBarBlurRadius = bottomBarBlurRadius
|
||||||
|
ThemeConfig.topBarBlurAlpha = topBarBlurAlpha
|
||||||
|
ThemeConfig.bottomBarBlurAlpha = bottomBarBlurAlpha
|
||||||
|
ThemeConfig.bottomBarLensRadius = bottomBarLensRadius
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
fun fromCurrent(): ThemeAppearanceConfig {
|
||||||
|
return ThemeAppearanceConfig(
|
||||||
|
enableDeepPersonalization = ThemeConfig.enableDeepPersonalization,
|
||||||
|
themeColor = ThemeConfig.themeColor,
|
||||||
|
secondaryThemeColor = ThemeConfig.secondaryThemeColor,
|
||||||
|
primaryTextColor = ThemeConfig.primaryTextColor,
|
||||||
|
secondaryTextColor = ThemeConfig.secondaryTextColor,
|
||||||
|
backgroundColor = ThemeConfig.themeBackgroundColor,
|
||||||
|
labelContainerColor = ThemeConfig.labelContainerColor,
|
||||||
|
bookInfoInputColor = ThemeConfig.bookInfoInputColor,
|
||||||
|
appFontPath = ThemeConfig.appFontPath,
|
||||||
|
enableContainerBorder = ThemeConfig.enableContainerBorder,
|
||||||
|
containerBorderWidth = ThemeConfig.containerBorderWidth,
|
||||||
|
containerBorderStyle = ThemeConfig.containerBorderStyle,
|
||||||
|
containerBorderDashWidth = ThemeConfig.containerBorderDashWidth,
|
||||||
|
containerBorderColor = ThemeConfig.containerBorderColor,
|
||||||
|
enableItemDivider = ThemeConfig.enableItemDivider,
|
||||||
|
itemDividerWidth = ThemeConfig.itemDividerWidth,
|
||||||
|
itemDividerLength = ThemeConfig.itemDividerLength,
|
||||||
|
itemDividerColor = ThemeConfig.itemDividerColor,
|
||||||
|
enableCustomTagColors = ThemeConfig.enableCustomTagColors,
|
||||||
|
customTagColorsJson = ThemeConfig.customTagColorsJson,
|
||||||
|
enableBlur = ThemeConfig.enableBlur,
|
||||||
|
topBarBlurRadius = ThemeConfig.topBarBlurRadius,
|
||||||
|
bottomBarBlurRadius = ThemeConfig.bottomBarBlurRadius,
|
||||||
|
topBarBlurAlpha = ThemeConfig.topBarBlurAlpha,
|
||||||
|
bottomBarBlurAlpha = ThemeConfig.bottomBarBlurAlpha,
|
||||||
|
bottomBarLensRadius = ThemeConfig.bottomBarLensRadius
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import androidx.core.graphics.toColorInt
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonParser
|
||||||
|
import io.legado.app.ui.config.themeConfig.TagColorPair
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.getBoolean
|
||||||
|
import io.legado.app.utils.getFloat
|
||||||
|
import io.legado.app.utils.getString
|
||||||
|
|
||||||
|
object ThemeAppearanceJson {
|
||||||
|
|
||||||
|
fun writeTo(obj: JsonObject, src: ThemeAppearanceConfig) {
|
||||||
|
obj.addProperty("enableDeepPersonalization", src.enableDeepPersonalization)
|
||||||
|
obj.addProperty("themeColor", colorToHex(src.themeColor))
|
||||||
|
obj.addProperty("secondaryThemeColor", colorToHex(src.secondaryThemeColor))
|
||||||
|
obj.addProperty("primaryTextColor", colorToHex(src.primaryTextColor))
|
||||||
|
obj.addProperty("secondaryTextColor", colorToHex(src.secondaryTextColor))
|
||||||
|
obj.addProperty("backgroundColor", colorToHex(src.backgroundColor))
|
||||||
|
obj.addProperty("labelContainerColor", colorToHex(src.labelContainerColor))
|
||||||
|
obj.addProperty("bookInfoInputColor", colorToHex(src.bookInfoInputColor))
|
||||||
|
obj.addProperty("appFontPath", src.appFontPath)
|
||||||
|
obj.addProperty("enableContainerBorder", src.enableContainerBorder)
|
||||||
|
obj.addProperty("containerBorderWidth", src.containerBorderWidth)
|
||||||
|
obj.addProperty("containerBorderStyle", src.containerBorderStyle)
|
||||||
|
obj.addProperty("containerBorderDashWidth", src.containerBorderDashWidth)
|
||||||
|
obj.addProperty("containerBorderColor", colorToHex(src.containerBorderColor))
|
||||||
|
obj.addProperty("enableItemDivider", src.enableItemDivider)
|
||||||
|
obj.addProperty("itemDividerWidth", src.itemDividerWidth)
|
||||||
|
obj.addProperty("itemDividerLength", src.itemDividerLength)
|
||||||
|
obj.addProperty("itemDividerColor", colorToHex(src.itemDividerColor))
|
||||||
|
obj.addProperty("enableCustomTagColors", src.enableCustomTagColors)
|
||||||
|
obj.addProperty("customTagColorsJson", convertTagColorsToHex(src.customTagColorsJson))
|
||||||
|
obj.addProperty("enableBlur", src.enableBlur)
|
||||||
|
obj.addProperty("topBarBlurRadius", src.topBarBlurRadius)
|
||||||
|
obj.addProperty("bottomBarBlurRadius", src.bottomBarBlurRadius)
|
||||||
|
obj.addProperty("topBarBlurAlpha", src.topBarBlurAlpha)
|
||||||
|
obj.addProperty("bottomBarBlurAlpha", src.bottomBarBlurAlpha)
|
||||||
|
obj.addProperty("bottomBarLensRadius", src.bottomBarLensRadius)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readFrom(obj: JsonObject): ThemeAppearanceConfig {
|
||||||
|
val appearance = ThemeAppearanceConfig(
|
||||||
|
themeColor = readColor(obj, "themeColor", "md3Primary"),
|
||||||
|
secondaryThemeColor = readColor(obj, "secondaryThemeColor", "md3Secondary", "md3OnPrimary"),
|
||||||
|
primaryTextColor = readColor(obj, "primaryTextColor", "md3OnSurface"),
|
||||||
|
secondaryTextColor = readColor(obj, "secondaryTextColor", "md3OnPrimaryContainer"),
|
||||||
|
backgroundColor = readColor(obj, "backgroundColor", "md3Background"),
|
||||||
|
labelContainerColor = readColor(obj, "labelContainerColor", "md3SurfaceContainerLow"),
|
||||||
|
bookInfoInputColor = readColor(obj, "bookInfoInputColor"),
|
||||||
|
appFontPath = obj.getString("appFontPath", null),
|
||||||
|
enableContainerBorder = obj.getBoolean("enableContainerBorder", false),
|
||||||
|
containerBorderWidth = obj.getFloat("containerBorderWidth", 1f),
|
||||||
|
containerBorderStyle = obj.getString("containerBorderStyle", "solid") ?: "solid",
|
||||||
|
containerBorderDashWidth = obj.getFloat("containerBorderDashWidth", 4f),
|
||||||
|
containerBorderColor = readColor(obj, "containerBorderColor"),
|
||||||
|
enableItemDivider = obj.getBoolean("enableItemDivider", true),
|
||||||
|
itemDividerWidth = obj.getFloat("itemDividerWidth", 1f),
|
||||||
|
itemDividerLength = obj.getFloat("itemDividerLength", 80f),
|
||||||
|
itemDividerColor = readColor(obj, "itemDividerColor"),
|
||||||
|
enableCustomTagColors = obj.getBoolean("enableCustomTagColors", false),
|
||||||
|
customTagColorsJson = obj.getString("customTagColorsJson", null),
|
||||||
|
enableBlur = obj.getBoolean("enableBlur", false),
|
||||||
|
topBarBlurRadius = obj.getInt("topBarBlurRadius", 24),
|
||||||
|
bottomBarBlurRadius = obj.getInt("bottomBarBlurRadius", 8),
|
||||||
|
topBarBlurAlpha = obj.getInt("topBarBlurAlpha", 73),
|
||||||
|
bottomBarBlurAlpha = obj.getInt("bottomBarBlurAlpha", 40),
|
||||||
|
bottomBarLensRadius = obj.getFloat("bottomBarLensRadius", 24f)
|
||||||
|
)
|
||||||
|
appearance.enableDeepPersonalization = if (obj.has("enableDeepPersonalization")) {
|
||||||
|
obj.getBoolean("enableDeepPersonalization", false)
|
||||||
|
} else {
|
||||||
|
appearance.hasDeepColorOverrides
|
||||||
|
}
|
||||||
|
return appearance
|
||||||
|
}
|
||||||
|
|
||||||
|
fun convertTagColorsFromHex(json: String?): String? {
|
||||||
|
if (json.isNullOrBlank()) return json
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val root = JsonParser.parseString(json)
|
||||||
|
if (root.isJsonArray) {
|
||||||
|
val arr = root.asJsonArray
|
||||||
|
val converted = arr.map { elem ->
|
||||||
|
val obj = elem.asJsonObject
|
||||||
|
val textColor = parseColorFromJson(obj.get("textColor"))
|
||||||
|
val bgColor = parseColorFromJson(obj.get("bgColor"))
|
||||||
|
"""{"textColor":$textColor,"bgColor":$bgColor}"""
|
||||||
|
}
|
||||||
|
"[\n ${converted.joinToString(",\n ")}\n]"
|
||||||
|
} else json
|
||||||
|
}.getOrNull() ?: json
|
||||||
|
}
|
||||||
|
|
||||||
|
fun readColor(obj: JsonObject, vararg names: String): Int {
|
||||||
|
for (name in names) {
|
||||||
|
val color = obj.getInt(name, Int.MIN_VALUE)
|
||||||
|
if (color != Int.MIN_VALUE) return color
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
fun colorToHex(color: Int): String = "#${String.format("%08X", color)}"
|
||||||
|
|
||||||
|
private fun convertTagColorsToHex(json: String?): String? {
|
||||||
|
if (json.isNullOrBlank()) return json
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val colors = GSON.fromJson(json, Array<TagColorPair>::class.java)
|
||||||
|
val converted = colors.map { pair ->
|
||||||
|
val textHex = colorToHex(pair.textColor)
|
||||||
|
val bgHex = colorToHex(pair.bgColor)
|
||||||
|
"""{"textColor":"$textHex","bgColor":"$bgHex"}"""
|
||||||
|
}
|
||||||
|
"[\n ${converted.joinToString(",\n ")}\n]"
|
||||||
|
}.getOrNull() ?: json
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseColorFromJson(elem: JsonElement?): Int {
|
||||||
|
if (elem == null || elem.isJsonNull) return 0
|
||||||
|
val prim = elem.asJsonPrimitive
|
||||||
|
return when {
|
||||||
|
prim.isNumber -> prim.asNumber.toInt()
|
||||||
|
prim.isString -> parseColorString(prim.asString)
|
||||||
|
else -> 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun JsonObject.getInt(name: String, default: Int): Int {
|
||||||
|
val elem = get(name)
|
||||||
|
return when {
|
||||||
|
elem == null || elem.isJsonNull -> default
|
||||||
|
elem.isJsonPrimitive -> {
|
||||||
|
val prim = elem.asJsonPrimitive
|
||||||
|
when {
|
||||||
|
prim.isNumber -> prim.asNumber.toInt()
|
||||||
|
prim.isString -> parseColorString(prim.asString)
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else -> default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun parseColorString(colorStr: String): Int {
|
||||||
|
val text = colorStr.trim()
|
||||||
|
if (text.isEmpty()) return 0
|
||||||
|
return kotlin.runCatching {
|
||||||
|
if (text.startsWith("#")) {
|
||||||
|
text.substring(1).toLong(16).toInt()
|
||||||
|
} else {
|
||||||
|
text.toLong().toInt()
|
||||||
|
}
|
||||||
|
}.getOrElse {
|
||||||
|
kotlin.runCatching { text.toColorInt() }.getOrDefault(0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,327 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import com.google.gson.Gson
|
||||||
|
import com.google.gson.GsonBuilder
|
||||||
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.utils.FileUtils
|
||||||
|
import io.legado.app.utils.externalFiles
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.util.zip.ZipEntry
|
||||||
|
import java.util.zip.ZipInputStream
|
||||||
|
import java.util.zip.ZipOutputStream
|
||||||
|
|
||||||
|
object ThemePackConfig {
|
||||||
|
|
||||||
|
private const val CONFIG_FILE = "theme.json"
|
||||||
|
private const val DIR_NAME = "theme_packs"
|
||||||
|
private val baseDir get() = File(appCtx.filesDir, DIR_NAME)
|
||||||
|
|
||||||
|
private val PACK_GSON: Gson by lazy {
|
||||||
|
GsonBuilder()
|
||||||
|
.registerTypeAdapter(ThemePack::class.java, ThemePackSerializer())
|
||||||
|
.setPrettyPrinting()
|
||||||
|
.disableHtmlEscaping()
|
||||||
|
.create()
|
||||||
|
}
|
||||||
|
|
||||||
|
private val _packList = mutableListOf<ThemePack>()
|
||||||
|
val packList: List<ThemePack> get() = _packList
|
||||||
|
|
||||||
|
init {
|
||||||
|
loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun loadAll() {
|
||||||
|
_packList.clear()
|
||||||
|
baseDir.mkdirs()
|
||||||
|
baseDir.listFiles()?.forEach { dir ->
|
||||||
|
if (dir.isDirectory) {
|
||||||
|
val configFile = File(dir, CONFIG_FILE)
|
||||||
|
if (configFile.exists()) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val json = configFile.readText()
|
||||||
|
val pack = PACK_GSON.fromJson(json, ThemePack::class.java)
|
||||||
|
if (pack.name.isNotEmpty()) {
|
||||||
|
pack.folderName = dir.name
|
||||||
|
_packList.add(pack)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun reload() {
|
||||||
|
loadAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getPackDir(pack: ThemePack): File {
|
||||||
|
return File(baseDir, pack.folderName)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun savePack(pack: ThemePack) {
|
||||||
|
val dir = getPackDir(pack)
|
||||||
|
dir.mkdirs()
|
||||||
|
val configFile = File(dir, CONFIG_FILE)
|
||||||
|
val json = PACK_GSON.toJson(pack)
|
||||||
|
FileUtils.createFileIfNotExist(configFile.absolutePath).writeText(json)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun deletePack(pack: ThemePack) {
|
||||||
|
val dir = getPackDir(pack)
|
||||||
|
if (dir.exists()) {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
}
|
||||||
|
_packList.remove(pack)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createFromCurrent(name: String): ThemePack {
|
||||||
|
val folderName = System.currentTimeMillis().toString()
|
||||||
|
val pack = ThemePack(
|
||||||
|
name = name,
|
||||||
|
folderName = folderName,
|
||||||
|
appearance = ThemeAppearanceConfig.fromCurrent(),
|
||||||
|
bgImageLight = ThemeConfig.bgImageLight,
|
||||||
|
bgImageDark = ThemeConfig.bgImageDark,
|
||||||
|
bgImageBlurring = ThemeConfig.bgImageBlurring,
|
||||||
|
bgImageNBlurring = ThemeConfig.bgImageNBlurring,
|
||||||
|
navIconBookshelf = MainConfig.navIconBookshelf,
|
||||||
|
navIconExplore = MainConfig.navIconExplore,
|
||||||
|
navIconRss = MainConfig.navIconRss,
|
||||||
|
navIconMy = MainConfig.navIconMy,
|
||||||
|
coverTextColor = CoverConfig.coverTextColor,
|
||||||
|
coverShadowColor = CoverConfig.coverShadowColor,
|
||||||
|
coverTextColorN = CoverConfig.coverTextColorN,
|
||||||
|
coverShadowColorN = CoverConfig.coverShadowColorN,
|
||||||
|
coverShowName = CoverConfig.coverShowName,
|
||||||
|
coverShowAuthor = CoverConfig.coverShowAuthor,
|
||||||
|
coverShowNameN = CoverConfig.coverShowNameN,
|
||||||
|
coverShowAuthorN = CoverConfig.coverShowAuthorN,
|
||||||
|
coverDefaultColor = CoverConfig.coverDefaultColor,
|
||||||
|
coverShowShadow = CoverConfig.coverShowShadow,
|
||||||
|
coverShowStroke = CoverConfig.coverShowStroke,
|
||||||
|
coverInfoOrientation = CoverConfig.coverInfoOrientation,
|
||||||
|
defaultCover = CoverConfig.defaultCover,
|
||||||
|
defaultCoverDark = CoverConfig.defaultCoverDark
|
||||||
|
)
|
||||||
|
|
||||||
|
val dir = File(baseDir, folderName)
|
||||||
|
dir.mkdirs()
|
||||||
|
|
||||||
|
copyResourceFile(pack.appearance.appFontPath, dir, "app_font")
|
||||||
|
copyResourceFile(pack.bgImageLight, dir, "bg_light")
|
||||||
|
copyResourceFile(pack.bgImageDark, dir, "bg_dark")
|
||||||
|
copyResourceFile(pack.navIconBookshelf, dir, "nav_bookshelf")
|
||||||
|
copyResourceFile(pack.navIconExplore, dir, "nav_explore")
|
||||||
|
copyResourceFile(pack.navIconRss, dir, "nav_rss")
|
||||||
|
copyResourceFile(pack.navIconMy, dir, "nav_my")
|
||||||
|
|
||||||
|
copyCoverImages(pack.defaultCover, dir, "cover_day")
|
||||||
|
copyCoverImages(pack.defaultCoverDark, dir, "cover_night")
|
||||||
|
|
||||||
|
savePack(pack)
|
||||||
|
_packList.add(pack)
|
||||||
|
return pack
|
||||||
|
}
|
||||||
|
|
||||||
|
fun applyPack(pack: ThemePack) {
|
||||||
|
val dir = getPackDir(pack)
|
||||||
|
val restoredAppearance = pack.appearance.copy(
|
||||||
|
appFontPath = restoreResourcePath(dir, "app_font", pack.appearance.appFontPath)
|
||||||
|
)
|
||||||
|
restoredAppearance.applyToThemeConfig()
|
||||||
|
|
||||||
|
ThemeConfig.bgImageLight = restoreResourcePath(dir, "bg_light", pack.bgImageLight)
|
||||||
|
ThemeConfig.bgImageDark = restoreResourcePath(dir, "bg_dark", pack.bgImageDark)
|
||||||
|
ThemeConfig.bgImageBlurring = pack.bgImageBlurring
|
||||||
|
ThemeConfig.bgImageNBlurring = pack.bgImageNBlurring
|
||||||
|
|
||||||
|
MainConfig.navIconBookshelf = restoreResourcePath(dir, "nav_bookshelf", pack.navIconBookshelf) ?: ""
|
||||||
|
MainConfig.navIconExplore = restoreResourcePath(dir, "nav_explore", pack.navIconExplore) ?: ""
|
||||||
|
MainConfig.navIconRss = restoreResourcePath(dir, "nav_rss", pack.navIconRss) ?: ""
|
||||||
|
MainConfig.navIconMy = restoreResourcePath(dir, "nav_my", pack.navIconMy) ?: ""
|
||||||
|
|
||||||
|
CoverConfig.coverTextColor = pack.coverTextColor
|
||||||
|
CoverConfig.coverShadowColor = pack.coverShadowColor
|
||||||
|
CoverConfig.coverTextColorN = pack.coverTextColorN
|
||||||
|
CoverConfig.coverShadowColorN = pack.coverShadowColorN
|
||||||
|
CoverConfig.coverShowName = pack.coverShowName
|
||||||
|
CoverConfig.coverShowAuthor = pack.coverShowAuthor
|
||||||
|
CoverConfig.coverShowNameN = pack.coverShowNameN
|
||||||
|
CoverConfig.coverShowAuthorN = pack.coverShowAuthorN
|
||||||
|
CoverConfig.coverDefaultColor = pack.coverDefaultColor
|
||||||
|
CoverConfig.coverShowShadow = pack.coverShowShadow
|
||||||
|
CoverConfig.coverShowStroke = pack.coverShowStroke
|
||||||
|
CoverConfig.coverInfoOrientation = pack.coverInfoOrientation
|
||||||
|
CoverConfig.defaultCover = restoreCoverImages(dir, "cover_day", pack.defaultCover)
|
||||||
|
CoverConfig.defaultCoverDark = restoreCoverImages(dir, "cover_night", pack.defaultCoverDark)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun exportToZip(pack: ThemePack, destFile: File) {
|
||||||
|
val dir = getPackDir(pack)
|
||||||
|
if (!dir.exists()) return
|
||||||
|
ZipOutputStream(FileOutputStream(destFile)).use { zos ->
|
||||||
|
dir.walkTopDown().forEach { file ->
|
||||||
|
if (file.isFile) {
|
||||||
|
val entryName = file.relativeTo(dir).path.replace('\\', '/')
|
||||||
|
zos.putNextEntry(ZipEntry(entryName))
|
||||||
|
FileInputStream(file).use { fis ->
|
||||||
|
fis.copyTo(zos)
|
||||||
|
}
|
||||||
|
zos.closeEntry()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importFromZip(zipFile: File): ThemePack? {
|
||||||
|
val folderName = System.currentTimeMillis().toString()
|
||||||
|
val dir = File(baseDir, folderName)
|
||||||
|
dir.mkdirs()
|
||||||
|
|
||||||
|
return kotlin.runCatching {
|
||||||
|
ZipInputStream(FileInputStream(zipFile)).use { zis ->
|
||||||
|
var entry = zis.nextEntry
|
||||||
|
while (entry != null) {
|
||||||
|
val outFile = resolveZipOutputFile(dir, entry)
|
||||||
|
if (entry.isDirectory) {
|
||||||
|
outFile.mkdirs()
|
||||||
|
} else {
|
||||||
|
outFile.parentFile?.mkdirs()
|
||||||
|
FileOutputStream(outFile).use { fos ->
|
||||||
|
zis.copyTo(fos)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
zis.closeEntry()
|
||||||
|
entry = zis.nextEntry
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val configFile = File(dir, CONFIG_FILE)
|
||||||
|
if (!configFile.exists()) {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
val json = configFile.readText()
|
||||||
|
val pack = PACK_GSON.fromJson(json, ThemePack::class.java)
|
||||||
|
pack.folderName = folderName
|
||||||
|
_packList.add(pack)
|
||||||
|
pack
|
||||||
|
}.onFailure {
|
||||||
|
dir.deleteRecursively()
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun resolveZipOutputFile(dir: File, entry: ZipEntry): File {
|
||||||
|
val outFile = File(dir, entry.name).canonicalFile
|
||||||
|
val canonicalDir = dir.canonicalFile
|
||||||
|
if (outFile.path == canonicalDir.path ||
|
||||||
|
outFile.path.startsWith(canonicalDir.path + File.separator)
|
||||||
|
) {
|
||||||
|
return outFile
|
||||||
|
}
|
||||||
|
throw IllegalArgumentException("Invalid theme pack entry: ${entry.name}")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyResourceFile(sourcePath: String?, destDir: File, prefix: String) {
|
||||||
|
if (sourcePath.isNullOrEmpty()) return
|
||||||
|
val sourceUri = Uri.parse(sourcePath)
|
||||||
|
val sourceName = sourceUri.lastPathSegment ?: sourcePath
|
||||||
|
val ext = sourceName.substringAfterLast(".", "dat").ifBlank { "dat" }
|
||||||
|
val destFile = File(destDir, "$prefix.$ext")
|
||||||
|
|
||||||
|
if (sourceUri.scheme == "content") {
|
||||||
|
appCtx.contentResolver.openInputStream(sourceUri)?.use { input ->
|
||||||
|
FileOutputStream(destFile).use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
val sourceFile = File(sourcePath)
|
||||||
|
if (!sourceFile.exists()) return
|
||||||
|
sourceFile.copyTo(destFile, overwrite = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreResourcePath(packDir: File, prefix: String, originalPath: String?): String? {
|
||||||
|
if (originalPath.isNullOrEmpty()) return null
|
||||||
|
val candidates = packDir.listFiles { file ->
|
||||||
|
file.name.startsWith(prefix) && file.isFile
|
||||||
|
}
|
||||||
|
if (candidates.isNullOrEmpty()) return null
|
||||||
|
val sourceFile = candidates.first()
|
||||||
|
val targetDir = File(appCtx.filesDir, "theme_pack_resources")
|
||||||
|
targetDir.mkdirs()
|
||||||
|
val targetFile = File(targetDir, "${System.currentTimeMillis()}_${sourceFile.name}")
|
||||||
|
sourceFile.copyTo(targetFile, overwrite = true)
|
||||||
|
return targetFile.absolutePath
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun copyCoverImages(coverPaths: String, destDir: File, prefix: String) {
|
||||||
|
val paths = coverPaths.split(",").filter { it.isNotBlank() }
|
||||||
|
paths.forEachIndexed { index, path ->
|
||||||
|
copyResourceFile(path, destDir, "${prefix}_$index")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun restoreCoverImages(packDir: File, prefix: String, originalPaths: String): String {
|
||||||
|
if (originalPaths.isEmpty()) return ""
|
||||||
|
val candidates = packDir.listFiles { file ->
|
||||||
|
file.name.startsWith(prefix + "_") && file.isFile
|
||||||
|
}
|
||||||
|
if (candidates.isNullOrEmpty()) return originalPaths
|
||||||
|
val sortedCandidates = candidates.sortedBy { file ->
|
||||||
|
file.name.substringAfterLast("_").substringBefore(".").toIntOrNull() ?: 0
|
||||||
|
}
|
||||||
|
val coverDir = File(appCtx.externalFiles, "covers")
|
||||||
|
coverDir.mkdirs()
|
||||||
|
val restoredPaths = mutableListOf<String>()
|
||||||
|
sortedCandidates.forEach { sourceFile ->
|
||||||
|
val targetFile = File(coverDir, sourceFile.name)
|
||||||
|
sourceFile.copyTo(targetFile, overwrite = true)
|
||||||
|
restoredPaths.add(targetFile.absolutePath)
|
||||||
|
}
|
||||||
|
return if (restoredPaths.isEmpty()) originalPaths else restoredPaths.joinToString(",")
|
||||||
|
}
|
||||||
|
|
||||||
|
data class ThemePack(
|
||||||
|
var name: String = "",
|
||||||
|
@Transient
|
||||||
|
var folderName: String = "",
|
||||||
|
var appearance: ThemeAppearanceConfig = ThemeAppearanceConfig(),
|
||||||
|
var bgImageLight: String? = null,
|
||||||
|
var bgImageDark: String? = null,
|
||||||
|
var bgImageBlurring: Int = 0,
|
||||||
|
var bgImageNBlurring: Int = 0,
|
||||||
|
var navIconBookshelf: String = "",
|
||||||
|
var navIconExplore: String = "",
|
||||||
|
var navIconRss: String = "",
|
||||||
|
var navIconMy: String = "",
|
||||||
|
var coverTextColor: Int = -16777216,
|
||||||
|
var coverShadowColor: Int = -16777216,
|
||||||
|
var coverTextColorN: Int = -1,
|
||||||
|
var coverShadowColorN: Int = -1,
|
||||||
|
var coverShowName: Boolean = true,
|
||||||
|
var coverShowAuthor: Boolean = true,
|
||||||
|
var coverShowNameN: Boolean = true,
|
||||||
|
var coverShowAuthorN: Boolean = true,
|
||||||
|
var coverDefaultColor: Boolean = true,
|
||||||
|
var coverShowShadow: Boolean = false,
|
||||||
|
var coverShowStroke: Boolean = true,
|
||||||
|
var coverInfoOrientation: String = "0",
|
||||||
|
var defaultCover: String = "",
|
||||||
|
var defaultCoverDark: String = ""
|
||||||
|
) {
|
||||||
|
val themeColor: Int get() = appearance.themeColor
|
||||||
|
val appFontPath: String? get() = appearance.appFontPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
package io.legado.app.help.config
|
||||||
|
|
||||||
|
import com.google.gson.JsonDeserializationContext
|
||||||
|
import com.google.gson.JsonDeserializer
|
||||||
|
import com.google.gson.JsonElement
|
||||||
|
import com.google.gson.JsonObject
|
||||||
|
import com.google.gson.JsonSerializationContext
|
||||||
|
import com.google.gson.JsonSerializer
|
||||||
|
import io.legado.app.utils.getBoolean
|
||||||
|
import io.legado.app.utils.getInt
|
||||||
|
import io.legado.app.utils.getString
|
||||||
|
import java.lang.reflect.Type
|
||||||
|
|
||||||
|
class ThemePackSerializer : JsonSerializer<ThemePackConfig.ThemePack>,
|
||||||
|
JsonDeserializer<ThemePackConfig.ThemePack> {
|
||||||
|
|
||||||
|
override fun serialize(
|
||||||
|
src: ThemePackConfig.ThemePack,
|
||||||
|
typeOfSrc: Type,
|
||||||
|
context: JsonSerializationContext
|
||||||
|
): JsonElement {
|
||||||
|
val obj = JsonObject()
|
||||||
|
obj.addProperty("name", src.name)
|
||||||
|
ThemeAppearanceJson.writeTo(obj, src.appearance)
|
||||||
|
obj.addProperty("bgImageLight", src.bgImageLight)
|
||||||
|
obj.addProperty("bgImageDark", src.bgImageDark)
|
||||||
|
obj.addProperty("bgImageBlurring", src.bgImageBlurring)
|
||||||
|
obj.addProperty("bgImageNBlurring", src.bgImageNBlurring)
|
||||||
|
obj.addProperty("navIconBookshelf", src.navIconBookshelf)
|
||||||
|
obj.addProperty("navIconExplore", src.navIconExplore)
|
||||||
|
obj.addProperty("navIconRss", src.navIconRss)
|
||||||
|
obj.addProperty("navIconMy", src.navIconMy)
|
||||||
|
obj.addProperty("coverTextColor", ThemeAppearanceJson.colorToHex(src.coverTextColor))
|
||||||
|
obj.addProperty("coverShadowColor", ThemeAppearanceJson.colorToHex(src.coverShadowColor))
|
||||||
|
obj.addProperty("coverTextColorN", ThemeAppearanceJson.colorToHex(src.coverTextColorN))
|
||||||
|
obj.addProperty("coverShadowColorN", ThemeAppearanceJson.colorToHex(src.coverShadowColorN))
|
||||||
|
obj.addProperty("coverShowName", src.coverShowName)
|
||||||
|
obj.addProperty("coverShowAuthor", src.coverShowAuthor)
|
||||||
|
obj.addProperty("coverShowNameN", src.coverShowNameN)
|
||||||
|
obj.addProperty("coverShowAuthorN", src.coverShowAuthorN)
|
||||||
|
obj.addProperty("coverDefaultColor", src.coverDefaultColor)
|
||||||
|
obj.addProperty("coverShowShadow", src.coverShowShadow)
|
||||||
|
obj.addProperty("coverShowStroke", src.coverShowStroke)
|
||||||
|
obj.addProperty("coverInfoOrientation", src.coverInfoOrientation)
|
||||||
|
obj.addProperty("defaultCover", src.defaultCover)
|
||||||
|
obj.addProperty("defaultCoverDark", src.defaultCoverDark)
|
||||||
|
return obj
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun deserialize(
|
||||||
|
json: JsonElement,
|
||||||
|
typeOfT: Type,
|
||||||
|
context: JsonDeserializationContext
|
||||||
|
): ThemePackConfig.ThemePack {
|
||||||
|
val obj = json.asJsonObject
|
||||||
|
return ThemePackConfig.ThemePack(
|
||||||
|
name = obj.getString("name", "") ?: "",
|
||||||
|
appearance = ThemeAppearanceJson.readFrom(obj),
|
||||||
|
bgImageLight = obj.getString("bgImageLight", null),
|
||||||
|
bgImageDark = obj.getString("bgImageDark", null),
|
||||||
|
bgImageBlurring = obj.getInt("bgImageBlurring", 0),
|
||||||
|
bgImageNBlurring = obj.getInt("bgImageNBlurring", 0),
|
||||||
|
navIconBookshelf = obj.getString("navIconBookshelf", "") ?: "",
|
||||||
|
navIconExplore = obj.getString("navIconExplore", "") ?: "",
|
||||||
|
navIconRss = obj.getString("navIconRss", "") ?: "",
|
||||||
|
navIconMy = obj.getString("navIconMy", "") ?: "",
|
||||||
|
coverTextColor = ThemeAppearanceJson.readColor(obj, "coverTextColor").takeIf { it != 0 } ?: -16777216,
|
||||||
|
coverShadowColor = ThemeAppearanceJson.readColor(obj, "coverShadowColor").takeIf { it != 0 } ?: -16777216,
|
||||||
|
coverTextColorN = ThemeAppearanceJson.readColor(obj, "coverTextColorN").takeIf { it != 0 } ?: -1,
|
||||||
|
coverShadowColorN = ThemeAppearanceJson.readColor(obj, "coverShadowColorN").takeIf { it != 0 } ?: -1,
|
||||||
|
coverShowName = obj.getBoolean("coverShowName", true),
|
||||||
|
coverShowAuthor = obj.getBoolean("coverShowAuthor", true),
|
||||||
|
coverShowNameN = obj.getBoolean("coverShowNameN", true),
|
||||||
|
coverShowAuthorN = obj.getBoolean("coverShowAuthorN", true),
|
||||||
|
coverDefaultColor = obj.getBoolean("coverDefaultColor", true),
|
||||||
|
coverShowShadow = obj.getBoolean("coverShowShadow", false),
|
||||||
|
coverShowStroke = obj.getBoolean("coverShowStroke", true),
|
||||||
|
coverInfoOrientation = obj.getString("coverInfoOrientation", "0") ?: "0",
|
||||||
|
defaultCover = obj.getString("defaultCover", "") ?: "",
|
||||||
|
defaultCoverDark = obj.getString("defaultCoverDark", "") ?: ""
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,6 +14,7 @@ import io.legado.app.help.DirectLinkUpload
|
|||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.OldThemeConfig
|
||||||
|
import io.legado.app.help.config.PersonalizationThemeConfig
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
@@ -83,6 +84,7 @@ object Backup {
|
|||||||
ReadBookConfig.configFileName,
|
ReadBookConfig.configFileName,
|
||||||
ReadBookConfig.shareConfigFileName,
|
ReadBookConfig.shareConfigFileName,
|
||||||
OldThemeConfig.configFileName,
|
OldThemeConfig.configFileName,
|
||||||
|
PersonalizationThemeConfig.configFileName,
|
||||||
BookCover.configFileName,
|
BookCover.configFileName,
|
||||||
"config.xml"
|
"config.xml"
|
||||||
)
|
)
|
||||||
@@ -181,6 +183,8 @@ object Backup {
|
|||||||
FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
|
FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
|
||||||
.writeText(GSON.toJson(it))
|
.writeText(GSON.toJson(it))
|
||||||
}
|
}
|
||||||
|
FileUtils.createFileIfNotExist(backupPath + File.separator + PersonalizationThemeConfig.configFileName)
|
||||||
|
.writeText(PersonalizationThemeConfig.toJson())
|
||||||
currentCoroutineContext().ensureActive()
|
currentCoroutineContext().ensureActive()
|
||||||
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
|
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
|
||||||
sp.edit(commit = true) {
|
sp.edit(commit = true) {
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ object BackupConfig {
|
|||||||
private const val readConfigKey = "readConfig"
|
private const val readConfigKey = "readConfig"
|
||||||
private const val themeConfigKey = "themeConfig"
|
private const val themeConfigKey = "themeConfig"
|
||||||
private const val coverConfigKey = "coverConfig"
|
private const val coverConfigKey = "coverConfig"
|
||||||
|
private const val personalizationThemeConfigKey = "personalizationThemeConfig"
|
||||||
private const val localBookKey = "localBook"
|
private const val localBookKey = "localBook"
|
||||||
|
|
||||||
//配置忽略key
|
//配置忽略key
|
||||||
@@ -30,6 +31,7 @@ object BackupConfig {
|
|||||||
readConfigKey,
|
readConfigKey,
|
||||||
PreferKey.themeMode,
|
PreferKey.themeMode,
|
||||||
themeConfigKey,
|
themeConfigKey,
|
||||||
|
personalizationThemeConfigKey,
|
||||||
coverConfigKey,
|
coverConfigKey,
|
||||||
PreferKey.bookshelfLayout,
|
PreferKey.bookshelfLayout,
|
||||||
PreferKey.showRss,
|
PreferKey.showRss,
|
||||||
@@ -42,6 +44,7 @@ object BackupConfig {
|
|||||||
appCtx.getString(R.string.read_config),
|
appCtx.getString(R.string.read_config),
|
||||||
appCtx.getString(R.string.theme_mode),
|
appCtx.getString(R.string.theme_mode),
|
||||||
appCtx.getString(R.string.theme_config),
|
appCtx.getString(R.string.theme_config),
|
||||||
|
"深度个性化主题",
|
||||||
appCtx.getString(R.string.cover_config),
|
appCtx.getString(R.string.cover_config),
|
||||||
appCtx.getString(R.string.bookshelf_layout),
|
appCtx.getString(R.string.bookshelf_layout),
|
||||||
appCtx.getString(R.string.show_rss),
|
appCtx.getString(R.string.show_rss),
|
||||||
@@ -84,15 +87,9 @@ object BackupConfig {
|
|||||||
|
|
||||||
private val themePrefKeys = arrayOf(
|
private val themePrefKeys = arrayOf(
|
||||||
PreferKey.cPrimary,
|
PreferKey.cPrimary,
|
||||||
PreferKey.cAccent,
|
PreferKey.cNPrimary,
|
||||||
PreferKey.cBackground,
|
|
||||||
PreferKey.cBBackground,
|
|
||||||
PreferKey.bgImage,
|
PreferKey.bgImage,
|
||||||
PreferKey.bgImageBlurring,
|
PreferKey.bgImageBlurring,
|
||||||
PreferKey.cNPrimary,
|
|
||||||
PreferKey.cNAccent,
|
|
||||||
PreferKey.cNBackground,
|
|
||||||
PreferKey.cNBBackground,
|
|
||||||
PreferKey.bgImageN,
|
PreferKey.bgImageN,
|
||||||
PreferKey.bgImageNBlurring
|
PreferKey.bgImageNBlurring
|
||||||
)
|
)
|
||||||
@@ -128,6 +125,8 @@ object BackupConfig {
|
|||||||
get() = ignoreConfig[themeConfigKey] == true
|
get() = ignoreConfig[themeConfigKey] == true
|
||||||
private val ignoreCoverConfig: Boolean
|
private val ignoreCoverConfig: Boolean
|
||||||
get() = ignoreConfig[coverConfigKey] == true
|
get() = ignoreConfig[coverConfigKey] == true
|
||||||
|
val ignorePersonalizationThemeConfig: Boolean
|
||||||
|
get() = ignoreConfig[personalizationThemeConfigKey] == true
|
||||||
private val ignoreBookshelfLayout: Boolean
|
private val ignoreBookshelfLayout: Boolean
|
||||||
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
|
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
|
||||||
private val ignoreShowRss: Boolean
|
private val ignoreShowRss: Boolean
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import io.legado.app.help.book.isLocal
|
|||||||
import io.legado.app.help.book.upType
|
import io.legado.app.help.book.upType
|
||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.OldThemeConfig
|
||||||
|
import io.legado.app.help.config.PersonalizationThemeConfig
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
@@ -233,6 +234,16 @@ object Restore {
|
|||||||
}?.onFailure {
|
}?.onFailure {
|
||||||
AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it)
|
AppLog.put("恢复封面规则出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
|
if (!BackupConfig.ignorePersonalizationThemeConfig) {
|
||||||
|
File(path, PersonalizationThemeConfig.configFileName).takeIf {
|
||||||
|
it.exists()
|
||||||
|
}?.runCatching {
|
||||||
|
val json = readText()
|
||||||
|
PersonalizationThemeConfig.fromJson(json)
|
||||||
|
}?.onFailure {
|
||||||
|
AppLog.put("恢复个性化主题出错\n${it.localizedMessage}", it)
|
||||||
|
}
|
||||||
|
}
|
||||||
if (!BackupConfig.ignoreReadConfig) {
|
if (!BackupConfig.ignoreReadConfig) {
|
||||||
//恢复阅读界面配置
|
//恢复阅读界面配置
|
||||||
File(path, ReadBookConfig.configFileName).takeIf {
|
File(path, ReadBookConfig.configFileName).takeIf {
|
||||||
|
|||||||
@@ -24,9 +24,9 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
|
|
||||||
private suspend fun getLatestRelease(): List<AppReleaseInfo> {
|
private suspend fun getLatestRelease(): List<AppReleaseInfo> {
|
||||||
val url = if (checkVariant == AppVariant.OFFICIAL)
|
val url = if (checkVariant == AppVariant.OFFICIAL)
|
||||||
"https://api.github.com/repos/HapeLee/legado-with-MD3/releases/latest"
|
"https://api.github.com/repos/325506/legado-with-MD3-DIY/releases/latest"
|
||||||
else
|
else
|
||||||
"https://api.github.com/repos/HapeLee/legado-with-MD3/releases"
|
"https://api.github.com/repos/325506/legado-with-MD3-DIY/releases"
|
||||||
|
|
||||||
val res = okHttpClient.newCallResponse { url(url) }
|
val res = okHttpClient.newCallResponse { url(url) }
|
||||||
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
|
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
|
||||||
@@ -65,7 +65,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
suspend fun getReleaseByTag(tag: String): AppUpdate.UpdateInfo? {
|
suspend fun getReleaseByTag(tag: String): AppUpdate.UpdateInfo? {
|
||||||
val url = "https://api.github.com/repos/HapeLee/legado-with-MD3/releases/tags/$tag"
|
val url = "https://api.github.com/repos/325506/legado-with-MD3-DIY/releases/tags/$tag"
|
||||||
val res = okHttpClient.newCallResponse { url(url) }
|
val res = okHttpClient.newCallResponse { url(url) }
|
||||||
if (!res.isSuccessful) return null
|
if (!res.isSuccessful) return null
|
||||||
|
|
||||||
@@ -86,7 +86,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
val currentVersion = AppConst.appInfo.versionName
|
val currentVersion = AppConst.appInfo.versionName
|
||||||
val releases = getLatestRelease()
|
val releases = getLatestRelease()
|
||||||
|
|
||||||
val filtered = if (checkVariant == AppVariant.ALL) {
|
val filtered = if (checkVariant == AppVariant.ALL || checkVariant == AppVariant.UNKNOWN) {
|
||||||
releases
|
releases
|
||||||
} else {
|
} else {
|
||||||
releases.filter { it.appVariant == checkVariant }
|
releases.filter { it.appVariant == checkVariant }
|
||||||
@@ -153,8 +153,8 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
|||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
fun parse(version: String): SemVer {
|
fun parse(version: String): SemVer {
|
||||||
val regex = Regex("""(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?""")
|
val regex = Regex("""(\d+)\.(\d+)\.(\d+)(?:[-_]([\w.]+))?""")
|
||||||
val match = regex.matchEntire(version)
|
val match = regex.find(version)
|
||||||
?: throw IllegalArgumentException("Invalid version: $version")
|
?: throw IllegalArgumentException("Invalid version: $version")
|
||||||
val (maj, min, pat, pre) = match.destructured
|
val (maj, min, pat, pre) = match.destructured
|
||||||
return SemVer(maj.toInt(), min.toInt(), pat.toInt(), pre.ifBlank { null })
|
return SemVer(maj.toInt(), min.toInt(), pat.toInt(), pre.ifBlank { null })
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import io.legado.app.data.appDb
|
|||||||
import io.legado.app.data.entities.BaseSource
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.help.CacheManager
|
||||||
import io.legado.app.help.coroutine.Coroutine
|
import io.legado.app.help.coroutine.Coroutine
|
||||||
import io.legado.app.model.AudioPlay
|
import io.legado.app.model.AudioPlay
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
@@ -17,22 +19,25 @@ import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setChapter
|
|||||||
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
import io.legado.app.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
|
||||||
import io.legado.app.ui.rss.read.RssJsExtensions
|
import io.legado.app.ui.rss.read.RssJsExtensions
|
||||||
import io.legado.app.utils.GSON
|
import io.legado.app.utils.GSON
|
||||||
import io.legado.app.utils.escapeForJs
|
|
||||||
import io.legado.app.utils.fromJsonObject
|
import io.legado.app.utils.fromJsonObject
|
||||||
|
import io.legado.app.utils.isJsonObject
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.UUID
|
import java.util.UUID
|
||||||
|
|
||||||
@Suppress("unused")
|
@Suppress("unused")
|
||||||
class WebJsExtensions(
|
class WebJsExtensions(
|
||||||
source: BaseSource, activity: AppCompatActivity,
|
source: BaseSource,
|
||||||
private val webView: WebView,
|
activity: AppCompatActivity?,
|
||||||
private val bookType: Int = 0,
|
webView: WebView,
|
||||||
|
bookType: Int = 0,
|
||||||
callback: Callback? = null
|
callback: Callback? = null
|
||||||
) : RssJsExtensions(activity, source) {
|
) : RssJsExtensions(activity, source) {
|
||||||
private val callbackRef: WeakReference<Callback> = WeakReference(callback)
|
private val callbackRef: WeakReference<Callback> = WeakReference(callback)
|
||||||
|
private val webViewRef: WeakReference<WebView?> = WeakReference(webView)
|
||||||
|
|
||||||
interface Callback {
|
interface Callback {
|
||||||
fun upConfig(config: String)
|
fun upConfig(config: String)
|
||||||
|
fun onNavigateToArticles(sortUrl: String? = null)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
@@ -40,6 +45,16 @@ class WebJsExtensions(
|
|||||||
callbackRef.get()?.upConfig(config)
|
callbackRef.get()?.upConfig(config)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun navigateToArticles() {
|
||||||
|
callbackRef.get()?.onNavigateToArticles(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
fun navigateToArticles(sortUrl: String?) {
|
||||||
|
callbackRef.get()?.onNavigateToArticles(sortUrl)
|
||||||
|
}
|
||||||
|
|
||||||
private val bookAndChapter by lazy {
|
private val bookAndChapter by lazy {
|
||||||
var book: Book? = null
|
var book: Book? = null
|
||||||
var chapter: BookChapter? = null
|
var chapter: BookChapter? = null
|
||||||
@@ -63,154 +78,201 @@ class WebJsExtensions(
|
|||||||
private val book: Book? get() = bookAndChapter.first
|
private val book: Book? get() = bookAndChapter.first
|
||||||
private val chapter: BookChapter? get() = bookAndChapter.second
|
private val chapter: BookChapter? get() = bookAndChapter.second
|
||||||
|
|
||||||
/**
|
val analyzeRule by lazy {
|
||||||
* 由软件主动注入的js函数调用
|
AnalyzeRule(book, source = getSource()).setChapter(chapter)
|
||||||
*/
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun request(funName: String, jsParam: Array<String>, id: String) {
|
fun request(funName: String, jsParam: Array<String?>, id: String) {
|
||||||
val activity = activityRef.get() ?: return
|
val activity = activityRef.get() ?: return
|
||||||
Coroutine.async(activity.lifecycleScope) {
|
Coroutine.async(activity.lifecycleScope) {
|
||||||
|
val params = Array(6) { i -> jsParam.getOrNull(i) }
|
||||||
|
val p0 = params[0]
|
||||||
|
val p1 = params[1]
|
||||||
|
val p2 = params[2]
|
||||||
|
val p3 = params[3]
|
||||||
|
val p4 = params[4]
|
||||||
|
val p5 = params[5]
|
||||||
when (funName) {
|
when (funName) {
|
||||||
"run" -> AnalyzeRule(book, getSource()).run {
|
"run" -> {
|
||||||
setCoroutineContext(coroutineContext)
|
analyzeRule.setCoroutineContext(coroutineContext)
|
||||||
setChapter(chapter)
|
.evalJS(
|
||||||
evalJS(jsParam[0]).toString()
|
p0 ?: throw NoStackTraceException("error null")
|
||||||
|
).toString()
|
||||||
}
|
}
|
||||||
"ajaxAwait" -> {
|
"ajaxAwait" -> {
|
||||||
ajax(jsParam[0], jsParam[1].toIntOrNull()).toString()
|
ajax(
|
||||||
|
p0 ?: throw NoStackTraceException("error url null"),
|
||||||
|
p1?.toLongOrNull()
|
||||||
|
).toString()
|
||||||
}
|
}
|
||||||
"connectAwait" -> {
|
"connectAwait" -> {
|
||||||
connect(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
connect(
|
||||||
|
p0 ?: throw NoStackTraceException("error url null"),
|
||||||
|
p1,
|
||||||
|
p2?.toIntOrNull()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"getAwait" -> {
|
"getAwait" -> {
|
||||||
get(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
get(
|
||||||
|
p0 ?: throw NoStackTraceException("error url null"),
|
||||||
|
p1 ?: throw NoStackTraceException("error header null"),
|
||||||
|
p2?.toIntOrNull()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"headAwait" -> {
|
"headAwait" -> {
|
||||||
head(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
head(
|
||||||
|
p0 ?: throw NoStackTraceException("error url null"),
|
||||||
|
p1 ?: throw NoStackTraceException("error header null"),
|
||||||
|
p2?.toIntOrNull()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"postAwait" -> {
|
"postAwait" -> {
|
||||||
post(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toIntOrNull())
|
post(
|
||||||
|
p0 ?: throw NoStackTraceException("error url null"),
|
||||||
|
p1 ?: throw NoStackTraceException("error body null"),
|
||||||
|
p2 ?: throw NoStackTraceException("error header null"),
|
||||||
|
p3?.toIntOrNull()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
"webViewAwait" -> {
|
"webViewAwait" -> {
|
||||||
webView(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toBoolean()).toString()
|
webView(
|
||||||
|
p0,
|
||||||
|
p1,
|
||||||
|
p2,
|
||||||
|
p3.toBoolean()
|
||||||
|
).toString()
|
||||||
}
|
}
|
||||||
"webViewGetSourceAwait" -> {
|
"webViewGetSourceAwait" -> {
|
||||||
webViewGetSource(
|
webViewGetSource(
|
||||||
jsParam[0],
|
p0,
|
||||||
jsParam[1],
|
p1,
|
||||||
jsParam[2],
|
p2,
|
||||||
jsParam[3],
|
p3 ?: throw NoStackTraceException("error sourceRegex null"),
|
||||||
jsParam[4].toBoolean(),
|
p4.toBoolean(),
|
||||||
jsParam[5].toLongOrNull() ?: 0
|
p5?.toLongOrNull() ?: 0
|
||||||
).toString()
|
).toString()
|
||||||
}
|
}
|
||||||
"decryptStrAwait" -> {
|
"decryptStrAwait" -> {
|
||||||
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).decryptStr(jsParam[3])
|
createSymmetricCrypto(
|
||||||
|
p0 ?: throw NoStackTraceException("error transformation null"),
|
||||||
|
p1 ?: throw NoStackTraceException("error key null"),
|
||||||
|
p2
|
||||||
|
).decryptStr(p3 ?: throw NoStackTraceException("error data null"))
|
||||||
}
|
}
|
||||||
"encryptBase64Await" -> {
|
"encryptBase64Await" -> {
|
||||||
createSymmetricCrypto(
|
createSymmetricCrypto(
|
||||||
jsParam[0],
|
p0 ?: throw NoStackTraceException("error transformation null"),
|
||||||
jsParam[1],
|
p1 ?: throw NoStackTraceException("error key null"),
|
||||||
jsParam[2]
|
p2
|
||||||
).encryptBase64(jsParam[3])
|
).encryptBase64(p3 ?: throw NoStackTraceException("error data null"))
|
||||||
}
|
}
|
||||||
"encryptHexAwait" -> {
|
"encryptHexAwait" -> {
|
||||||
createSymmetricCrypto(jsParam[0], jsParam[1], jsParam[2]).encryptHex(jsParam[3])
|
createSymmetricCrypto(
|
||||||
|
p0 ?: throw NoStackTraceException("error transformation null"),
|
||||||
|
p1 ?: throw NoStackTraceException("error key null"),
|
||||||
|
p2
|
||||||
|
).encryptHex(p3 ?: throw NoStackTraceException("error data null"))
|
||||||
}
|
}
|
||||||
"createSignHexAwait" -> {
|
"createSignHexAwait" -> {
|
||||||
createSign(jsParam[0]).setPublicKey(jsParam[1]).setPrivateKey(jsParam[2])
|
createSign(p0 ?: throw NoStackTraceException("error algorithm null"))
|
||||||
.signHex(jsParam[3])
|
.setPublicKey(p1 ?: throw NoStackTraceException("error publicKey null"))
|
||||||
|
.setPrivateKey(p2 ?: throw NoStackTraceException("error privateKey null"))
|
||||||
|
.signHex(p3 ?: throw NoStackTraceException("error data null"))
|
||||||
}
|
}
|
||||||
"downloadFileAwait" -> {
|
"downloadFileAwait" -> {
|
||||||
downloadFile(jsParam[0])
|
downloadFile(p0 ?: throw NoStackTraceException("error url null"))
|
||||||
}
|
}
|
||||||
"readTxtFileAwait" -> {
|
"readTxtFileAwait" -> {
|
||||||
readTxtFile(jsParam[0])
|
readTxtFile(p0 ?: throw NoStackTraceException("error path null"))
|
||||||
}
|
}
|
||||||
"importScriptAwait" -> {
|
"importScriptAwait" -> {
|
||||||
importScript(jsParam[0])
|
importScript(p0 ?: throw NoStackTraceException("error path null"))
|
||||||
}
|
}
|
||||||
"getStringAwait" -> AnalyzeRule(book, getSource()).run {
|
"getStringAwait" -> {
|
||||||
setCoroutineContext(coroutineContext)
|
analyzeRule.setCoroutineContext(coroutineContext)
|
||||||
setChapter(chapter)
|
.getString(p0, p1)
|
||||||
getString(jsParam[0], jsParam[1])
|
|
||||||
}
|
}
|
||||||
else -> "error funName"
|
else -> throw NoStackTraceException("error funName")
|
||||||
}
|
}
|
||||||
}.onSuccess { data ->
|
}.onSuccess { data ->
|
||||||
webView.evaluateJavascript(
|
CacheManager.putMemory(id, data)
|
||||||
"window.$JSBridgeResult('$id', '${data.escapeForJs()}', null);",
|
webViewRef.get()?.evaluateJavascript("window.$JSBridgeResult('$id', true);", null)
|
||||||
null
|
|
||||||
)
|
|
||||||
}.onError {
|
}.onError {
|
||||||
webView.evaluateJavascript(
|
CacheManager.putMemory(id, it.localizedMessage ?: "err")
|
||||||
"window.$JSBridgeResult('$id', null, '${it.localizedMessage?.escapeForJs()}');",
|
webViewRef.get()?.evaluateJavascript("window.$JSBridgeResult('$id', false);", null)
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun toast(msg: String?) {
|
fun toast(msg: String?) {
|
||||||
super.toast(msg)
|
super.toast(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun longToast(msg: String?) {
|
fun longToast(msg: String?) {
|
||||||
super.longToast(msg)
|
super.longToast(msg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JavascriptInterface
|
||||||
|
override fun open(name: String, url: String?, title: String?, origin: String?) {
|
||||||
|
when (name) {
|
||||||
|
"sort" -> {
|
||||||
|
val sortUrl = if (!url.isNullOrBlank()) {
|
||||||
|
if (url.isJsonObject()) url else title?.let { "{\"$it\":\"$url\"}" } ?: url
|
||||||
|
} else null
|
||||||
|
callbackRef.get()?.onNavigateToArticles(sortUrl)
|
||||||
|
}
|
||||||
|
else -> super.open(name, url, title, origin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun log(msg: String?): String {
|
fun log(msg: String?): String {
|
||||||
return super.log(msg).toString()
|
return super.log(msg).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun ajax(url: String): String? {
|
@JvmOverloads
|
||||||
return super.ajax(url, 9000)
|
fun ajax(url: String, callTimeout: Int = 9000): String? {
|
||||||
|
return super.ajax(url, callTimeout.toLong())
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun ajax(url: String, callTimeout: Int?): String? {
|
@JvmOverloads
|
||||||
return super.ajax(url, callTimeout?.toLong())
|
fun connect(urlStr: String, header: String? = null, callTimeout: Int? = 9000): String {
|
||||||
}
|
|
||||||
@JavascriptInterface
|
|
||||||
fun connect(urlStr: String?): String {
|
|
||||||
if (urlStr.isNullOrEmpty()) return "error empty url"
|
|
||||||
return super.connect(urlStr, null, 9000).toString()
|
|
||||||
}
|
|
||||||
@JavascriptInterface
|
|
||||||
fun connect(urlStr: String, header: String): String {
|
|
||||||
return super.connect(urlStr, header, 9000).toString()
|
|
||||||
}
|
|
||||||
@JavascriptInterface
|
|
||||||
fun connect(urlStr: String, header: String, callTimeout: Int?): String {
|
|
||||||
return super.connect(urlStr, header, callTimeout?.toLong()).toString()
|
return super.connect(urlStr, header, callTimeout?.toLong()).toString()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun get(urlStr: String, headers: String): String {
|
@JvmOverloads
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
fun get(urlStr: String, header: String, timeout: Int? = 9000): String {
|
||||||
return super.get(urlStr, headerMap, 9000).body()
|
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||||
|
return super.get(urlStr, headers, timeout).body()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun get(urlStr: String, headers: String, timeout: Int?): String {
|
@JvmOverloads
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
fun post(urlStr: String, body: String, header: String, timeout: Int? = 9000): String {
|
||||||
return super.get(urlStr, headerMap, timeout).body()
|
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||||
|
return super.post(urlStr, body, headers, timeout).body()
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun post(urlStr: String, body: String, headers: String): String {
|
@JvmOverloads
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
fun head(urlStr: String, header: String, timeout: Int? = 9000): String {
|
||||||
return super.post(urlStr, body, headerMap, 9000).body()
|
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||||
|
return GSON.toJson(super.head(urlStr, headers, timeout).headers())
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun post(urlStr: String, body: String, headers: String, timeout: Int?): String {
|
fun getStringList(rule: String?, mContent: String? = null, isUrl: Boolean = false): List<String>? {
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
return super.getStringList(rule, mContent, isUrl)
|
||||||
return super.post(urlStr, body, headerMap, timeout).body()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
fun head(urlStr: String, headers: String): String {
|
fun getString(ruleStr: String?, mContent: String? = null, isUrl: Boolean = false): String {
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
return super.getString(ruleStr, mContent, isUrl)
|
||||||
return GSON.toJson(super.head(urlStr, headerMap, 9000).headers())
|
|
||||||
}
|
|
||||||
@JavascriptInterface
|
|
||||||
fun head(urlStr: String, headers: String, timeout: Int?): String {
|
|
||||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
|
||||||
return GSON.toJson(super.head(urlStr, headerMap, timeout).headers())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
@@ -218,6 +280,7 @@ class WebJsExtensions(
|
|||||||
val letters = "abcdefghijklmnopqrstuvwxyz"
|
val letters = "abcdefghijklmnopqrstuvwxyz"
|
||||||
return letters.random()
|
return letters.random()
|
||||||
}
|
}
|
||||||
|
|
||||||
val uuid by lazy {
|
val uuid by lazy {
|
||||||
UUID.randomUUID().toString().replace('-', getRandomLetter()).chunked(6)
|
UUID.randomUUID().toString().replace('-', getRandomLetter()).chunked(6)
|
||||||
}
|
}
|
||||||
@@ -234,9 +297,12 @@ class WebJsExtensions(
|
|||||||
"<script src=\"$nameUrl\"></script>"
|
"<script src=\"$nameUrl\"></script>"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val getInjectionString = "try{var cache=$nameCache,source=$nameSource,java=$nameJava;}catch(e){}"
|
||||||
|
|
||||||
val JS_INJECTION by lazy {
|
val JS_INJECTION by lazy {
|
||||||
"""
|
"""
|
||||||
const requestId = n => 'req_' + n + '_' + Date.now() + '_' + Math.random().toString(36).slice(-3);
|
const requestId = n => 'req_' + n + '_' + Date.now() + '_' + Math.random().toString(36).slice(-3);
|
||||||
|
const params = a => a.map(p => p != null && typeof p.toString === 'function' ? p.toString() : null);
|
||||||
const JSBridgeCallbacks = {};
|
const JSBridgeCallbacks = {};
|
||||||
const java = window.$nameJava;
|
const java = window.$nameJava;
|
||||||
delete window.$nameJava;
|
delete window.$nameJava;
|
||||||
@@ -248,122 +314,124 @@ class WebJsExtensions(
|
|||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("run");
|
const id = requestId("run");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("run", [String(jsCode)], id);
|
java.request("run", [String(jsCode)], id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function ajaxAwait(url, callTimeout) {
|
function ajaxAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("ajaxAwait");
|
const id = requestId("ajaxAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("ajaxAwait", [String(url), String(callTimeout)], id);
|
java.request("ajaxAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function connectAwait(url, header, callTimeout) {
|
function connectAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("connectAwait");
|
const id = requestId("connectAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("connectAwait", [String(url), String(header), String(callTimeout)], id);
|
java.request("connectAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function getAwait(url, header, callTimeout) {
|
function getAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("getAwait");
|
const id = requestId("getAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("getAwait", [String(url), String(header), String(callTimeout)], id);
|
java.request("getAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function headAwait(url, header, callTimeout) {
|
function headAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("headAwait");
|
const id = requestId("headAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("headAwait", [String(url), String(header), String(callTimeout)], id);
|
java.request("headAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function postAwait(url, body, header, callTimeout) {
|
function postAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("postAwait");
|
const id = requestId("postAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("postAwait", [String(url), String(body), String(header), String(callTimeout)], id);
|
java.request("postAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function webViewAwait(html, url, js, cacheFirst) {
|
function webViewAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("webViewAwait");
|
const id = requestId("webViewAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("webViewAwait", [String(html), String(url), String(js), String(cacheFirst)], id);
|
java.request("webViewAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function webViewGetSourceAwait(html, url, js, sourceRegex, cacheFirst, delayTime) {
|
function webViewGetSourceAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("webViewGetSourceAwait");
|
const id = requestId("webViewGetSourceAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("webViewGetSourceAwait", [String(html), String(url), String(js), String(sourceRegex), String(cacheFirst), String(delayTime)], id);
|
java.request("webViewGetSourceAwait", params(args), id);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function decryptStrAwait(transformation, key, iv, data) {
|
function decryptStrAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("decryptStrAwait");
|
const id = requestId("decryptStrAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("decryptStrAwait", [String(transformation), String(key), String(iv), String(data)], id);
|
java.request("decryptStrAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function encryptBase64Await(transformation, key, iv, data) {
|
function encryptBase64Await(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("encryptBase64Await");
|
const id = requestId("encryptBase64Await");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("encryptBase64Await", [String(transformation), String(key), String(iv), String(data)], id);
|
java.request("encryptBase64Await", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function encryptHexAwait(transformation, key, iv, data) {
|
function encryptHexAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("encryptHexAwait");
|
const id = requestId("encryptHexAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("encryptHexAwait", [String(transformation), String(key), String(iv), String(data)], id);
|
java.request("encryptHexAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function createSignHexAwait(algorithm, publicKey, privateKey, data) {
|
function createSignHexAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("createSignHexAwait");
|
const id = requestId("createSignHexAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("createSignHexAwait", [String(algorithm), String(publicKey), String(privateKey), String(data)], id);
|
java.request("createSignHexAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function downloadFileAwait(url) {
|
function downloadFileAwait(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("downloadFileAwait");
|
const id = requestId("downloadFileAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("downloadFileAwait", [String(url)], id);
|
java.request("downloadFileAwait", [String(url)], id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function readTxtFileAwait(path) {
|
function readTxtFileAwait(path) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("readTxtFileAwait");
|
const id = requestId("readTxtFileAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("readTxtFileAwait", [String(path)], id);
|
java.request("readTxtFileAwait", [String(path)], id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function importScriptAwait(url) {
|
function importScriptAwait(url) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("importScriptAwait");
|
const id = requestId("importScriptAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("importScriptAwait", [String(url)], id);
|
java.request("importScriptAwait", [String(url)], id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
function getStringAwait(ruleStr, mContent) {
|
function getStringAwait(...args) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("getStringAwait");
|
const id = requestId("getStringAwait");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("getStringAwait", [String(ruleStr), String(mContent)], id);
|
java.request("getStringAwait", params(args), id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
window.$JSBridgeResult = function(requestId, result, error) {
|
window.$JSBridgeResult = function(id, success) {
|
||||||
if (JSBridgeCallbacks[requestId]) {
|
const callBack = JSBridgeCallbacks[id];
|
||||||
if (error) {
|
if (callBack) {
|
||||||
JSBridgeCallbacks[requestId].reject(error);
|
const result = cache.getFromMemory(id);
|
||||||
|
if (success) {
|
||||||
|
callBack.resolve(result);
|
||||||
} else {
|
} else {
|
||||||
JSBridgeCallbacks[requestId].resolve(result);
|
callBack.reject(result);
|
||||||
}
|
}
|
||||||
delete JSBridgeCallbacks[requestId];
|
delete JSBridgeCallbacks[id];
|
||||||
}
|
}
|
||||||
};""".trimIndent()
|
};""".trimIndent()
|
||||||
}
|
}
|
||||||
@@ -374,21 +442,25 @@ class WebJsExtensions(
|
|||||||
const JSBridgeCallbacks = {};
|
const JSBridgeCallbacks = {};
|
||||||
const java = window.$nameJava;
|
const java = window.$nameJava;
|
||||||
delete window.$nameJava;
|
delete window.$nameJava;
|
||||||
|
const cache = window.$nameCache;
|
||||||
|
delete window.$nameCache;
|
||||||
function run(jsCode) {
|
function run(jsCode) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const id = requestId("run");
|
const id = requestId("run");
|
||||||
JSBridgeCallbacks[id] = { resolve, reject };
|
JSBridgeCallbacks[id] = { resolve, reject };
|
||||||
java?.request("run", [String(jsCode)], id);
|
java.request("run", [String(jsCode)], id);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
window.$JSBridgeResult = function(requestId, result, error) {
|
window.$JSBridgeResult = function(id, success) {
|
||||||
if (JSBridgeCallbacks[requestId]) {
|
const callBack = JSBridgeCallbacks[id];
|
||||||
if (error) {
|
if (callBack) {
|
||||||
JSBridgeCallbacks[requestId].reject(error);
|
const result = cache.getFromMemory(id);
|
||||||
|
if (success) {
|
||||||
|
callBack.resolve(result);
|
||||||
} else {
|
} else {
|
||||||
JSBridgeCallbacks[requestId].resolve(result);
|
callBack.reject(result);
|
||||||
}
|
}
|
||||||
delete JSBridgeCallbacks[requestId];
|
delete JSBridgeCallbacks[id];
|
||||||
}
|
}
|
||||||
};""".trimIndent()
|
};""".trimIndent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ object AudioPlay : CoroutineScope by MainScope() {
|
|||||||
var durChapterPos = 0
|
var durChapterPos = 0
|
||||||
var durChapter: BookChapter? = null
|
var durChapter: BookChapter? = null
|
||||||
var durPlayUrl = ""
|
var durPlayUrl = ""
|
||||||
|
var durLyric: String? = null
|
||||||
var durAudioSize = 0
|
var durAudioSize = 0
|
||||||
var inBookshelf = false
|
var inBookshelf = false
|
||||||
var bookSource: BookSource? = null
|
var bookSource: BookSource? = null
|
||||||
@@ -153,14 +154,23 @@ object AudioPlay : CoroutineScope by MainScope() {
|
|||||||
.onSuccess { content ->
|
.onSuccess { content ->
|
||||||
if (content.isEmpty()) {
|
if (content.isEmpty()) {
|
||||||
appCtx.toastOnUi("未获取到资源链接")
|
appCtx.toastOnUi("未获取到资源链接")
|
||||||
|
// 加载失败时清空歌词
|
||||||
|
durLyric = null
|
||||||
|
callback?.upLyric(null)
|
||||||
} else {
|
} else {
|
||||||
contentLoadFinish(chapter, content)
|
contentLoadFinish(chapter, content)
|
||||||
}
|
}
|
||||||
}.onError {
|
}.onError { error ->
|
||||||
AppLog.put("获取资源链接出错\n$it", it, true)
|
AppLog.put("获取资源链接出错\n$error", error, true)
|
||||||
upLoading(false)
|
upLoading(false)
|
||||||
|
// 加载失败时清空歌词
|
||||||
|
durLyric = null
|
||||||
|
callback?.upLyric(null)
|
||||||
}.onCancel {
|
}.onCancel {
|
||||||
removeLoading(index)
|
removeLoading(index)
|
||||||
|
// 取消加载时清空歌词
|
||||||
|
durLyric = null
|
||||||
|
callback?.upLyric(null)
|
||||||
}.onFinally {
|
}.onFinally {
|
||||||
removeLoading(index)
|
removeLoading(index)
|
||||||
}
|
}
|
||||||
@@ -177,7 +187,10 @@ object AudioPlay : CoroutineScope by MainScope() {
|
|||||||
private fun contentLoadFinish(chapter: BookChapter, content: String) {
|
private fun contentLoadFinish(chapter: BookChapter, content: String) {
|
||||||
if (chapter.index == book?.durChapterIndex) {
|
if (chapter.index == book?.durChapterIndex) {
|
||||||
durPlayUrl = content
|
durPlayUrl = content
|
||||||
|
durLyric = chapter.getVariable("lyric").takeIf { it.isNotBlank() }
|
||||||
upPlayUrl()
|
upPlayUrl()
|
||||||
|
// 重新加载歌词,确保循环播放时歌词进度重置
|
||||||
|
callback?.upLyric(durLyric)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -397,6 +410,8 @@ object AudioPlay : CoroutineScope by MainScope() {
|
|||||||
fun register(context: Context) {
|
fun register(context: Context) {
|
||||||
activityContext = context
|
activityContext = context
|
||||||
callback = context as CallBack
|
callback = context as CallBack
|
||||||
|
// 主动更新歌词状态,确保重新进入时歌词控件能正确显示
|
||||||
|
callback?.upLyric(durLyric)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun unregister(context: Context) {
|
fun unregister(context: Context) {
|
||||||
@@ -418,6 +433,8 @@ object AudioPlay : CoroutineScope by MainScope() {
|
|||||||
interface CallBack {
|
interface CallBack {
|
||||||
|
|
||||||
fun upLoading(loading: Boolean)
|
fun upLoading(loading: Boolean)
|
||||||
|
fun upLyric(lyric: String?)
|
||||||
|
fun upLyricP(position: Int)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -138,6 +138,90 @@ object Debug {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDebug(scope: CoroutineScope, rssSource: RssSource, key: String) {
|
||||||
|
cancelDebug()
|
||||||
|
debugSource = rssSource.sourceUrl
|
||||||
|
startTime = System.currentTimeMillis()
|
||||||
|
when {
|
||||||
|
key.contains("@js:") -> {
|
||||||
|
val ruleContent = rssSource.ruleContent
|
||||||
|
if (ruleContent.isNullOrEmpty()) {
|
||||||
|
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
|
||||||
|
} else {
|
||||||
|
val rssArticle = RssArticle()
|
||||||
|
rssArticle.origin = rssSource.sourceUrl
|
||||||
|
rssArticle.link = key
|
||||||
|
log(debugSource, "⇒开始解析@js:链接:$key")
|
||||||
|
rssContentDebug(scope, rssArticle, ruleContent, rssSource)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
key.contains("::") -> {
|
||||||
|
val name = key.substringBefore("::")
|
||||||
|
val url = key.substringAfter("::")
|
||||||
|
log(debugSource, "⇒开始访问分类页:$url")
|
||||||
|
log(debugSource, "︾开始解析分类页")
|
||||||
|
rssSortDebug(scope, rssSource, name, url)
|
||||||
|
}
|
||||||
|
|
||||||
|
key.isAbsUrl() -> {
|
||||||
|
val ruleContent = rssSource.ruleContent
|
||||||
|
if (!rssSource.ruleArticles.isNullOrBlank() && rssSource.ruleDescription.isNullOrBlank()) {
|
||||||
|
if (ruleContent.isNullOrEmpty()) {
|
||||||
|
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
|
||||||
|
} else {
|
||||||
|
val rssArticle = RssArticle()
|
||||||
|
rssArticle.origin = rssSource.sourceUrl
|
||||||
|
rssArticle.link = key
|
||||||
|
log(debugSource, "⇒开始访问内容页:$key")
|
||||||
|
rssContentDebug(scope, rssArticle, ruleContent, rssSource)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(debugSource, "⇒存在描述规则,不解析内容页")
|
||||||
|
log(debugSource, "︽解析完成", state = 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
val searchUrl = rssSource.searchUrl
|
||||||
|
if (searchUrl.isNullOrEmpty()) {
|
||||||
|
log(debugSource, "⇒搜索URL为空", state = -1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log(debugSource, "⇒开始搜索关键字:$key")
|
||||||
|
log(debugSource, "︾开始解析搜索页")
|
||||||
|
rssSortDebug(scope, rssSource, "搜索", searchUrl, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun rssSortDebug(scope: CoroutineScope, rssSource: RssSource, name: String, url: String, key: String? = null) {
|
||||||
|
Rss.getArticles(scope, name, url, rssSource, 1, key)
|
||||||
|
.onSuccess {
|
||||||
|
if (it.first.isEmpty()) {
|
||||||
|
log(debugSource, "⇒列表页解析成功,为空")
|
||||||
|
log(debugSource, "︽解析完成", state = 1000)
|
||||||
|
} else {
|
||||||
|
val ruleContent = rssSource.ruleContent
|
||||||
|
if (!rssSource.ruleArticles.isNullOrBlank() && rssSource.ruleDescription.isNullOrBlank()) {
|
||||||
|
log(debugSource, "︽列表页解析完成")
|
||||||
|
log(debugSource, showTime = false)
|
||||||
|
if (ruleContent.isNullOrEmpty()) {
|
||||||
|
log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000)
|
||||||
|
} else {
|
||||||
|
rssContentDebug(scope, it.first[0], ruleContent, rssSource)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log(debugSource, "⇒存在描述规则,不解析内容页")
|
||||||
|
log(debugSource, "︽解析完成", state = 1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onError {
|
||||||
|
log(debugSource, it.stackTraceStr, state = -1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun rssContentDebug(
|
private fun rssContentDebug(
|
||||||
scope: CoroutineScope,
|
scope: CoroutineScope,
|
||||||
rssArticle: RssArticle,
|
rssArticle: RssArticle,
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ object Rss {
|
|||||||
sortUrl: String,
|
sortUrl: String,
|
||||||
rssSource: RssSource,
|
rssSource: RssSource,
|
||||||
page: Int,
|
page: Int,
|
||||||
|
searchKey: String? = null,
|
||||||
context: CoroutineContext = Dispatchers.IO
|
context: CoroutineContext = Dispatchers.IO
|
||||||
): Coroutine<Pair<MutableList<RssArticle>, String?>> {
|
): Coroutine<Pair<MutableList<RssArticle>, String?>> {
|
||||||
return Coroutine.async(scope, context) {
|
return Coroutine.async(scope, context) {
|
||||||
getArticlesAwait(sortName, sortUrl, rssSource, page)
|
getArticlesAwait(sortName, sortUrl, rssSource, page, searchKey)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,8 +37,13 @@ object Rss {
|
|||||||
sortUrl: String,
|
sortUrl: String,
|
||||||
rssSource: RssSource,
|
rssSource: RssSource,
|
||||||
page: Int,
|
page: Int,
|
||||||
|
searchKey: String? = null
|
||||||
): Pair<MutableList<RssArticle>, String?> {
|
): Pair<MutableList<RssArticle>, String?> {
|
||||||
val ruleData = RuleData()
|
val ruleData = RuleData()
|
||||||
|
if (!searchKey.isNullOrBlank()) {
|
||||||
|
ruleData.putVariable("searchKey", searchKey)
|
||||||
|
ruleData.putVariable("key", searchKey)
|
||||||
|
}
|
||||||
val analyzeUrl = AnalyzeUrl(
|
val analyzeUrl = AnalyzeUrl(
|
||||||
sortUrl,
|
sortUrl,
|
||||||
page = page,
|
page = page,
|
||||||
|
|||||||
@@ -211,9 +211,9 @@ object BookContent {
|
|||||||
if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
|
if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
|
||||||
val useHtmlMap = mutableMapOf<String, String>()
|
val useHtmlMap = mutableMapOf<String, String>()
|
||||||
if (AppConfig.adaptSpecialStyle) {
|
if (AppConfig.adaptSpecialStyle) {
|
||||||
content = AppPattern.useHtmlRegex.replace(content) { matchResult ->
|
content = AppPattern.useHtmlRegex.replace(content) {
|
||||||
val placeholder = "{usehtml_${useHtmlMap.size}}"
|
val placeholder = "{usehtml_${useHtmlMap.size}}"
|
||||||
useHtmlMap[placeholder] = matchResult.value
|
useHtmlMap[placeholder] = it.value
|
||||||
placeholder
|
placeholder
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -225,6 +225,13 @@ object BookContent {
|
|||||||
content = content.replace(placeholder, originalContent)
|
content = content.replace(placeholder, originalContent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
//获取副文(歌词等)
|
||||||
|
contentRule.subContent?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
val subContent = analyzeRule.getString(it, unescape = false)
|
||||||
|
if (subContent.isNotBlank()) {
|
||||||
|
chapter.putVariable("lyric", subContent)
|
||||||
|
}
|
||||||
|
}
|
||||||
//获取下一页链接
|
//获取下一页链接
|
||||||
if (getNextPageUrl) {
|
if (getNextPageUrl) {
|
||||||
val nextUrlRule = contentRule.nextContentUrl
|
val nextUrlRule = contentRule.nextContentUrl
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ import io.legado.app.ui.widget.components.SplicedColumnGroup
|
|||||||
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
import io.legado.app.ui.widget.components.card.TextCard
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
import io.legado.app.ui.widget.components.settingItem.SettingItem
|
||||||
|
import io.legado.app.ui.widget.components.SettingItemWithDivider
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
@@ -132,57 +133,57 @@ fun AboutScreen(
|
|||||||
modifier = Modifier.padding(horizontal = 16.dp),
|
modifier = Modifier.padding(horizontal = 16.dp),
|
||||||
title = ""
|
title = ""
|
||||||
){
|
){
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.contributors),
|
title = stringResource(R.string.contributors),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.privacy_policy),
|
title = stringResource(R.string.privacy_policy),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
onShowMdFile("隐私政策", "privacyPolicy.md")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.license),
|
title = stringResource(R.string.license),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onShowMdFile("许可证", "LICENSE.md")
|
onShowMdFile("许可证", "LICENSE.md")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.disclaimer),
|
title = stringResource(R.string.disclaimer),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = {
|
onClick = {
|
||||||
onShowMdFile("免责声明", "disclaimer.md")
|
onShowMdFile("免责声明", "disclaimer.md")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.crash_log),
|
title = stringResource(R.string.crash_log),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = onShowCrashLogs
|
onClick = onShowCrashLogs
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.save_log),
|
title = stringResource(R.string.save_log),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = onSaveLog
|
onClick = onSaveLog
|
||||||
)
|
)
|
||||||
|
}
|
||||||
|
SettingItemWithDivider {
|
||||||
SettingItem(
|
SettingItem(
|
||||||
title = stringResource(R.string.create_heap_dump),
|
title = stringResource(R.string.create_heap_dump),
|
||||||
description = null,
|
|
||||||
option = null,
|
|
||||||
onClick = onCreateHeapDump
|
onClick = onCreateHeapDump
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -75,7 +75,7 @@ fun CrashReportScreen(
|
|||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.verticalScroll(rememberScrollState())
|
.verticalScroll(rememberScrollState())
|
||||||
) {
|
) {
|
||||||
Text(
|
AppText(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
style = LegadoTheme.typography.bodySmall,
|
style = LegadoTheme.typography.bodySmall,
|
||||||
fontFamily = FontFamily.Monospace
|
fontFamily = FontFamily.Monospace
|
||||||
|
|||||||
@@ -25,10 +25,13 @@ import androidx.lifecycle.lifecycleScope
|
|||||||
import androidx.transition.TransitionManager
|
import androidx.transition.TransitionManager
|
||||||
import com.google.android.material.color.DynamicColors
|
import com.google.android.material.color.DynamicColors
|
||||||
import com.google.android.material.color.DynamicColorsOptions
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
|
import androidx.constraintlayout.widget.ConstraintLayout
|
||||||
import com.google.android.material.color.MaterialColors
|
import com.google.android.material.color.MaterialColors
|
||||||
import com.google.android.material.slider.Slider
|
import com.google.android.material.slider.Slider
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||||
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
import com.google.android.material.transition.platform.MaterialContainerTransformSharedElementCallback
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import com.dirror.lyricviewx.OnPlayClickListener
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.VMBaseActivity
|
import io.legado.app.base.VMBaseActivity
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
@@ -43,6 +46,7 @@ import io.legado.app.databinding.ActivityAudioPlayBinding
|
|||||||
import io.legado.app.help.book.isAudio
|
import io.legado.app.help.book.isAudio
|
||||||
import io.legado.app.help.book.removeType
|
import io.legado.app.help.book.removeType
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.model.AudioPlay
|
import io.legado.app.model.AudioPlay
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.model.SourceCallBack
|
import io.legado.app.model.SourceCallBack
|
||||||
@@ -55,6 +59,7 @@ import io.legado.app.ui.login.SourceLoginActivity
|
|||||||
import io.legado.app.utils.StartActivityContract
|
import io.legado.app.utils.StartActivityContract
|
||||||
import io.legado.app.utils.ToolbarUtils.setAllIconsColor
|
import io.legado.app.utils.ToolbarUtils.setAllIconsColor
|
||||||
import io.legado.app.utils.applyNavigationBarPadding
|
import io.legado.app.utils.applyNavigationBarPadding
|
||||||
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
import io.legado.app.utils.observeEvent
|
import io.legado.app.utils.observeEvent
|
||||||
import io.legado.app.utils.observeEventSticky
|
import io.legado.app.utils.observeEventSticky
|
||||||
@@ -96,6 +101,9 @@ class AudioPlayActivity :
|
|||||||
private var secondaryContainerFinalColor: Int = 0
|
private var secondaryContainerFinalColor: Int = 0
|
||||||
private var currentJob: Job? = null
|
private var currentJob: Job? = null
|
||||||
private var wrappedContext: Context? = null
|
private var wrappedContext: Context? = null
|
||||||
|
private val lyricViewX by lazy { binding.lyricViewX }
|
||||||
|
private var lyricOn = false
|
||||||
|
private var oldLyric: String? = null
|
||||||
|
|
||||||
private val progressTimeFormat by lazy {
|
private val progressTimeFormat by lazy {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||||
@@ -227,6 +235,21 @@ class AudioPlayActivity :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
|
// 应用深度个性化中设置的字体
|
||||||
|
ThemeConfig.appFontPath?.let {
|
||||||
|
try {
|
||||||
|
val typeface = Typeface.createFromFile(it)
|
||||||
|
// 为所有文本控件设置字体
|
||||||
|
binding.tvTitle.typeface = typeface
|
||||||
|
binding.tvSubTitle.typeface = typeface
|
||||||
|
binding.tvDurTime.typeface = typeface
|
||||||
|
binding.tvAllTime.typeface = typeface
|
||||||
|
binding.tvSpeed.typeface = typeface
|
||||||
|
binding.tvTimer.typeface = typeface
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
binding.ivTimer.isEnabled = false
|
binding.ivTimer.isEnabled = false
|
||||||
binding.ivFastForward.isEnabled = false
|
binding.ivFastForward.isEnabled = false
|
||||||
|
|
||||||
@@ -279,7 +302,10 @@ class AudioPlayActivity :
|
|||||||
|
|
||||||
override fun onStopTrackingTouch(slider: Slider) {
|
override fun onStopTrackingTouch(slider: Slider) {
|
||||||
adjustProgress = false
|
adjustProgress = false
|
||||||
AudioPlay.adjustProgress(slider.value.toInt())
|
val progress = slider.value.toInt()
|
||||||
|
AudioPlay.adjustProgress(progress)
|
||||||
|
// 更新歌词进度
|
||||||
|
upLyricP(progress)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -503,6 +529,8 @@ class AudioPlayActivity :
|
|||||||
binding.playerProgress.trackActiveTintList = ColorStateList.valueOf(color)
|
binding.playerProgress.trackActiveTintList = ColorStateList.valueOf(color)
|
||||||
binding.tvDurTime.setTextColor(color)
|
binding.tvDurTime.setTextColor(color)
|
||||||
binding.tvAllTime.setTextColor(color)
|
binding.tvAllTime.setTextColor(color)
|
||||||
|
// 更新歌词高亮颜色
|
||||||
|
binding.lyricViewX.setCurrentColor(color)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -671,6 +699,8 @@ class AudioPlayActivity :
|
|||||||
val safeValue = progress.toFloat().coerceIn(slider.valueFrom, slider.valueTo)
|
val safeValue = progress.toFloat().coerceIn(slider.valueFrom, slider.valueTo)
|
||||||
if (!adjustProgress) slider.value = safeValue
|
if (!adjustProgress) slider.value = safeValue
|
||||||
binding.tvDurTime.text = progressTimeFormat.format(progress.toLong())
|
binding.tvDurTime.text = progressTimeFormat.format(progress.toLong())
|
||||||
|
// 更新歌词进度
|
||||||
|
upLyricP(progress)
|
||||||
}
|
}
|
||||||
|
|
||||||
observeEventSticky<Int>(EventBus.AUDIO_BUFFER_PROGRESS) {
|
observeEventSticky<Int>(EventBus.AUDIO_BUFFER_PROGRESS) {
|
||||||
@@ -702,6 +732,56 @@ class AudioPlayActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun upLyric(lyric: String?) {
|
||||||
|
if (oldLyric == lyric) return
|
||||||
|
oldLyric = lyric
|
||||||
|
if (lyric.isNullOrBlank()) {
|
||||||
|
// 没有歌词时,显示标题区域,隐藏歌词
|
||||||
|
binding.lyricViewX.gone()
|
||||||
|
binding.llTitle.visible()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// 有歌词时,隐藏标题区域,显示歌词
|
||||||
|
binding.llTitle.gone()
|
||||||
|
// 先隐藏歌词视图,避免显示旧歌词
|
||||||
|
binding.lyricViewX.gone()
|
||||||
|
lyricViewX.loadLyric(lyric)
|
||||||
|
binding.lyricViewX.visible()
|
||||||
|
if (lyricOn) {
|
||||||
|
upLyricP(AudioPlay.durChapterPos)
|
||||||
|
} else {
|
||||||
|
lyricOn = true
|
||||||
|
lyricViewX.apply {
|
||||||
|
setNormalTextSize(50F)
|
||||||
|
setCurrentTextSize(60F)
|
||||||
|
// 加载深度个性化中设置的字体
|
||||||
|
ThemeConfig.appFontPath?.let {
|
||||||
|
try {
|
||||||
|
val typeface = Typeface.createFromFile(it)
|
||||||
|
// LyricViewX 1.3.2 版本可能不支持 setTypeface 方法
|
||||||
|
// 暂时注释掉,后续可以通过 XML 或其他方式设置
|
||||||
|
// setTypeface(typeface)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
e.printStackTrace()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setDraggable(true, object : OnPlayClickListener {
|
||||||
|
override fun onPlayClick(time: Long): Boolean {
|
||||||
|
AudioPlay.adjustProgress(time.toInt())
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
lyricViewX.postDelayed({
|
||||||
|
upLyricP(AudioPlay.durChapterPos)
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun upLyricP(position: Int) {
|
||||||
|
lyricViewX.updateTime(position.toLong(), false)
|
||||||
|
}
|
||||||
|
|
||||||
override fun addToBookshelf(book: Book, toc: List<BookChapter>) {
|
override fun addToBookshelf(book: Book, toc: List<BookChapter>) {
|
||||||
viewModel.addToBookshelf(book, toc) {
|
viewModel.addToBookshelf(book, toc) {
|
||||||
toastOnUi("已添加到书架")
|
toastOnUi("已添加到书架")
|
||||||
|
|||||||
@@ -72,6 +72,8 @@ import io.legado.app.constant.BookType
|
|||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
import io.legado.app.help.book.isLocal
|
import io.legado.app.help.book.isLocal
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.ui.about.AppLogSheet
|
import io.legado.app.ui.about.AppLogSheet
|
||||||
import io.legado.app.ui.config.coverConfig.CoverConfig
|
import io.legado.app.ui.config.coverConfig.CoverConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
@@ -402,6 +404,9 @@ private fun BookInfoTransparentTopAppBar(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
||||||
|
val useDefaultCover = AppConfig.useDefaultCover || book?.customCoverUrl == "use_default_cover"
|
||||||
|
if (useDefaultCover) return null
|
||||||
|
|
||||||
val imageLoader = koinInject<ImageLoader>()
|
val imageLoader = koinInject<ImageLoader>()
|
||||||
var shouldExtractColor by remember(book?.bookUrl) { mutableStateOf(false) }
|
var shouldExtractColor by remember(book?.bookUrl) { mutableStateOf(false) }
|
||||||
|
|
||||||
@@ -413,7 +418,12 @@ private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val coverPath = if (shouldExtractColor) book?.getDisplayCover() else null
|
val isNight = AppConfig.isNightTheme
|
||||||
|
|
||||||
|
val coverPath = if (shouldExtractColor) {
|
||||||
|
book?.getDisplayCover()
|
||||||
|
} else null
|
||||||
|
|
||||||
val sourceOrigin = if (shouldExtractColor) book?.origin else null
|
val sourceOrigin = if (shouldExtractColor) book?.origin else null
|
||||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||||
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
||||||
@@ -469,8 +479,15 @@ private fun BookInfoTopBarActions(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun BookInfoBackdrop(book: Book) {
|
private fun BookInfoBackdrop(book: Book) {
|
||||||
val cover = book.getDisplayCover()
|
val useDefaultCover = AppConfig.useDefaultCover || book.customCoverUrl == "use_default_cover"
|
||||||
val sourceOrigin = book.origin
|
val isNight = AppConfig.isNightTheme
|
||||||
|
|
||||||
|
val cover = if (useDefaultCover) {
|
||||||
|
BookCover.getRandomDefaultPath(book.bookUrl, isNight)
|
||||||
|
} else {
|
||||||
|
book.getDisplayCover()
|
||||||
|
}
|
||||||
|
val sourceOrigin = if (!useDefaultCover) book.origin else null
|
||||||
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
val loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val imageLoader = koinInject<ImageLoader>()
|
val imageLoader = koinInject<ImageLoader>()
|
||||||
|
|||||||
@@ -709,19 +709,30 @@ class BookInfoViewModel(
|
|||||||
|
|
||||||
private fun refreshMeta(book: Book) {
|
private fun refreshMeta(book: Book) {
|
||||||
execute {
|
execute {
|
||||||
val kinds = book.getKindList().toMutableList()
|
val allKinds = book.getKindList()
|
||||||
|
val customKinds = allKinds.filter { it.startsWith("#") }
|
||||||
|
val sourceKinds = allKinds.filter { !it.startsWith("#") }.toMutableList()
|
||||||
if (book.isLocal) {
|
if (book.isLocal) {
|
||||||
val size = FileDoc.fromFile(book.bookUrl).size
|
val size = FileDoc.fromFile(book.bookUrl).size
|
||||||
if (size > 0) {
|
if (size > 0) {
|
||||||
kinds.add(ConvertUtils.formatFileSize(size))
|
sourceKinds.add(ConvertUtils.formatFileSize(size))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val mergedKinds = (customKinds + sourceKinds).distinct()
|
||||||
val userGroupIds = appDb.bookGroupDao.idsSum
|
val userGroupIds = appDb.bookGroupDao.idsSum
|
||||||
val groupAnd = userGroupIds and book.group
|
val groupAnd = userGroupIds and book.group
|
||||||
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
||||||
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
||||||
val normalizedGroupNames = groupNames.ifBlank { null }
|
val normalizedGroupNames = groupNames.ifBlank { null }
|
||||||
Triple(kinds.toList(), normalizedGroupNames, hasCustomGroup)
|
val persistedCustomKinds = currentKindLabels.filter { it.startsWith("#") }
|
||||||
|
val finalKinds = if (persistedCustomKinds.isNotEmpty()) {
|
||||||
|
(persistedCustomKinds + sourceKinds).distinct()
|
||||||
|
} else {
|
||||||
|
mergedKinds
|
||||||
|
}
|
||||||
|
book.kind = finalKinds.joinToString(",")
|
||||||
|
appDb.bookDao.update(book)
|
||||||
|
Triple(finalKinds, normalizedGroupNames, hasCustomGroup)
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
currentKindLabels = it.first
|
currentKindLabels = it.first
|
||||||
currentGroupNames = it.second
|
currentGroupNames = it.second
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.book.info.edit
|
|||||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.horizontalScroll
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
import androidx.compose.foundation.layout.consumeWindowInsets
|
import androidx.compose.foundation.layout.consumeWindowInsets
|
||||||
@@ -18,6 +19,8 @@ import androidx.compose.foundation.text.input.rememberTextFieldState
|
|||||||
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
import androidx.compose.foundation.text.input.setTextAndPlaceCursorAtEnd
|
||||||
import androidx.compose.foundation.verticalScroll
|
import androidx.compose.foundation.verticalScroll
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.FolderOpen
|
import androidx.compose.material.icons.filled.FolderOpen
|
||||||
import androidx.compose.material.icons.filled.ImageSearch
|
import androidx.compose.material.icons.filled.ImageSearch
|
||||||
import androidx.compose.material.icons.filled.Replay
|
import androidx.compose.material.icons.filled.Replay
|
||||||
@@ -27,6 +30,8 @@ import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
|||||||
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
import androidx.compose.material3.ExposedDropdownMenuAnchorType
|
||||||
import androidx.compose.material3.ExposedDropdownMenuBox
|
import androidx.compose.material3.ExposedDropdownMenuBox
|
||||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||||
|
|||||||