更多自定义设置 @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
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
@@ -35,6 +40,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
@@ -52,6 +58,7 @@ import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
|||||||
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
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
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.utils.SelectImageContract
|
import io.legado.app.utils.SelectImageContract
|
||||||
import io.legado.app.utils.launch
|
import io.legado.app.utils.launch
|
||||||
import io.legado.app.utils.showDialogFragment
|
import io.legado.app.utils.showDialogFragment
|
||||||
@@ -174,10 +181,17 @@ fun BookInfoEditContent(
|
|||||||
onCheckedChange = { viewModel.onFixedTypeChange(it) }
|
onCheckedChange = { viewModel.onFixedTypeChange(it) }
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
|
val bookInfoInputColor = ThemeConfig.bookInfoInputColor
|
||||||
|
val inputBackgroundColor = if (bookInfoInputColor != 0) {
|
||||||
|
Color(bookInfoInputColor)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = uiState.name,
|
value = uiState.name,
|
||||||
onValueChange = { viewModel.onNameChange(it) },
|
onValueChange = { viewModel.onNameChange(it) },
|
||||||
label = "书名",
|
label = "书名",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -185,6 +199,7 @@ fun BookInfoEditContent(
|
|||||||
value = uiState.author,
|
value = uiState.author,
|
||||||
onValueChange = { viewModel.onAuthorChange(it) },
|
onValueChange = { viewModel.onAuthorChange(it) },
|
||||||
label = "作者",
|
label = "作者",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -192,13 +207,21 @@ fun BookInfoEditContent(
|
|||||||
value = uiState.coverUrl ?: "",
|
value = uiState.coverUrl ?: "",
|
||||||
onValueChange = { viewModel.onCoverUrlChange(it) },
|
onValueChange = { viewModel.onCoverUrlChange(it) },
|
||||||
label = "封面链接",
|
label = "封面链接",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
|
KindEditor(
|
||||||
|
kindList = uiState.kindList,
|
||||||
|
onKindListChange = { viewModel.onKindListChange(it) },
|
||||||
|
backgroundColor = inputBackgroundColor
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
AppTextField(
|
AppTextField(
|
||||||
value = uiState.intro ?: "",
|
value = uiState.intro ?: "",
|
||||||
onValueChange = { viewModel.onIntroChange(it) },
|
onValueChange = { viewModel.onIntroChange(it) },
|
||||||
label = "简介",
|
label = "简介",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
@@ -206,6 +229,7 @@ fun BookInfoEditContent(
|
|||||||
value = uiState.remark ?: "",
|
value = uiState.remark ?: "",
|
||||||
onValueChange = { viewModel.onRemarkChange(it) },
|
onValueChange = { viewModel.onRemarkChange(it) },
|
||||||
label = "备注",
|
label = "备注",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -228,6 +252,13 @@ fun BookTypeDropdown(
|
|||||||
textFieldState.setTextAndPlaceCursorAtEnd(selectedType)
|
textFieldState.setTextAndPlaceCursorAtEnd(selectedType)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val bookInfoInputColor = ThemeConfig.bookInfoInputColor
|
||||||
|
val inputBackgroundColor = if (bookInfoInputColor != 0) {
|
||||||
|
Color(bookInfoInputColor)
|
||||||
|
} else {
|
||||||
|
Color.Unspecified
|
||||||
|
}
|
||||||
|
|
||||||
ExposedDropdownMenuBox(
|
ExposedDropdownMenuBox(
|
||||||
modifier = Modifier.padding(horizontal = 8.dp),
|
modifier = Modifier.padding(horizontal = 8.dp),
|
||||||
expanded = expanded,
|
expanded = expanded,
|
||||||
@@ -238,6 +269,7 @@ fun BookTypeDropdown(
|
|||||||
readOnly = true,
|
readOnly = true,
|
||||||
lineLimits = TextFieldLineLimits.SingleLine,
|
lineLimits = TextFieldLineLimits.SingleLine,
|
||||||
label = "书籍类型",
|
label = "书籍类型",
|
||||||
|
backgroundColor = inputBackgroundColor,
|
||||||
trailingIcon = {
|
trailingIcon = {
|
||||||
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded)
|
||||||
},
|
},
|
||||||
@@ -265,3 +297,90 @@ fun BookTypeDropdown(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun KindEditor(
|
||||||
|
kindList: List<String>,
|
||||||
|
onKindListChange: (List<String>) -> Unit,
|
||||||
|
backgroundColor: Color
|
||||||
|
) {
|
||||||
|
var newKindText by remember { mutableStateOf("") }
|
||||||
|
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppTextField(
|
||||||
|
value = newKindText,
|
||||||
|
onValueChange = { newKindText = it },
|
||||||
|
label = "标签",
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
)
|
||||||
|
Spacer(modifier = Modifier.width(8.dp))
|
||||||
|
MediumOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
val trimmed = newKindText.trim()
|
||||||
|
val tag = if (trimmed.startsWith("#")) trimmed else "#$trimmed"
|
||||||
|
if (tag.isNotBlank() && tag !in kindList) {
|
||||||
|
val sortedList = (kindList + tag).sortedWith(
|
||||||
|
compareBy<String> { !it.startsWith("#") }.thenBy { it }
|
||||||
|
)
|
||||||
|
onKindListChange(sortedList)
|
||||||
|
newKindText = ""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Spacer(modifier = Modifier.height(4.dp))
|
||||||
|
if (kindList.isNotEmpty()) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.horizontalScroll(rememberScrollState()),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
kindList.forEach { kind ->
|
||||||
|
Row(
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
AppTextField(
|
||||||
|
value = kind,
|
||||||
|
onValueChange = { newValue ->
|
||||||
|
val index = kindList.indexOf(kind)
|
||||||
|
if (index >= 0) {
|
||||||
|
val newList = kindList.toMutableList()
|
||||||
|
newList[index] = newValue
|
||||||
|
onKindListChange(newList)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
label = null,
|
||||||
|
backgroundColor = backgroundColor,
|
||||||
|
singleLine = true,
|
||||||
|
maxLines = 1,
|
||||||
|
modifier = Modifier.width(80.dp)
|
||||||
|
)
|
||||||
|
IconButton(
|
||||||
|
onClick = {
|
||||||
|
onKindListChange(kindList - kind)
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(0.dp)
|
||||||
|
) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Close,
|
||||||
|
contentDescription = "删除标签",
|
||||||
|
tint = Color.Red.copy(alpha = 0.7f),
|
||||||
|
modifier = Modifier.padding(4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,6 +19,7 @@ import io.legado.app.model.ReadBook
|
|||||||
import io.legado.app.utils.FileUtils
|
import io.legado.app.utils.FileUtils
|
||||||
import io.legado.app.utils.MD5Utils
|
import io.legado.app.utils.MD5Utils
|
||||||
import io.legado.app.utils.inputStream
|
import io.legado.app.utils.inputStream
|
||||||
|
import io.legado.app.utils.splitNotBlank
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
@@ -31,6 +32,7 @@ data class BookInfoEditUiState(
|
|||||||
val coverUrl: String? = null,
|
val coverUrl: String? = null,
|
||||||
val intro: String? = null,
|
val intro: String? = null,
|
||||||
val remark: String? = null,
|
val remark: String? = null,
|
||||||
|
val kindList: List<String> = emptyList(),
|
||||||
val selectedType: String = "文本",
|
val selectedType: String = "文本",
|
||||||
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
||||||
val fixedType: Boolean = false,
|
val fixedType: Boolean = false,
|
||||||
@@ -57,6 +59,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
|||||||
coverUrl = it.getDisplayCover(),
|
coverUrl = it.getDisplayCover(),
|
||||||
intro = it.getDisplayIntro(),
|
intro = it.getDisplayIntro(),
|
||||||
remark = it.remark,
|
remark = it.remark,
|
||||||
|
kindList = it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty(),
|
||||||
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
||||||
fixedType = it.config.fixedType,
|
fixedType = it.config.fixedType,
|
||||||
book = it
|
book = it
|
||||||
@@ -85,6 +88,10 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
|||||||
_uiState.value = _uiState.value.copy(remark = remark)
|
_uiState.value = _uiState.value.copy(remark = remark)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onKindListChange(kindList: List<String>) {
|
||||||
|
_uiState.value = _uiState.value.copy(kindList = kindList)
|
||||||
|
}
|
||||||
|
|
||||||
fun onBookTypeChange(bookType: String) {
|
fun onBookTypeChange(bookType: String) {
|
||||||
_uiState.value = _uiState.value.copy(selectedType = bookType)
|
_uiState.value = _uiState.value.copy(selectedType = bookType)
|
||||||
}
|
}
|
||||||
@@ -116,6 +123,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
|||||||
book.config.fixedType = currentState.fixedType
|
book.config.fixedType = currentState.fixedType
|
||||||
book.customCoverUrl = if (currentState.coverUrl == book.coverUrl) null else currentState.coverUrl
|
book.customCoverUrl = if (currentState.coverUrl == book.coverUrl) null else currentState.coverUrl
|
||||||
book.customIntro = if (currentState.intro == book.intro) null else currentState.intro
|
book.customIntro = if (currentState.intro == book.intro) null else currentState.intro
|
||||||
|
book.kind = currentState.kindList.joinToString(",")
|
||||||
BookHelp.updateCacheFolder(oldBook, book)
|
BookHelp.updateCacheFolder(oldBook, book)
|
||||||
|
|
||||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||||
|
|||||||
@@ -86,8 +86,12 @@ import io.legado.app.ui.book.read.config.ReadAloudDialog
|
|||||||
import io.legado.app.ui.book.read.config.ReadStyleDialog
|
import io.legado.app.ui.book.read.config.ReadStyleDialog
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.A_COLOR
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.A_COLOR
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.B_COLOR
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.B_COLOR
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_HEADER_COLOR
|
||||||
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_FOOTER_COLOR
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
|
||||||
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TITLE_COLOR
|
||||||
|
import io.legado.app.ui.book.read.config.RegexColorConfigDialog
|
||||||
|
import io.legado.app.ui.book.read.config.RegexColorConfigDialog.Companion.REGEX_RULE_COLOR
|
||||||
import io.legado.app.ui.book.read.config.ToolButtonConfigDialog
|
import io.legado.app.ui.book.read.config.ToolButtonConfigDialog
|
||||||
import io.legado.app.ui.book.read.config.UnderlineConfigDialog.Companion.U_COLOR
|
import io.legado.app.ui.book.read.config.UnderlineConfigDialog.Companion.U_COLOR
|
||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
import io.legado.app.ui.book.read.page.ContentTextView
|
||||||
@@ -95,6 +99,7 @@ import io.legado.app.ui.book.read.page.ReadView
|
|||||||
import io.legado.app.ui.book.read.page.entities.PageDirection
|
import io.legado.app.ui.book.read.page.entities.PageDirection
|
||||||
import io.legado.app.ui.book.read.page.entities.TextPage
|
import io.legado.app.ui.book.read.page.entities.TextPage
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||||
|
import io.legado.app.ui.book.read.page.provider.TextChapterLayout
|
||||||
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
import io.legado.app.ui.book.read.page.provider.LayoutProgressListener
|
||||||
import io.legado.app.ui.book.searchContent.SearchContentActivity
|
import io.legado.app.ui.book.searchContent.SearchContentActivity
|
||||||
import io.legado.app.ui.book.searchContent.SearchResult
|
import io.legado.app.ui.book.searchContent.SearchResult
|
||||||
@@ -1698,8 +1703,14 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TIP_COLOR -> {
|
TIP_HEADER_COLOR -> {
|
||||||
ReadTipConfig.tipColor = color
|
ReadTipConfig.tipHeaderColor = color
|
||||||
|
postEvent(EventBus.TIP_COLOR, "")
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
TIP_FOOTER_COLOR -> {
|
||||||
|
ReadTipConfig.tipFooterColor = color
|
||||||
postEvent(EventBus.TIP_COLOR, "")
|
postEvent(EventBus.TIP_COLOR, "")
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
}
|
}
|
||||||
@@ -1710,6 +1721,24 @@ class ReadBookActivity : BaseReadBookActivity(),
|
|||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TITLE_COLOR -> {
|
||||||
|
ReadBookConfig.titleColor = color
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
||||||
|
}
|
||||||
|
|
||||||
|
REGEX_RULE_COLOR -> {
|
||||||
|
val pos = RegexColorConfigDialog.pendingColorPosition
|
||||||
|
if (pos in ReadBookConfig.regexColorRules.indices) {
|
||||||
|
ReadBookConfig.regexColorRules[pos].color = color
|
||||||
|
ReadBookConfig.saveRegexColorRules()
|
||||||
|
TextChapterLayout.invalidateRegexCache()
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
||||||
|
}
|
||||||
|
val fontConfigDialog = supportFragmentManager.findFragmentByTag("FontConfigDialog")
|
||||||
|
(fontConfigDialog?.childFragmentManager?.findFragmentByTag("regexColorConfig") as? RegexColorConfigDialog)
|
||||||
|
?.onColorSelected(color)
|
||||||
|
}
|
||||||
|
|
||||||
B_COLOR -> {
|
B_COLOR -> {
|
||||||
setMenuCurBg(color)
|
setMenuCurBg(color)
|
||||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
||||||
|
|||||||
@@ -107,6 +107,10 @@ class FontConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_conf
|
|||||||
callBack2?.showShadowSet()
|
callBack2?.showShadowSet()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.btnRegexColor.setOnClickListener {
|
||||||
|
RegexColorConfigDialog().show(childFragmentManager, "regexColorConfig")
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnSelectFonts.setOnClickListener {
|
binding.btnSelectFonts.setOnClickListener {
|
||||||
callBack2?.showFontSelect()
|
callBack2?.showFontSelect()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.core.net.toUri
|
import androidx.core.net.toUri
|
||||||
|
import androidx.core.widget.addTextChangedListener
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
import androidx.recyclerview.widget.GridLayoutManager
|
import androidx.recyclerview.widget.GridLayoutManager
|
||||||
@@ -50,6 +51,7 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
|||||||
val curFontPath = callBack?.curFontPath ?: ""
|
val curFontPath = callBack?.curFontPath ?: ""
|
||||||
FontAdapter(requireContext(), curFontPath)
|
FontAdapter(requireContext(), curFontPath)
|
||||||
}
|
}
|
||||||
|
private var allFontItems: List<FileDoc> = emptyList()
|
||||||
|
|
||||||
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
||||||
it.uri?.let { uri ->
|
it.uri?.let { uri ->
|
||||||
@@ -76,6 +78,22 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
|||||||
binding.recyclerView.layoutManager = GridLayoutManager(context, 2)
|
binding.recyclerView.layoutManager = GridLayoutManager(context, 2)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
initView()
|
initView()
|
||||||
|
initSearch()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initSearch() {
|
||||||
|
binding.etSearch.addTextChangedListener {
|
||||||
|
filterFonts(it.toString())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun filterFonts(keyword: String) {
|
||||||
|
val filtered = if (keyword.isBlank()) {
|
||||||
|
allFontItems
|
||||||
|
} else {
|
||||||
|
allFontItems.filter { it.name.contains(keyword, ignoreCase = true) }
|
||||||
|
}
|
||||||
|
adapter.setItems(filtered)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -135,6 +153,7 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
|||||||
} ?: ArrayList()
|
} ?: ArrayList()
|
||||||
mergeFontItems(fontItems, getLocalFonts())
|
mergeFontItems(fontItems, getLocalFonts())
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
|
allFontItems = it
|
||||||
adapter.setItems(it)
|
adapter.setItems(it)
|
||||||
}.onError {
|
}.onError {
|
||||||
AppLog.put("加载字体文件失败\n${it.localizedMessage}", it)
|
AppLog.put("加载字体文件失败\n${it.localizedMessage}", it)
|
||||||
@@ -176,8 +195,10 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
|||||||
callBack?.selectFont("")
|
callBack?.selectFont("")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var explicitCallback: CallBack? = null
|
||||||
|
|
||||||
private val callBack: CallBack?
|
private val callBack: CallBack?
|
||||||
get() = (parentFragment as? CallBack) ?: (activity as? CallBack)
|
get() = explicitCallback ?: (parentFragment as? CallBack) ?: (activity as? CallBack)
|
||||||
|
|
||||||
inner class FontAdapter(context: Context, curFilePath: String) :
|
inner class FontAdapter(context: Context, curFilePath: String) :
|
||||||
RecyclerAdapter<FileDoc, ItemFontBinding>(context) {
|
RecyclerAdapter<FileDoc, ItemFontBinding>(context) {
|
||||||
|
|||||||
@@ -11,23 +11,32 @@ import io.legado.app.help.config.ReadBookConfig
|
|||||||
import io.legado.app.help.config.ReadTipConfig
|
import io.legado.app.help.config.ReadTipConfig
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
import io.legado.app.ui.book.read.ReadBookActivity
|
import io.legado.app.ui.book.read.ReadBookActivity
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_COLOR
|
|
||||||
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
|
import io.legado.app.ui.book.read.config.TipConfigDialog.Companion.TIP_DIVIDER_COLOR
|
||||||
import io.legado.app.utils.getCompatColor
|
import io.legado.app.utils.getCompatColor
|
||||||
import io.legado.app.utils.observeEvent
|
import io.legado.app.utils.observeEvent
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
|
||||||
class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info) {
|
class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info), FontSelectDialog.CallBack {
|
||||||
|
|
||||||
private val binding by viewBinding(DialogReadInfoBinding::bind)
|
private val binding by viewBinding(DialogReadInfoBinding::bind)
|
||||||
private val callBack get() = activity as? ReadBookActivity
|
private val callBack get() = activity as? ReadBookActivity
|
||||||
|
|
||||||
|
override val curFontPath: String
|
||||||
|
get() = ReadBookConfig.headerFont
|
||||||
|
|
||||||
|
override fun selectFont(path: String) {
|
||||||
|
ReadBookConfig.headerFont = path
|
||||||
|
ReadBookConfig.save()
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
initView()
|
initView()
|
||||||
initEvent()
|
initEvent()
|
||||||
observeEvent<String>(EventBus.TIP_COLOR) {
|
observeEvent<String>(EventBus.TIP_COLOR) {
|
||||||
upTvTipColor()
|
upTvHeaderColor()
|
||||||
|
upTvFooterColor()
|
||||||
upTvTipDividerColor()
|
upTvTipDividerColor()
|
||||||
}
|
}
|
||||||
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) { list ->
|
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) { list ->
|
||||||
@@ -59,17 +68,28 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
|
|||||||
callBack?.showPaddingConfig()
|
callBack?.showPaddingConfig()
|
||||||
dismissAllowingStateLoss()
|
dismissAllowingStateLoss()
|
||||||
}
|
}
|
||||||
upTvTipColor()
|
upTvHeaderColor()
|
||||||
|
upTvFooterColor()
|
||||||
upTvTipDividerColor()
|
upTvTipDividerColor()
|
||||||
|
binding.scvHeaderFontSize.progress = ReadBookConfig.headerFontSize
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upTvTipColor() {
|
private fun upTvHeaderColor() {
|
||||||
val tipColor = if (ReadTipConfig.tipColor == 0) {
|
val tipColor = if (ReadTipConfig.tipHeaderColor == 0) {
|
||||||
ReadBookConfig.textColor
|
ReadBookConfig.textColor
|
||||||
} else {
|
} else {
|
||||||
ReadTipConfig.tipColor
|
ReadTipConfig.tipHeaderColor
|
||||||
}
|
}
|
||||||
binding.btnTipColor.color = tipColor
|
binding.btnHeaderColor.color = tipColor
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun upTvFooterColor() {
|
||||||
|
val tipColor = if (ReadTipConfig.tipFooterColor == 0) {
|
||||||
|
ReadBookConfig.textColor
|
||||||
|
} else {
|
||||||
|
ReadTipConfig.tipFooterColor
|
||||||
|
}
|
||||||
|
binding.btnFooterColor.color = tipColor
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upTvTipDividerColor() {
|
private fun upTvTipDividerColor() {
|
||||||
@@ -171,19 +191,36 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
|
|||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
btnTipColor.setOnClickListener {
|
btnHeaderColor.setOnClickListener {
|
||||||
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
||||||
when (i) {
|
when (i) {
|
||||||
0 -> {
|
0 -> {
|
||||||
ReadTipConfig.tipColor = 0
|
ReadTipConfig.tipHeaderColor = 0
|
||||||
upTvTipColor()
|
upTvHeaderColor()
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
}
|
}
|
||||||
|
|
||||||
1 -> ColorPickerDialog.newBuilder()
|
1 -> ColorPickerDialog.newBuilder()
|
||||||
.setShowAlphaSlider(false)
|
.setShowAlphaSlider(false)
|
||||||
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
||||||
.setDialogId(TIP_COLOR)
|
.setDialogId(TipConfigDialog.TIP_HEADER_COLOR)
|
||||||
|
.show(requireActivity())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
btnFooterColor.setOnClickListener {
|
||||||
|
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
||||||
|
when (i) {
|
||||||
|
0 -> {
|
||||||
|
ReadTipConfig.tipFooterColor = 0
|
||||||
|
upTvFooterColor()
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
|
}
|
||||||
|
|
||||||
|
1 -> ColorPickerDialog.newBuilder()
|
||||||
|
.setShowAlphaSlider(false)
|
||||||
|
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
||||||
|
.setDialogId(TipConfigDialog.TIP_FOOTER_COLOR)
|
||||||
.show(requireActivity())
|
.show(requireActivity())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -205,6 +242,14 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
btnHeaderFont.setOnClickListener {
|
||||||
|
FontSelectDialog().show(childFragmentManager, "headerFontSelect")
|
||||||
|
}
|
||||||
|
binding.scvHeaderFontSize.onChanged = {
|
||||||
|
ReadBookConfig.headerFontSize = it
|
||||||
|
ReadBookConfig.save()
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun clearRepeat(repeat: Int) = ReadTipConfig.apply {
|
private fun clearRepeat(repeat: Int) = ReadTipConfig.apply {
|
||||||
|
|||||||
@@ -0,0 +1,204 @@
|
|||||||
|
package io.legado.app.ui.book.read.config
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.LayoutInflater
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import androidx.recyclerview.widget.RecyclerView
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseBottomSheetDialogFragment
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.databinding.DialogRegexColorConfigBinding
|
||||||
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import io.legado.app.help.config.RegexColorRule
|
||||||
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.ui.book.read.ReadBookActivity
|
||||||
|
import io.legado.app.ui.book.read.page.provider.TextChapterLayout
|
||||||
|
import io.legado.app.ui.widget.AccentColorButton
|
||||||
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
import com.jaredrummler.android.colorpicker.ColorPickerDialog
|
||||||
|
|
||||||
|
class RegexColorConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_regex_color_config),
|
||||||
|
FontSelectDialog.CallBack {
|
||||||
|
|
||||||
|
private val binding by viewBinding(DialogRegexColorConfigBinding::bind)
|
||||||
|
private val callBack2 get() = activity as? ReadBookActivity
|
||||||
|
private lateinit var adapter: RegexColorRuleAdapter
|
||||||
|
private var editingRulePosition = -1
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val REGEX_RULE_COLOR = 7900
|
||||||
|
var pendingColorPosition = -1
|
||||||
|
}
|
||||||
|
|
||||||
|
override val curFontPath: String
|
||||||
|
get() = if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
||||||
|
ReadBookConfig.regexColorRules[editingRulePosition].fontPath
|
||||||
|
} else ""
|
||||||
|
|
||||||
|
override fun selectFont(path: String) {
|
||||||
|
if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
||||||
|
ReadBookConfig.regexColorRules[editingRulePosition].fontPath = path
|
||||||
|
notifyConfigChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
adapter = RegexColorRuleAdapter(
|
||||||
|
onDeleteClick = { position -> deleteRule(position) },
|
||||||
|
onColorClick = { position -> showColorPicker(position) },
|
||||||
|
onFontClick = { position -> showFontSelect(position) }
|
||||||
|
)
|
||||||
|
initView()
|
||||||
|
initViewEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initView() = binding.run {
|
||||||
|
recyclerView.layoutManager = LinearLayoutManager(context)
|
||||||
|
recyclerView.adapter = adapter
|
||||||
|
adapter.setItems(ReadBookConfig.regexColorRules)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initViewEvent() = binding.run {
|
||||||
|
btnAddRule.setOnClickListener {
|
||||||
|
showAddRuleDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showAddRuleDialog() {
|
||||||
|
val defaultPatterns = listOf(
|
||||||
|
"\u201C匹配内容\u201D" to "\u201C.+?\u201D",
|
||||||
|
"《匹配内容》" to "《.+?》",
|
||||||
|
"\"匹配内容\"" to "\".+?\""
|
||||||
|
)
|
||||||
|
val displayItems = defaultPatterns.map { it.first } + "自定义规则"
|
||||||
|
context?.alert(title = "添加正则规则") {
|
||||||
|
items(displayItems) { _, i ->
|
||||||
|
if (i < defaultPatterns.size) {
|
||||||
|
val (name, pattern) = defaultPatterns[i]
|
||||||
|
addRule(name, pattern)
|
||||||
|
} else {
|
||||||
|
showCustomRuleDialog()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showCustomRuleDialog() {
|
||||||
|
val editText = android.widget.EditText(context).apply {
|
||||||
|
hint = "输入正则表达式,如:\\u201C.+?\\u201D"
|
||||||
|
}
|
||||||
|
context?.alert(title = "自定义正则规则") {
|
||||||
|
customView { editText }
|
||||||
|
okButton {
|
||||||
|
val pattern = editText.text.toString().trim()
|
||||||
|
if (pattern.isNotEmpty()) {
|
||||||
|
addRule(pattern, pattern)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun addRule(name: String, pattern: String) {
|
||||||
|
val rule = RegexColorRule(name, pattern, ReadBookConfig.durConfig.curTextAccentColor())
|
||||||
|
ReadBookConfig.regexColorRules.add(rule)
|
||||||
|
notifyConfigChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun deleteRule(position: Int) {
|
||||||
|
if (position >= 0 && position < ReadBookConfig.regexColorRules.size) {
|
||||||
|
ReadBookConfig.regexColorRules.removeAt(position)
|
||||||
|
notifyConfigChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showColorPicker(position: Int) {
|
||||||
|
if (position !in ReadBookConfig.regexColorRules.indices) return
|
||||||
|
editingRulePosition = position
|
||||||
|
pendingColorPosition = position
|
||||||
|
val rule = ReadBookConfig.regexColorRules[position]
|
||||||
|
val colorValue = rule.color or 0xFF000000.toInt()
|
||||||
|
ColorPickerDialog.newBuilder()
|
||||||
|
.setColor(colorValue)
|
||||||
|
.setShowAlphaSlider(false)
|
||||||
|
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
||||||
|
.setDialogId(REGEX_RULE_COLOR)
|
||||||
|
.show(requireActivity())
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFontSelect(position: Int) {
|
||||||
|
if (position !in ReadBookConfig.regexColorRules.indices) return
|
||||||
|
editingRulePosition = position
|
||||||
|
FontSelectDialog().apply {
|
||||||
|
explicitCallback = this@RegexColorConfigDialog
|
||||||
|
}.show(childFragmentManager, "regexFontSelect")
|
||||||
|
}
|
||||||
|
|
||||||
|
fun onColorSelected(color: Int) {
|
||||||
|
if (editingRulePosition in ReadBookConfig.regexColorRules.indices) {
|
||||||
|
ReadBookConfig.regexColorRules[editingRulePosition].color = color
|
||||||
|
notifyConfigChanged()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun notifyConfigChanged() {
|
||||||
|
ReadBookConfig.saveRegexColorRules()
|
||||||
|
TextChapterLayout.invalidateRegexCache()
|
||||||
|
adapter.setItems(ReadBookConfig.regexColorRules)
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class RegexColorRuleAdapter(
|
||||||
|
private val onDeleteClick: ((Int) -> Unit)? = null,
|
||||||
|
private val onColorClick: ((Int) -> Unit)? = null,
|
||||||
|
private val onFontClick: ((Int) -> Unit)? = null
|
||||||
|
) : RecyclerView.Adapter<RegexColorRuleAdapter.ViewHolder>() {
|
||||||
|
|
||||||
|
private var items: List<RegexColorRule> = emptyList()
|
||||||
|
|
||||||
|
fun setItems(items: List<RegexColorRule>) {
|
||||||
|
this.items = items
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
|
||||||
|
val view = LayoutInflater.from(parent.context).inflate(R.layout.item_regex_color_rule, parent, false)
|
||||||
|
return ViewHolder(view)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
|
||||||
|
val item = items[position]
|
||||||
|
holder.bind(item, position)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getItemCount(): Int {
|
||||||
|
return items.size
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
|
||||||
|
val tvRuleName = itemView.findViewById<android.widget.TextView>(R.id.tv_rule_name)
|
||||||
|
val tvRulePattern = itemView.findViewById<android.widget.TextView>(R.id.tv_rule_pattern)
|
||||||
|
val btnSelectFont = itemView.findViewById<com.google.android.material.button.MaterialButton>(R.id.btn_select_font)
|
||||||
|
val btnSelectColor = itemView.findViewById<AccentColorButton>(R.id.btn_select_color)
|
||||||
|
val btnDelete = itemView.findViewById<android.widget.ImageButton>(R.id.btn_delete)
|
||||||
|
|
||||||
|
fun bind(item: RegexColorRule, position: Int) {
|
||||||
|
tvRuleName.text = item.name
|
||||||
|
tvRulePattern.text = item.pattern
|
||||||
|
btnSelectColor.color = item.color or 0xFF000000.toInt()
|
||||||
|
btnSelectFont.setOnClickListener {
|
||||||
|
onFontClick?.invoke(position)
|
||||||
|
}
|
||||||
|
btnSelectColor.setOnClickListener {
|
||||||
|
onColorClick?.invoke(position)
|
||||||
|
}
|
||||||
|
btnDelete.setOnClickListener {
|
||||||
|
onDeleteClick?.invoke(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -19,17 +19,27 @@ import io.legado.app.utils.toastOnUi
|
|||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
|
||||||
|
|
||||||
class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config) {
|
class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config), FontSelectDialog.CallBack {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TIP_COLOR = 7897
|
const val TIP_HEADER_COLOR = 7897
|
||||||
|
const val TIP_FOOTER_COLOR = 7899
|
||||||
const val TIP_DIVIDER_COLOR = 7898
|
const val TIP_DIVIDER_COLOR = 7898
|
||||||
|
const val TITLE_COLOR = 7896
|
||||||
const val B_COLOR = 114
|
const val B_COLOR = 114
|
||||||
const val A_COLOR = 514
|
const val A_COLOR = 514
|
||||||
}
|
}
|
||||||
|
|
||||||
private val binding by viewBinding(DialogTipConfigBinding::bind)
|
private val binding by viewBinding(DialogTipConfigBinding::bind)
|
||||||
|
|
||||||
|
override val curFontPath: String
|
||||||
|
get() = ReadBookConfig.titleFont
|
||||||
|
|
||||||
|
override fun selectFont(path: String) {
|
||||||
|
ReadBookConfig.titleFont = path
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 5))
|
||||||
|
}
|
||||||
|
|
||||||
override fun onStart() {
|
override fun onStart() {
|
||||||
super.onStart()
|
super.onStart()
|
||||||
}
|
}
|
||||||
@@ -41,6 +51,10 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
|||||||
binding.abtnBackgroundColor.color = ReadBookConfig.durConfig.curMenuBg()
|
binding.abtnBackgroundColor.color = ReadBookConfig.durConfig.curMenuBg()
|
||||||
binding.abtnAccentColor.color = ReadBookConfig.durConfig.curMenuAc()
|
binding.abtnAccentColor.color = ReadBookConfig.durConfig.curMenuAc()
|
||||||
}
|
}
|
||||||
|
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) {
|
||||||
|
val preview = if (ReadBookConfig.titleColor != 0) ReadBookConfig.titleColor else ReadBookConfig.textColor
|
||||||
|
binding.abtnTitleColor.color = preview or 0xFF000000.toInt()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initView() {
|
private fun initView() {
|
||||||
@@ -53,16 +67,40 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
|||||||
}
|
}
|
||||||
val weightOptions = context?.resources?.getStringArray(R.array.text_font_weight)
|
val weightOptions = context?.resources?.getStringArray(R.array.text_font_weight)
|
||||||
val weightValues = listOf(0, 1, 2)
|
val weightValues = listOf(0, 1, 2)
|
||||||
|
val initialWeightIndex = weightValues.indexOf(ReadBookConfig.titleBold)
|
||||||
|
val weightIconMap = mapOf(
|
||||||
|
0 to R.drawable.ic_text_weight_0,
|
||||||
|
1 to R.drawable.ic_text_weight_1,
|
||||||
|
2 to R.drawable.ic_text_weight_2,
|
||||||
|
)
|
||||||
|
val initialIconRes = weightIconMap[initialWeightIndex] ?: R.drawable.ic_text_weight_2
|
||||||
|
binding.textFontWeightConverter.setIconResource(initialIconRes)
|
||||||
binding.textFontWeightConverter.setOnClickListener {
|
binding.textFontWeightConverter.setOnClickListener {
|
||||||
context?.alert(titleResource = R.string.text_font_weight_converter) {
|
context?.alert(titleResource = R.string.text_font_weight_converter) {
|
||||||
weightOptions?.let { options ->
|
weightOptions?.let { options ->
|
||||||
items(options.toList()) { _, i ->
|
items(options.toList()) { _, i ->
|
||||||
ReadBookConfig.titleBold = weightValues[i]
|
ReadBookConfig.titleBold = weightValues[i]
|
||||||
|
val iconRes = weightIconMap[i] ?: R.drawable.ic_text_weight_2
|
||||||
|
binding.textFontWeightConverter.setIconResource(iconRes)
|
||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(8, 9, 6))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
binding.btnSelectTitleFont.setOnClickListener {
|
||||||
|
FontSelectDialog().show(childFragmentManager, "fontSelect")
|
||||||
|
}
|
||||||
|
val titleColorValue = ReadBookConfig.titleColor
|
||||||
|
val titleColorPreview = if (titleColorValue != 0) titleColorValue else ReadBookConfig.textColor
|
||||||
|
binding.abtnTitleColor.color = titleColorPreview or 0xFF000000.toInt()
|
||||||
|
binding.abtnTitleColor.setOnClickListener {
|
||||||
|
ColorPickerDialog.newBuilder()
|
||||||
|
.setColor(titleColorPreview or 0xFF000000.toInt())
|
||||||
|
.setShowAlphaSlider(false)
|
||||||
|
.setDialogType(ColorPickerDialog.TYPE_CUSTOM)
|
||||||
|
.setDialogId(TITLE_COLOR)
|
||||||
|
.show(requireActivity())
|
||||||
|
}
|
||||||
binding.abtnBackgroundColor.color = ReadBookConfig.durConfig.curMenuBg()
|
binding.abtnBackgroundColor.color = ReadBookConfig.durConfig.curMenuBg()
|
||||||
binding.abtnAccentColor.color = ReadBookConfig.durConfig.curMenuAc()
|
binding.abtnAccentColor.color = ReadBookConfig.durConfig.curMenuAc()
|
||||||
binding.abtnBackgroundColor.setOnClickListener {
|
binding.abtnBackgroundColor.setOnClickListener {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import io.legado.app.databinding.DialogUnderlineConfigBinding
|
|||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.help.config.ReadBookConfig.dottedLine
|
import io.legado.app.help.config.ReadBookConfig.dottedLine
|
||||||
import io.legado.app.help.config.ReadBookConfig.underline
|
import io.legado.app.help.config.ReadBookConfig.underline
|
||||||
|
import io.legado.app.help.config.ReadBookConfig.underlineExtend
|
||||||
import io.legado.app.utils.observeEvent
|
import io.legado.app.utils.observeEvent
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
@@ -39,6 +40,7 @@ class UnderlineConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_unde
|
|||||||
binding.swUnderline.isChecked = underline
|
binding.swUnderline.isChecked = underline
|
||||||
binding.swDottedline.isChecked = dottedLine
|
binding.swDottedline.isChecked = dottedLine
|
||||||
binding.swDottedline.isEnabled = underline
|
binding.swDottedline.isEnabled = underline
|
||||||
|
binding.swUnderlineExtend.isChecked = underlineExtend
|
||||||
|
|
||||||
binding.swUnderline.addOnCheckedChangeListener { _, isChecked ->
|
binding.swUnderline.addOnCheckedChangeListener { _, isChecked ->
|
||||||
underline = isChecked
|
underline = isChecked
|
||||||
@@ -55,6 +57,11 @@ class UnderlineConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_unde
|
|||||||
postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11))
|
postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
binding.swUnderlineExtend.addOnCheckedChangeListener { _, isChecked ->
|
||||||
|
underlineExtend = isChecked
|
||||||
|
postEvent(EventBus.UP_CONFIG, arrayListOf(6, 9, 11))
|
||||||
|
}
|
||||||
|
|
||||||
binding.btnDottedLineBlack.apply {
|
binding.btnDottedLineBlack.apply {
|
||||||
progress = ReadBookConfig.durConfig.dottedBase.toInt()
|
progress = ReadBookConfig.durConfig.dottedBase.toInt()
|
||||||
onChanged = {
|
onChanged = {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
|||||||
import android.widget.FrameLayout
|
import android.widget.FrameLayout
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
import androidx.core.graphics.drawable.toDrawable
|
import androidx.core.graphics.drawable.toDrawable
|
||||||
|
import androidx.core.net.toUri
|
||||||
import androidx.core.view.ViewCompat
|
import androidx.core.view.ViewCompat
|
||||||
import androidx.core.view.WindowInsetsCompat
|
import androidx.core.view.WindowInsetsCompat
|
||||||
import androidx.core.view.isGone
|
import androidx.core.view.isGone
|
||||||
@@ -30,9 +31,11 @@ import io.legado.app.utils.applyNavigationBarPadding
|
|||||||
import io.legado.app.utils.applyStatusBarPadding
|
import io.legado.app.utils.applyStatusBarPadding
|
||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
|
import io.legado.app.utils.isContentScheme
|
||||||
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
||||||
import io.legado.app.utils.statusBarHeight
|
import io.legado.app.utils.statusBarHeight
|
||||||
import splitties.views.backgroundColor
|
import splitties.views.backgroundColor
|
||||||
|
import java.io.File
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,8 +94,11 @@ class PageView(context: Context) : FrameLayout(context) {
|
|||||||
upTipStyle()
|
upTipStyle()
|
||||||
ReadBookConfig.let {
|
ReadBookConfig.let {
|
||||||
val textColor = it.textColor
|
val textColor = it.textColor
|
||||||
val tipColor = with(ReadTipConfig) {
|
val headerColor = with(ReadTipConfig) {
|
||||||
if (tipColor == 0) textColor else tipColor
|
if (tipHeaderColor == 0) textColor else tipHeaderColor
|
||||||
|
}
|
||||||
|
val footerColor = with(ReadTipConfig) {
|
||||||
|
if (tipFooterColor == 0) textColor else tipFooterColor
|
||||||
}
|
}
|
||||||
val tipDividerColor = with(ReadTipConfig) {
|
val tipDividerColor = with(ReadTipConfig) {
|
||||||
when (tipDividerColor) {
|
when (tipDividerColor) {
|
||||||
@@ -101,12 +107,12 @@ class PageView(context: Context) : FrameLayout(context) {
|
|||||||
else -> tipDividerColor
|
else -> tipDividerColor
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tvHeaderLeft.setColor(tipColor)
|
tvHeaderLeft.setColor(headerColor)
|
||||||
tvHeaderMiddle.setColor(tipColor)
|
tvHeaderMiddle.setColor(headerColor)
|
||||||
tvHeaderRight.setColor(tipColor)
|
tvHeaderRight.setColor(headerColor)
|
||||||
tvFooterLeft.setColor(tipColor)
|
tvFooterLeft.setColor(footerColor)
|
||||||
tvFooterMiddle.setColor(tipColor)
|
tvFooterMiddle.setColor(footerColor)
|
||||||
tvFooterRight.setColor(tipColor)
|
tvFooterRight.setColor(footerColor)
|
||||||
vwTopDivider.backgroundColor = tipDividerColor
|
vwTopDivider.backgroundColor = tipDividerColor
|
||||||
vwBottomDivider.backgroundColor = tipDividerColor
|
vwBottomDivider.backgroundColor = tipDividerColor
|
||||||
upStatusBar()
|
upStatusBar()
|
||||||
@@ -131,6 +137,24 @@ class PageView(context: Context) : FrameLayout(context) {
|
|||||||
upBattery(battery)
|
upBattery(battery)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun loadTypeface(fontPath: String): Typeface? {
|
||||||
|
return runCatching {
|
||||||
|
when {
|
||||||
|
fontPath.isContentScheme() -> {
|
||||||
|
context.contentResolver
|
||||||
|
.openFileDescriptor(fontPath.toUri(), "r")
|
||||||
|
?.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fontPath.isNotEmpty() -> {
|
||||||
|
Typeface.Builder(File(fontPath)).build()
|
||||||
|
}
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 显示状态栏时隐藏header
|
* 显示状态栏时隐藏header
|
||||||
*/
|
*/
|
||||||
@@ -208,106 +232,108 @@ class PageView(context: Context) : FrameLayout(context) {
|
|||||||
tvFooterRight.batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
tvFooterRight.batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
val tipTypeface = loadTypeface(ReadBookConfig.headerFont) ?: ChapterProvider.typeface
|
||||||
|
val tipTextSize = ReadBookConfig.headerFontSize.toFloat()
|
||||||
tvTitle = getTipView(ReadTipConfig.chapterTitle)?.apply {
|
tvTitle = getTipView(ReadTipConfig.chapterTitle)?.apply {
|
||||||
tag = ReadTipConfig.chapterTitle
|
tag = ReadTipConfig.chapterTitle
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
}
|
}
|
||||||
tvTitleArrow = getTipView(ReadTipConfig.chapterTitleArrow)?.apply {
|
tvTitleArrow = getTipView(ReadTipConfig.chapterTitleArrow)?.apply {
|
||||||
tag = ReadTipConfig.chapterTitleArrow
|
tag = ReadTipConfig.chapterTitleArrow
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.ARROW
|
batteryMode = BatteryView.BatteryMode.ARROW
|
||||||
}
|
}
|
||||||
tvTitleArrowClassic = getTipView(ReadTipConfig.chapterTitleArrowClassic)?.apply {
|
tvTitleArrowClassic = getTipView(ReadTipConfig.chapterTitleArrowClassic)?.apply {
|
||||||
tag = ReadTipConfig.chapterTitleArrowClassic
|
tag = ReadTipConfig.chapterTitleArrowClassic
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.ARROW
|
batteryMode = BatteryView.BatteryMode.ARROW
|
||||||
}
|
}
|
||||||
tvTime = getTipView(ReadTipConfig.time)?.apply {
|
tvTime = getTipView(ReadTipConfig.time)?.apply {
|
||||||
tag = ReadTipConfig.time
|
tag = ReadTipConfig.time
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
}
|
}
|
||||||
tvBattery = getTipView(ReadTipConfig.battery)?.apply {
|
tvBattery = getTipView(ReadTipConfig.battery)?.apply {
|
||||||
tag = ReadTipConfig.battery
|
tag = ReadTipConfig.battery
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.OUTER
|
batteryMode = BatteryView.BatteryMode.OUTER
|
||||||
}
|
}
|
||||||
tvBatteryClassic = getTipView(ReadTipConfig.batteryClassic)?.apply {
|
tvBatteryClassic = getTipView(ReadTipConfig.batteryClassic)?.apply {
|
||||||
tag = ReadTipConfig.batteryClassic
|
tag = ReadTipConfig.batteryClassic
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.CLASSIC
|
batteryMode = BatteryView.BatteryMode.CLASSIC
|
||||||
}
|
}
|
||||||
tvBatteryInside = getTipView(ReadTipConfig.batteryInside)?.apply {
|
tvBatteryInside = getTipView(ReadTipConfig.batteryInside)?.apply {
|
||||||
tag = ReadTipConfig.batteryInside
|
tag = ReadTipConfig.batteryInside
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.INNER
|
batteryMode = BatteryView.BatteryMode.INNER
|
||||||
}
|
}
|
||||||
tvBatteryIcon = getTipView(ReadTipConfig.batteryIcon)?.apply {
|
tvBatteryIcon = getTipView(ReadTipConfig.batteryIcon)?.apply {
|
||||||
tag = ReadTipConfig.batteryIcon
|
tag = ReadTipConfig.batteryIcon
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.ICON
|
batteryMode = BatteryView.BatteryMode.ICON
|
||||||
}
|
}
|
||||||
tvPage = getTipView(ReadTipConfig.page)?.apply {
|
tvPage = getTipView(ReadTipConfig.page)?.apply {
|
||||||
tag = ReadTipConfig.page
|
tag = ReadTipConfig.page
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
}
|
}
|
||||||
tvTotalProgress = getTipView(ReadTipConfig.totalProgress)?.apply {
|
tvTotalProgress = getTipView(ReadTipConfig.totalProgress)?.apply {
|
||||||
tag = ReadTipConfig.totalProgress
|
tag = ReadTipConfig.totalProgress
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
tvTotalProgress1 = getTipView(ReadTipConfig.totalProgress1)?.apply {
|
tvTotalProgress1 = getTipView(ReadTipConfig.totalProgress1)?.apply {
|
||||||
tag = ReadTipConfig.totalProgress1
|
tag = ReadTipConfig.totalProgress1
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
tvPageAndTotal = getTipView(ReadTipConfig.pageAndTotal)?.apply {
|
tvPageAndTotal = getTipView(ReadTipConfig.pageAndTotal)?.apply {
|
||||||
tag = ReadTipConfig.pageAndTotal
|
tag = ReadTipConfig.pageAndTotal
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
tvBookName = getTipView(ReadTipConfig.bookName)?.apply {
|
tvBookName = getTipView(ReadTipConfig.bookName)?.apply {
|
||||||
tag = ReadTipConfig.bookName
|
tag = ReadTipConfig.bookName
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
tvTimeBattery = getTipView(ReadTipConfig.timeBattery)?.apply {
|
tvTimeBattery = getTipView(ReadTipConfig.timeBattery)?.apply {
|
||||||
tag = ReadTipConfig.timeBattery
|
tag = ReadTipConfig.timeBattery
|
||||||
typeface = Typeface.DEFAULT
|
typeface = Typeface.DEFAULT
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.TIME
|
batteryMode = BatteryView.BatteryMode.TIME
|
||||||
}
|
}
|
||||||
tvTimeBatteryClassic = getTipView(ReadTipConfig.timeBatteryClassic)?.apply {
|
tvTimeBatteryClassic = getTipView(ReadTipConfig.timeBatteryClassic)?.apply {
|
||||||
tag = ReadTipConfig.timeBatteryClassic
|
tag = ReadTipConfig.timeBatteryClassic
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 11f
|
textSize = tipTextSize
|
||||||
batteryMode = BatteryView.BatteryMode.CLASSIC
|
batteryMode = BatteryView.BatteryMode.CLASSIC
|
||||||
}
|
}
|
||||||
tvBatteryP = getTipView(ReadTipConfig.batteryPercentage)?.apply {
|
tvBatteryP = getTipView(ReadTipConfig.batteryPercentage)?.apply {
|
||||||
tag = ReadTipConfig.batteryPercentage
|
tag = ReadTipConfig.batteryPercentage
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
tvTimeBatteryP = getTipView(ReadTipConfig.timeBatteryPercentage)?.apply {
|
tvTimeBatteryP = getTipView(ReadTipConfig.timeBatteryPercentage)?.apply {
|
||||||
tag = ReadTipConfig.timeBatteryPercentage
|
tag = ReadTipConfig.timeBatteryPercentage
|
||||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||||
typeface = ChapterProvider.typeface
|
typeface = tipTypeface
|
||||||
textSize = 12f
|
textSize = tipTextSize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ data class TextLine(
|
|||||||
fun addColumn(column: BaseColumn) {
|
fun addColumn(column: BaseColumn) {
|
||||||
if (column !is TextColumn) {
|
if (column !is TextColumn) {
|
||||||
onlyTextColumn = false
|
onlyTextColumn = false
|
||||||
|
} else if (column.color != null) {
|
||||||
|
onlyTextColumn = false
|
||||||
}
|
}
|
||||||
column.textLine = this
|
column.textLine = this
|
||||||
textColumns.add(column)
|
textColumns.add(column)
|
||||||
@@ -189,6 +191,8 @@ data class TextLine(
|
|||||||
}
|
}
|
||||||
val textColor = if (isReadAloud) {
|
val textColor = if (isReadAloud) {
|
||||||
ReadBookConfig.textAccentColor
|
ReadBookConfig.textAccentColor
|
||||||
|
} else if (isTitle && ReadBookConfig.titleColor != 0) {
|
||||||
|
ReadBookConfig.titleColor
|
||||||
} else {
|
} else {
|
||||||
ReadBookConfig.textColor
|
ReadBookConfig.textColor
|
||||||
}
|
}
|
||||||
@@ -229,7 +233,17 @@ data class TextLine(
|
|||||||
null
|
null
|
||||||
|
|
||||||
val lineY = height + (ReadBookConfig.durConfig.underlinePadding - 10).dpToPx()
|
val lineY = height + (ReadBookConfig.durConfig.underlinePadding - 10).dpToPx()
|
||||||
canvas.drawLine(lineStart + indentWidth, lineY, lineEnd, lineY, paint)
|
val startX = if (ReadBookConfig.underlineExtend) {
|
||||||
|
ChapterProvider.paddingLeft.toFloat()
|
||||||
|
} else {
|
||||||
|
lineStart + indentWidth
|
||||||
|
}
|
||||||
|
val endX = if (ReadBookConfig.underlineExtend) {
|
||||||
|
(ChapterProvider.paddingLeft + ChapterProvider.visibleWidth).toFloat()
|
||||||
|
} else {
|
||||||
|
lineEnd
|
||||||
|
}
|
||||||
|
canvas.drawLine(startX, lineY, endX, lineY, paint)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -368,9 +368,14 @@ data class TextPage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun upRenderHeight() {
|
fun upRenderHeight() {
|
||||||
renderHeight = ceil(lines.last().lineBottom).toInt()
|
val underlineExtraHeight = if (ReadBookConfig.underline) {
|
||||||
|
(ReadBookConfig.durConfig.underlinePadding + ReadBookConfig.underlineHeight).dpToPx().toInt()
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
renderHeight = ceil(lines.last().lineBottom).toInt() + underlineExtraHeight
|
||||||
if (leftLineSize > 0 && leftLineSize != lines.size) {
|
if (leftLineSize > 0 && leftLineSize != lines.size) {
|
||||||
val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt()
|
val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt() + underlineExtraHeight
|
||||||
renderHeight = max(renderHeight, leftHeight)
|
renderHeight = max(renderHeight, leftHeight)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,22 +1,26 @@
|
|||||||
package io.legado.app.ui.book.read.page.entities.column
|
package io.legado.app.ui.book.read.page.entities.column
|
||||||
|
|
||||||
import android.graphics.Canvas
|
import android.graphics.Canvas
|
||||||
|
import android.graphics.Typeface
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import androidx.annotation.Keep
|
import androidx.annotation.Keep
|
||||||
|
import androidx.core.net.toUri
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
import io.legado.app.ui.book.read.page.ContentTextView
|
import io.legado.app.ui.book.read.page.ContentTextView
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine
|
||||||
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
import io.legado.app.ui.book.read.page.entities.TextLine.Companion.emptyTextLine
|
||||||
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
import io.legado.app.ui.book.read.page.provider.ChapterProvider
|
||||||
|
import io.legado.app.utils.isContentScheme
|
||||||
|
import splitties.init.appCtx
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
/**
|
|
||||||
* 文字列
|
|
||||||
*/
|
|
||||||
@Keep
|
@Keep
|
||||||
data class TextColumn(
|
data class TextColumn(
|
||||||
override var start: Float,
|
override var start: Float,
|
||||||
override var end: Float,
|
override var end: Float,
|
||||||
override val charData: String,
|
override val charData: String,
|
||||||
|
var color: Int? = null,
|
||||||
|
var fontPath: String? = null,
|
||||||
) : TextBaseColumn {
|
) : TextBaseColumn {
|
||||||
|
|
||||||
override var textLine: TextLine = emptyTextLine
|
override var textLine: TextLine = emptyTextLine
|
||||||
@@ -47,19 +51,44 @@ data class TextColumn(
|
|||||||
} else {
|
} else {
|
||||||
ChapterProvider.contentPaint
|
ChapterProvider.contentPaint
|
||||||
}
|
}
|
||||||
val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
val textColor = color ?: if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
||||||
ReadBookConfig.textAccentColor
|
ReadBookConfig.textAccentColor
|
||||||
|
} else if (textLine.isTitle && ReadBookConfig.titleColor != 0) {
|
||||||
|
ReadBookConfig.titleColor
|
||||||
} else {
|
} else {
|
||||||
ReadBookConfig.textColor
|
ReadBookConfig.textColor
|
||||||
}
|
}
|
||||||
|
val needRestoreSize = textLine.titleTextSize != null
|
||||||
|
val needRestoreColor = textPaint.color != textColor
|
||||||
|
val customTypeface = fontPath?.let { getTypeface(it) }
|
||||||
|
val needRestoreTypeface = customTypeface != null
|
||||||
|
if (needRestoreSize) {
|
||||||
val originalSize = textPaint.textSize
|
val originalSize = textPaint.textSize
|
||||||
textLine.titleTextSize?.let {
|
textPaint.textSize = textLine.titleTextSize!!
|
||||||
textPaint.textSize = it
|
if (needRestoreColor) textPaint.color = textColor
|
||||||
}
|
if (needRestoreTypeface) textPaint.typeface = customTypeface
|
||||||
if (textPaint.color != textColor) {
|
|
||||||
textPaint.color = textColor
|
|
||||||
}
|
|
||||||
val y = textLine.lineBase - textLine.lineTop
|
val y = textLine.lineBase - textLine.lineTop
|
||||||
|
drawText(canvas, y, textPaint)
|
||||||
|
textPaint.textSize = originalSize
|
||||||
|
} else if (needRestoreColor || needRestoreTypeface) {
|
||||||
|
val originalColor = textPaint.color
|
||||||
|
val originalTypeface = textPaint.typeface
|
||||||
|
if (needRestoreColor) textPaint.color = textColor
|
||||||
|
if (needRestoreTypeface) textPaint.typeface = customTypeface
|
||||||
|
val y = textLine.lineBase - textLine.lineTop
|
||||||
|
drawText(canvas, y, textPaint)
|
||||||
|
if (needRestoreColor) textPaint.color = originalColor
|
||||||
|
if (needRestoreTypeface) textPaint.typeface = originalTypeface
|
||||||
|
} else {
|
||||||
|
val y = textLine.lineBase - textLine.lineTop
|
||||||
|
drawText(canvas, y, textPaint)
|
||||||
|
}
|
||||||
|
if (selected) {
|
||||||
|
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun drawText(canvas: Canvas, y: Float, textPaint: android.text.TextPaint) {
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||||
val letterSpacing = textPaint.letterSpacing * textPaint.textSize
|
val letterSpacing = textPaint.letterSpacing * textPaint.textSize
|
||||||
val letterSpacingHalf = letterSpacing * 0.5f
|
val letterSpacingHalf = letterSpacing * 0.5f
|
||||||
@@ -67,10 +96,32 @@ data class TextColumn(
|
|||||||
} else {
|
} else {
|
||||||
canvas.drawText(charData, start, y, textPaint)
|
canvas.drawText(charData, start, y, textPaint)
|
||||||
}
|
}
|
||||||
if (selected) {
|
|
||||||
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
|
||||||
}
|
}
|
||||||
textPaint.textSize = originalSize
|
|
||||||
|
companion object {
|
||||||
|
private val typefaceCache = mutableMapOf<String, Typeface?>()
|
||||||
|
|
||||||
|
private fun getTypeface(fontPath: String): Typeface? {
|
||||||
|
return typefaceCache.getOrPut(fontPath) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
when {
|
||||||
|
fontPath.isContentScheme() -> {
|
||||||
|
appCtx.contentResolver
|
||||||
|
.openFileDescriptor(fontPath.toUri(), "r")!!
|
||||||
|
.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fontPath.isNotEmpty() -> {
|
||||||
|
Typeface.Builder(File(fontPath)).build()
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -934,30 +934,49 @@ object ChapterProvider {
|
|||||||
|
|
||||||
@SuppressLint("UseKtx")
|
@SuppressLint("UseKtx")
|
||||||
private fun getPaints(typeface: Typeface?): Pair<TextPaint, TextPaint> {
|
private fun getPaints(typeface: Typeface?): Pair<TextPaint, TextPaint> {
|
||||||
|
val titleTypeface = runCatching {
|
||||||
|
val titleFontPath = ReadBookConfig.titleFont
|
||||||
|
when {
|
||||||
|
titleFontPath.isContentScheme() -> {
|
||||||
|
appCtx.contentResolver
|
||||||
|
.openFileDescriptor(titleFontPath.toUri(), "r")!!
|
||||||
|
.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
titleFontPath.isNotEmpty() -> {
|
||||||
|
Typeface.Builder(File(titleFontPath)).build()
|
||||||
|
}
|
||||||
|
else -> typeface
|
||||||
|
}
|
||||||
|
}.getOrNull() ?: typeface
|
||||||
|
|
||||||
val bold = Typeface.create(typeface, Typeface.BOLD)
|
val bold = Typeface.create(typeface, Typeface.BOLD)
|
||||||
val normal = Typeface.create(typeface, Typeface.NORMAL)
|
val normal = Typeface.create(typeface, Typeface.NORMAL)
|
||||||
val titleFont = when (ReadBookConfig.titleBold) {
|
val titleBoldTypeface = Typeface.create(titleTypeface, Typeface.BOLD)
|
||||||
|
val titleNormalTypeface = Typeface.create(titleTypeface, Typeface.NORMAL)
|
||||||
|
val titleFontTypeface = when (ReadBookConfig.titleBold) {
|
||||||
1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
1 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
Typeface.create(typeface, 900, false)
|
Typeface.create(titleTypeface, 900, false)
|
||||||
else
|
else
|
||||||
bold
|
titleBoldTypeface
|
||||||
|
|
||||||
2 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
2 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
Typeface.create(typeface, 300, false)
|
Typeface.create(titleTypeface, 300, false)
|
||||||
else
|
else
|
||||||
normal
|
titleNormalTypeface
|
||||||
|
|
||||||
0 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
0 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
Typeface.create(typeface, 400, false)
|
Typeface.create(titleTypeface, 400, false)
|
||||||
else
|
else
|
||||||
normal
|
titleNormalTypeface
|
||||||
|
|
||||||
in 100..900 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
in 100..900 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||||
Typeface.create(typeface, ReadBookConfig.titleBold, false)
|
Typeface.create(titleTypeface, ReadBookConfig.titleBold, false)
|
||||||
else
|
else
|
||||||
normal
|
titleNormalTypeface
|
||||||
|
|
||||||
else -> normal
|
else -> titleNormalTypeface
|
||||||
}
|
}
|
||||||
|
|
||||||
val textFont = when (ReadBookConfig.textBold) {
|
val textFont = when (ReadBookConfig.textBold) {
|
||||||
@@ -989,7 +1008,7 @@ object ChapterProvider {
|
|||||||
val tPaint = TextPaint()
|
val tPaint = TextPaint()
|
||||||
tPaint.color = ReadBookConfig.textColor
|
tPaint.color = ReadBookConfig.textColor
|
||||||
tPaint.letterSpacing = ReadBookConfig.letterSpacing
|
tPaint.letterSpacing = ReadBookConfig.letterSpacing
|
||||||
tPaint.typeface = titleFont
|
tPaint.typeface = titleFontTypeface
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ReadBookConfig.titleBold in 100..900)
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ReadBookConfig.titleBold in 100..900)
|
||||||
tPaint.setFontVariationSettings("'wght' ${ReadBookConfig.titleBold}")
|
tPaint.setFontVariationSettings("'wght' ${ReadBookConfig.titleBold}")
|
||||||
tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
|
tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ class TextChapterLayout(
|
|||||||
private val bookContent: BookContent,
|
private val bookContent: BookContent,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val regexCache = mutableMapOf<String, Regex>()
|
||||||
|
|
||||||
|
fun invalidateRegexCache() {
|
||||||
|
regexCache.clear()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Volatile
|
@Volatile
|
||||||
private var listener: LayoutProgressListener? = textChapter
|
private var listener: LayoutProgressListener? = textChapter
|
||||||
|
|
||||||
@@ -864,6 +872,7 @@ class TextChapterLayout(
|
|||||||
) {
|
) {
|
||||||
val widthsArray = allocateFloatArray(text.length)
|
val widthsArray = allocateFloatArray(text.length)
|
||||||
textPaint.getTextWidthsCompat(text, widthsArray)
|
textPaint.getTextWidthsCompat(text, widthsArray)
|
||||||
|
val colorMap = applyRegexColorRules(text)
|
||||||
val layout = if (useZhLayout) {
|
val layout = if (useZhLayout) {
|
||||||
val (words, widths) = measureTextSplit(text, widthsArray)
|
val (words, widths) = measureTextSplit(text, widthsArray)
|
||||||
val indentSize = if (isFirstLine) paragraphIndent.length else 0
|
val indentSize = if (isFirstLine) paragraphIndent.length else 0
|
||||||
@@ -915,12 +924,15 @@ class TextChapterLayout(
|
|||||||
val (words, widths) = measureTextSplit(lineText, widthsArray, lineStart)
|
val (words, widths) = measureTextSplit(lineText, widthsArray, lineStart)
|
||||||
val desiredWidth = widths.fastSum()
|
val desiredWidth = widths.fastSum()
|
||||||
textLine.text = lineText
|
textLine.text = lineText
|
||||||
|
val lineWordStyles = if (colorMap != null) {
|
||||||
|
buildWordStyles(words, lineText, colorMap.colorArray, colorMap.fontPathArray, lineStart)
|
||||||
|
} else null
|
||||||
when (lineIndex) {
|
when (lineIndex) {
|
||||||
0 if layout.lineCount > 1 && !isTitle && isFirstLine -> {
|
0 if layout.lineCount > 1 && !isTitle && isFirstLine -> {
|
||||||
//多行的第一行 非标题
|
//多行的第一行 非标题
|
||||||
addCharsToLineFirst(
|
addCharsToLineFirst(
|
||||||
book, absStartX, textLine, words, textPaint,
|
book, absStartX, textLine, words, textPaint,
|
||||||
desiredWidth, widths, srcList, clickList
|
desiredWidth, widths, srcList, clickList, lineWordStyles
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -938,7 +950,7 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, !isTitle && lineIndex == 0, widths, srcList, clickList
|
startX, !isTitle && lineIndex == 0, widths, srcList, clickList, lineWordStyles
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
@@ -951,13 +963,13 @@ class TextChapterLayout(
|
|||||||
val startX = (visibleWidth - desiredWidth) / 2
|
val startX = (visibleWidth - desiredWidth) / 2
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, false, widths, srcList, clickList
|
startX, false, widths, srcList, clickList, lineWordStyles
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
//中间行
|
//中间行
|
||||||
addCharsToLineMiddle(
|
addCharsToLineMiddle(
|
||||||
book, absStartX, textLine, words, textPaint,
|
book, absStartX, textLine, words, textPaint,
|
||||||
desiredWidth, 0f, widths, srcList, clickList
|
desiredWidth, 0f, widths, srcList, clickList, lineWordStyles
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1007,17 +1019,17 @@ class TextChapterLayout(
|
|||||||
textLine: TextLine,
|
textLine: TextLine,
|
||||||
words: List<String>,
|
words: List<String>,
|
||||||
textPaint: TextPaint,
|
textPaint: TextPaint,
|
||||||
/**自然排版长度**/
|
|
||||||
desiredWidth: Float,
|
desiredWidth: Float,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?,
|
srcList: LinkedList<String>?,
|
||||||
clickList: LinkedList<String?>?
|
clickList: LinkedList<String?>?,
|
||||||
|
wordStyles: List<WordStyle>? = null
|
||||||
) {
|
) {
|
||||||
var x = 0f
|
var x = 0f
|
||||||
if (!textFullJustify) {
|
if (!textFullJustify) {
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
x, true, textWidths, srcList, clickList
|
x, true, textWidths, srcList, clickList, wordStyles
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1038,9 +1050,10 @@ class TextChapterLayout(
|
|||||||
if (words.size > bodyIndent.length) {
|
if (words.size > bodyIndent.length) {
|
||||||
val text1 = words.subList(bodyIndent.length, words.size)
|
val text1 = words.subList(bodyIndent.length, words.size)
|
||||||
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
||||||
|
val wordStyles1 = wordStyles?.subList(bodyIndent.length, wordStyles.size)
|
||||||
addCharsToLineMiddle(
|
addCharsToLineMiddle(
|
||||||
book, absStartX, textLine, text1, textPaint,
|
book, absStartX, textLine, text1, textPaint,
|
||||||
desiredWidth, x, textWidths1, srcList, clickList
|
desiredWidth, x, textWidths1, srcList, clickList, wordStyles1
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1054,18 +1067,17 @@ class TextChapterLayout(
|
|||||||
textLine: TextLine,
|
textLine: TextLine,
|
||||||
words: List<String>,
|
words: List<String>,
|
||||||
textPaint: TextPaint,
|
textPaint: TextPaint,
|
||||||
/**自然排版长度**/
|
|
||||||
desiredWidth: Float,
|
desiredWidth: Float,
|
||||||
/**起始x坐标**/
|
|
||||||
startX: Float,
|
startX: Float,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?,
|
srcList: LinkedList<String>?,
|
||||||
clickList: LinkedList<String?>?
|
clickList: LinkedList<String?>?,
|
||||||
|
wordStyles: List<WordStyle>? = null
|
||||||
) {
|
) {
|
||||||
if (!textFullJustify) {
|
if (!textFullJustify) {
|
||||||
addCharsToLineNatural(
|
addCharsToLineNatural(
|
||||||
book, absStartX, textLine, words,
|
book, absStartX, textLine, words,
|
||||||
startX, false, textWidths, srcList, clickList
|
startX, false, textWidths, srcList, clickList, wordStyles
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -1086,7 +1098,9 @@ class TextChapterLayout(
|
|||||||
}
|
}
|
||||||
addCharToLine(
|
addCharToLine(
|
||||||
book, absStartX, textLine, char,
|
book, absStartX, textLine, char,
|
||||||
x, x1, index + 1 == words.size, srcList, clickList
|
x, x1, index + 1 == words.size, srcList, clickList,
|
||||||
|
wordStyles?.getOrNull(index)?.color,
|
||||||
|
wordStyles?.getOrNull(index)?.fontPath
|
||||||
)
|
)
|
||||||
x = x1
|
x = x1
|
||||||
}
|
}
|
||||||
@@ -1102,7 +1116,9 @@ class TextChapterLayout(
|
|||||||
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
||||||
addCharToLine(
|
addCharToLine(
|
||||||
book, absStartX, textLine, char,
|
book, absStartX, textLine, char,
|
||||||
x, x1, index + 1 == words.size, srcList, clickList
|
x, x1, index + 1 == words.size, srcList, clickList,
|
||||||
|
wordStyles?.getOrNull(index)?.color,
|
||||||
|
wordStyles?.getOrNull(index)?.fontPath
|
||||||
)
|
)
|
||||||
x = x1
|
x = x1
|
||||||
}
|
}
|
||||||
@@ -1122,7 +1138,8 @@ class TextChapterLayout(
|
|||||||
hasIndent: Boolean,
|
hasIndent: Boolean,
|
||||||
textWidths: List<Float>,
|
textWidths: List<Float>,
|
||||||
srcList: LinkedList<String>?,
|
srcList: LinkedList<String>?,
|
||||||
clickList: LinkedList<String?>?
|
clickList: LinkedList<String?>?,
|
||||||
|
wordStyles: List<WordStyle>? = null
|
||||||
) {
|
) {
|
||||||
val indentLength = paragraphIndent.length
|
val indentLength = paragraphIndent.length
|
||||||
var x = startX
|
var x = startX
|
||||||
@@ -1140,7 +1157,9 @@ class TextChapterLayout(
|
|||||||
x1,
|
x1,
|
||||||
index + 1 == words.size,
|
index + 1 == words.size,
|
||||||
srcList,
|
srcList,
|
||||||
clickList
|
clickList,
|
||||||
|
wordStyles?.getOrNull(index)?.color,
|
||||||
|
wordStyles?.getOrNull(index)?.fontPath
|
||||||
)
|
)
|
||||||
x = x1
|
x = x1
|
||||||
if (hasIndent && index == indentLength - 1) {
|
if (hasIndent && index == indentLength - 1) {
|
||||||
@@ -1162,7 +1181,9 @@ class TextChapterLayout(
|
|||||||
xEnd: Float,
|
xEnd: Float,
|
||||||
isLineEnd: Boolean,
|
isLineEnd: Boolean,
|
||||||
srcList: LinkedList<String>?,
|
srcList: LinkedList<String>?,
|
||||||
clickList: LinkedList<String?>?
|
clickList: LinkedList<String?>?,
|
||||||
|
color: Int? = null,
|
||||||
|
fontPath: String? = null
|
||||||
) {
|
) {
|
||||||
val column = when {
|
val column = when {
|
||||||
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
||||||
@@ -1176,19 +1197,14 @@ class TextChapterLayout(
|
|||||||
click = click
|
click = click
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
|
||||||
// ReviewColumn(
|
|
||||||
// start = absStartX + xStart,
|
|
||||||
// end = absStartX + xEnd,
|
|
||||||
// count = 10
|
|
||||||
// )
|
|
||||||
// }
|
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
TextColumn(
|
TextColumn(
|
||||||
start = absStartX + xStart,
|
start = absStartX + xStart,
|
||||||
end = absStartX + xEnd,
|
end = absStartX + xEnd,
|
||||||
charData = char
|
charData = char,
|
||||||
|
color = color,
|
||||||
|
fontPath = fontPath
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1289,4 +1305,78 @@ class TextChapterLayout(
|
|||||||
return code == 8203 || code == 8204 || code == 8205 || code == 8288
|
return code == 8203 || code == 8204 || code == 8205 || code == 8288
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private data class RegexMatchResult(
|
||||||
|
val colorArray: IntArray,
|
||||||
|
val fontPathArray: Array<String?>
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun applyRegexColorRules(text: String): RegexMatchResult? {
|
||||||
|
val rules = ReadBookConfig.regexColorRules
|
||||||
|
if (rules.isEmpty()) return null
|
||||||
|
var hasMatch = false
|
||||||
|
for (rule in rules) {
|
||||||
|
try {
|
||||||
|
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) }
|
||||||
|
if (regex.containsMatchIn(text)) {
|
||||||
|
hasMatch = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!hasMatch) return null
|
||||||
|
val colorArray = IntArray(text.length) { -1 }
|
||||||
|
val fontPathArray = arrayOfNulls<String>(text.length)
|
||||||
|
for (rule in rules) {
|
||||||
|
try {
|
||||||
|
val regex = regexCache.getOrPut(rule.pattern) { Regex(rule.pattern) }
|
||||||
|
val matches = regex.findAll(text)
|
||||||
|
for (match in matches) {
|
||||||
|
for (i in match.range) {
|
||||||
|
colorArray[i] = rule.color
|
||||||
|
if (rule.fontPath.isNotEmpty()) {
|
||||||
|
fontPathArray[i] = rule.fontPath
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (_: Exception) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return RegexMatchResult(colorArray, fontPathArray)
|
||||||
|
}
|
||||||
|
|
||||||
|
private data class WordStyle(
|
||||||
|
val color: Int?,
|
||||||
|
val fontPath: String?
|
||||||
|
)
|
||||||
|
|
||||||
|
private fun buildWordStyles(
|
||||||
|
words: List<String>,
|
||||||
|
lineText: String,
|
||||||
|
colorArray: IntArray,
|
||||||
|
fontPathArray: Array<String?>,
|
||||||
|
lineStart: Int
|
||||||
|
): List<WordStyle> {
|
||||||
|
val wordStyles = mutableListOf<WordStyle>()
|
||||||
|
var charOffset = 0
|
||||||
|
for (word in words) {
|
||||||
|
val wordLen = word.length
|
||||||
|
var color: Int? = null
|
||||||
|
var fontPath: String? = null
|
||||||
|
for (j in 0 until wordLen) {
|
||||||
|
val idx = lineStart + charOffset + j
|
||||||
|
if (color == null && colorArray[idx] != -1) {
|
||||||
|
color = colorArray[idx]
|
||||||
|
}
|
||||||
|
if (fontPath == null && fontPathArray[idx] != null) {
|
||||||
|
fontPath = fontPathArray[idx]
|
||||||
|
}
|
||||||
|
if (color != null && fontPath != null) break
|
||||||
|
}
|
||||||
|
wordStyles.add(WordStyle(color, fontPath))
|
||||||
|
charOffset += wordLen
|
||||||
|
}
|
||||||
|
return wordStyles
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -342,6 +342,7 @@ class BookSourceEditActivity :
|
|||||||
contentEntities.clear()
|
contentEntities.clear()
|
||||||
contentEntities.apply {
|
contentEntities.apply {
|
||||||
add(EditEntity("content", cr.content, R.string.rule_book_content))
|
add(EditEntity("content", cr.content, R.string.rule_book_content))
|
||||||
|
add(EditEntity("subContent", cr.subContent, R.string.rule_sub_content))
|
||||||
add(EditEntity("title", cr.title, R.string.rule_chapter_name))
|
add(EditEntity("title", cr.title, R.string.rule_chapter_name))
|
||||||
add(EditEntity("nextContentUrl", cr.nextContentUrl, R.string.rule_next_content))
|
add(EditEntity("nextContentUrl", cr.nextContentUrl, R.string.rule_next_content))
|
||||||
add(EditEntity("webJs", cr.webJs, R.string.rule_web_js))
|
add(EditEntity("webJs", cr.webJs, R.string.rule_web_js))
|
||||||
@@ -530,6 +531,7 @@ class BookSourceEditActivity :
|
|||||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
"content" -> contentRule.content = viewModel.ruleComplete(it.value)
|
"content" -> contentRule.content = viewModel.ruleComplete(it.value)
|
||||||
|
"subContent" -> contentRule.subContent = viewModel.ruleComplete(it.value)
|
||||||
"title" -> contentRule.title = viewModel.ruleComplete(it.value)
|
"title" -> contentRule.title = viewModel.ruleComplete(it.value)
|
||||||
"nextContentUrl" -> contentRule.nextContentUrl =
|
"nextContentUrl" -> contentRule.nextContentUrl =
|
||||||
viewModel.ruleComplete(it.value, type = 2)
|
viewModel.ruleComplete(it.value, type = 2)
|
||||||
|
|||||||
@@ -7,10 +7,12 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
|||||||
import androidx.lifecycle.LifecycleOwner
|
import androidx.lifecycle.LifecycleOwner
|
||||||
import io.legado.app.utils.defaultSharedPreferences
|
import io.legado.app.utils.defaultSharedPreferences
|
||||||
import io.legado.app.utils.getPrefBoolean
|
import io.legado.app.utils.getPrefBoolean
|
||||||
|
import io.legado.app.utils.getPrefFloat
|
||||||
import io.legado.app.utils.getPrefInt
|
import io.legado.app.utils.getPrefInt
|
||||||
import io.legado.app.utils.getPrefLong
|
import io.legado.app.utils.getPrefLong
|
||||||
import io.legado.app.utils.getPrefString
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.putPrefBoolean
|
import io.legado.app.utils.putPrefBoolean
|
||||||
|
import io.legado.app.utils.putPrefFloat
|
||||||
import io.legado.app.utils.putPrefInt
|
import io.legado.app.utils.putPrefInt
|
||||||
import io.legado.app.utils.putPrefLong
|
import io.legado.app.utils.putPrefLong
|
||||||
import io.legado.app.utils.putPrefString
|
import io.legado.app.utils.putPrefString
|
||||||
@@ -63,6 +65,7 @@ fun <T> prefDelegate(
|
|||||||
defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T
|
defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T
|
||||||
defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
|
defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
|
||||||
defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T
|
defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T
|
||||||
|
defaultValue is Float -> appCtx.getPrefFloat(key, defaultValue) as T
|
||||||
else -> defaultValue
|
else -> defaultValue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +81,7 @@ fun <T> prefDelegate(
|
|||||||
is Int -> appCtx.putPrefInt(key, value)
|
is Int -> appCtx.putPrefInt(key, value)
|
||||||
is Boolean -> appCtx.putPrefBoolean(key, value)
|
is Boolean -> appCtx.putPrefBoolean(key, value)
|
||||||
is Long -> appCtx.putPrefLong(key, value)
|
is Long -> appCtx.putPrefLong(key, value)
|
||||||
|
is Float -> appCtx.putPrefFloat(key, value)
|
||||||
}
|
}
|
||||||
_value.value = value
|
_value.value = value
|
||||||
onValueChange?.invoke(value)
|
onValueChange?.invoke(value)
|
||||||
|
|||||||
@@ -53,6 +53,16 @@ object BookshelfConfig {
|
|||||||
*/
|
*/
|
||||||
var showLastUpdateTime by prefDelegate(PreferKey.showLastUpdateTime, false)
|
var showLastUpdateTime by prefDelegate(PreferKey.showLastUpdateTime, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 是否在列表中显示书籍简介
|
||||||
|
*/
|
||||||
|
var showBookIntro by prefDelegate(PreferKey.showBookIntro, false)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列表模式下简介显示行数 (0为显示全部)
|
||||||
|
*/
|
||||||
|
var bookshelfIntroMaxLines by prefDelegate(PreferKey.bookshelfIntroMaxLines, 0)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 是否显示等待更新的书籍数量
|
* 是否显示等待更新的书籍数量
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import androidx.compose.runtime.remember
|
|||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
@@ -86,7 +87,8 @@ fun CoverManageSheet(
|
|||||||
items(coverList) { path ->
|
items(coverList) { path ->
|
||||||
Box {
|
Box {
|
||||||
NormalCard(
|
NormalCard(
|
||||||
cornerRadius = 12.dp
|
cornerRadius = 12.dp,
|
||||||
|
containerColor = Color.Transparent
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = path,
|
model = path,
|
||||||
|
|||||||
@@ -0,0 +1,391 @@
|
|||||||
|
package io.legado.app.ui.config.customTheme
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import com.google.android.material.color.DynamicColors
|
||||||
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.lib.theme.ThemeStore
|
||||||
|
import io.legado.app.lib.theme.primaryColor
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun CustomThemeScreen(
|
||||||
|
onBackClick: () -> Unit,
|
||||||
|
onNavigateToThemePack: () -> Unit
|
||||||
|
) {
|
||||||
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
var showColorPicker by remember { mutableStateOf(false) }
|
||||||
|
var currentColorKey by remember { mutableStateOf("themeColor") }
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
val enableDeepPersonalization = ThemeConfig.enableDeepPersonalization
|
||||||
|
|
||||||
|
val themeColor = ThemeConfig.themeColor
|
||||||
|
val secondaryThemeColor = ThemeConfig.secondaryThemeColor
|
||||||
|
val primaryTextColor = ThemeConfig.primaryTextColor
|
||||||
|
val secondaryTextColor = ThemeConfig.secondaryTextColor
|
||||||
|
val themeBackgroundColor = ThemeConfig.themeBackgroundColor
|
||||||
|
val labelContainerColor = ThemeConfig.labelContainerColor
|
||||||
|
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
|
||||||
|
// 自定义主题 seed color
|
||||||
|
var showSeedColorPicker by remember { mutableStateOf(false) }
|
||||||
|
var pickNightSeedColor by remember { mutableStateOf(false) }
|
||||||
|
val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
|
||||||
|
val nightPrimaryColorValue = remember { mutableIntStateOf(ThemeConfig.cNPrimary) }
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = stringResource(R.string.custom_theme),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
TopBarNavigationButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 120.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// Master switch: ON = deep color overrides, OFF = seed color mode
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "使用色板生成颜色",
|
||||||
|
checked = !enableDeepPersonalization,
|
||||||
|
onCheckedChange = { ThemeConfig.enableDeepPersonalization = !it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Color settings vs Seed color toggle based on enableDeepPersonalization
|
||||||
|
if (enableDeepPersonalization) {
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.color_setting)) {
|
||||||
|
// Primary colors
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "主题色",
|
||||||
|
option = if (themeColor != 0) "#${Integer.toHexString(themeColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "themeColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (themeColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(themeColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "次要主题色",
|
||||||
|
option = if (secondaryThemeColor != 0) "#${Integer.toHexString(secondaryThemeColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "secondaryThemeColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (secondaryThemeColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(secondaryThemeColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "主要字体色",
|
||||||
|
option = if (primaryTextColor != 0) "#${Integer.toHexString(primaryTextColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "primaryTextColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (primaryTextColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(primaryTextColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "次要字体色",
|
||||||
|
option = if (secondaryTextColor != 0) "#${Integer.toHexString(secondaryTextColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "secondaryTextColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (secondaryTextColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(secondaryTextColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "背景色",
|
||||||
|
option = if (themeBackgroundColor != 0) "#${Integer.toHexString(themeBackgroundColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "themeBackgroundColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (themeBackgroundColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(themeBackgroundColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "标签容器色",
|
||||||
|
option = if (labelContainerColor != 0) "#${Integer.toHexString(labelContainerColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentColorKey = "labelContainerColor"
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (labelContainerColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(labelContainerColor))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.seed_color),
|
||||||
|
description = stringResource(R.string.day),
|
||||||
|
option = formatColorOption(primaryColorValue.intValue)
|
||||||
|
?: stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
pickNightSeedColor = false
|
||||||
|
showSeedColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = { ColorSwatch(colorValue = primaryColorValue.intValue) }
|
||||||
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.seed_color),
|
||||||
|
description = stringResource(R.string.night),
|
||||||
|
option = formatColorOption(nightPrimaryColorValue.intValue)
|
||||||
|
?: stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
pickNightSeedColor = true
|
||||||
|
showSeedColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = { ColorSwatch(colorValue = nightPrimaryColorValue.intValue) }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.palette_style),
|
||||||
|
selectedValue = ThemeConfig.paletteStyle,
|
||||||
|
displayEntries = stringArrayResource(R.array.paletteStyle),
|
||||||
|
entryValues = stringArrayResource(R.array.paletteStyle_value),
|
||||||
|
onValueChange = { ThemeConfig.paletteStyle = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.preferred_contrast),
|
||||||
|
selectedValue = ThemeConfig.customContrast,
|
||||||
|
displayEntries = stringArrayResource(R.array.customContrast),
|
||||||
|
entryValues = stringArrayResource(R.array.customContrast_value),
|
||||||
|
onValueChange = { ThemeConfig.customContrast = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = stringResource(R.string.material_version),
|
||||||
|
selectedValue = ThemeConfig.materialVersion,
|
||||||
|
displayEntries = stringArrayResource(R.array.materialVersion),
|
||||||
|
entryValues = stringArrayResource(R.array.materialVersion_value),
|
||||||
|
onValueChange = { ThemeConfig.materialVersion = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Theme Pack entry
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.theme_pack),
|
||||||
|
description = stringResource(R.string.theme_pack_s),
|
||||||
|
onClick = onNavigateToThemePack
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deep personalization color picker
|
||||||
|
ColorPickerSheet(
|
||||||
|
show = showColorPicker,
|
||||||
|
initialColor = when (currentColorKey) {
|
||||||
|
"themeColor" -> themeColor
|
||||||
|
"secondaryThemeColor" -> secondaryThemeColor
|
||||||
|
"primaryTextColor" -> primaryTextColor
|
||||||
|
"secondaryTextColor" -> secondaryTextColor
|
||||||
|
"themeBackgroundColor" -> themeBackgroundColor
|
||||||
|
"labelContainerColor" -> labelContainerColor
|
||||||
|
else -> 0
|
||||||
|
},
|
||||||
|
onDismissRequest = { showColorPicker = false },
|
||||||
|
onColorSelected = {
|
||||||
|
when (currentColorKey) {
|
||||||
|
"themeColor" -> ThemeConfig.themeColor = it
|
||||||
|
"secondaryThemeColor" -> ThemeConfig.secondaryThemeColor = it
|
||||||
|
"primaryTextColor" -> ThemeConfig.primaryTextColor = it
|
||||||
|
"secondaryTextColor" -> ThemeConfig.secondaryTextColor = it
|
||||||
|
"themeBackgroundColor" -> ThemeConfig.themeBackgroundColor = it
|
||||||
|
"labelContainerColor" -> ThemeConfig.labelContainerColor = it
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Seed color picker (from ThemeConfigScreen)
|
||||||
|
ColorPickerSheet(
|
||||||
|
show = showSeedColorPicker,
|
||||||
|
initialColor = if (pickNightSeedColor) {
|
||||||
|
nightPrimaryColorValue.value
|
||||||
|
} else {
|
||||||
|
primaryColorValue.value
|
||||||
|
},
|
||||||
|
onDismissRequest = { showSeedColorPicker = false },
|
||||||
|
onColorSelected = { color ->
|
||||||
|
if (pickNightSeedColor) {
|
||||||
|
nightPrimaryColorValue.intValue = color
|
||||||
|
ThemeConfig.cNPrimary = color
|
||||||
|
} else {
|
||||||
|
primaryColorValue.intValue = color
|
||||||
|
ThemeConfig.cPrimary = color
|
||||||
|
ThemeStore.editTheme(context)
|
||||||
|
.primaryColor(color)
|
||||||
|
.apply()
|
||||||
|
DynamicColors.applyToActivitiesIfAvailable(
|
||||||
|
context.applicationContext as android.app.Application,
|
||||||
|
DynamicColorsOptions.Builder()
|
||||||
|
.setContentBasedSource(context.primaryColor)
|
||||||
|
.build()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun formatColorOption(colorValue: Int): String? {
|
||||||
|
if (colorValue == 0) return null
|
||||||
|
return "#${Integer.toHexString(colorValue).uppercase()}"
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ColorSwatch(colorValue: Int) {
|
||||||
|
if (colorValue == 0) return
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(colorValue))
|
||||||
|
.border(
|
||||||
|
1.dp,
|
||||||
|
MaterialTheme.colorScheme.outlineVariant,
|
||||||
|
CircleShape
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -21,4 +21,8 @@ object MainConfig {
|
|||||||
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
|
var autoRefreshBook by prefDelegate(PreferKey.autoRefresh, false)
|
||||||
var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)
|
var autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, true)
|
||||||
var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
|
var showStatusBar by prefDelegate(PreferKey.showStatusBar, true)
|
||||||
|
var navIconBookshelf by prefDelegate(PreferKey.navIconBookshelf, "")
|
||||||
|
var navIconExplore by prefDelegate(PreferKey.navIconExplore, "")
|
||||||
|
var navIconRss by prefDelegate(PreferKey.navIconRss, "")
|
||||||
|
var navIconMy by prefDelegate(PreferKey.navIconMy, "")
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,185 @@
|
|||||||
|
package io.legado.app.ui.config.personalizationConfig
|
||||||
|
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
|
import androidx.compose.foundation.lazy.grid.items
|
||||||
|
import androidx.compose.material3.Button
|
||||||
|
import androidx.compose.material3.Card
|
||||||
|
import androidx.compose.material3.CardDefaults
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import androidx.core.content.res.ResourcesCompat
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.help.loadFontFiles
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassMediumFlexibleTopAppBar
|
||||||
|
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.utils.FileDoc
|
||||||
|
import io.legado.app.utils.getPrefString
|
||||||
|
import io.legado.app.utils.putPrefString
|
||||||
|
import io.legado.app.utils.takePersistablePermissionSafely
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun FontSelectScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
val context = LocalContext.current
|
||||||
|
var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
|
||||||
|
var selectedFolderUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
// 加载字体文件
|
||||||
|
fun loadFonts() {
|
||||||
|
fontItems = loadFontFiles(context, selectedFolderUri)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从偏好设置中加载保存的字体文件夹路径
|
||||||
|
remember {
|
||||||
|
val savedFontPath = context.getPrefString(PreferKey.fontFolder)
|
||||||
|
if (!savedFontPath.isNullOrEmpty()) {
|
||||||
|
selectedFolderUri = Uri.parse(savedFontPath)
|
||||||
|
}
|
||||||
|
loadFonts()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 文件夹选择启动器
|
||||||
|
val selectFolderLauncher = rememberLauncherForActivityResult(
|
||||||
|
contract = ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
selectedFolderUri = uri
|
||||||
|
// 获取持久化权限
|
||||||
|
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
// 保存选择的文件夹路径到偏好设置
|
||||||
|
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
||||||
|
loadFonts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = stringResource(R.string.font_setting),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
TopBarNavigationButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
if (fontItems.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally,
|
||||||
|
verticalArrangement = Arrangement.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "没有字体文件",
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
modifier = Modifier.padding(bottom = 16.dp)
|
||||||
|
)
|
||||||
|
Button(onClick = { selectFolderLauncher.launch(null) }) {
|
||||||
|
Text(text = stringResource(R.string.select_font))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 120.dp
|
||||||
|
),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(16.dp)
|
||||||
|
) {
|
||||||
|
items(fontItems) {
|
||||||
|
FontItem(it) {
|
||||||
|
// 选择字体的逻辑
|
||||||
|
ThemeConfig.appFontPath = it.uri.toString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun FontItem(fontDoc: FileDoc, onFontSelected: (FileDoc) -> Unit) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||||
|
onClick = {
|
||||||
|
onFontSelected(fontDoc)
|
||||||
|
},
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
// 使用AndroidView显示使用对应字体的文本
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
android.widget.TextView(ctx).apply {
|
||||||
|
text = fontDoc.name
|
||||||
|
textSize = 16f
|
||||||
|
// 设置垂直和水平居中
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
// 设置最大行数为2,超过省略
|
||||||
|
maxLines = 2
|
||||||
|
ellipsize = android.text.TextUtils.TruncateAt.END
|
||||||
|
// 加载并设置字体
|
||||||
|
runCatching {
|
||||||
|
val typeface: Typeface? = if (fontDoc.uri.scheme == "content") {
|
||||||
|
ctx.contentResolver.openFileDescriptor(fontDoc.uri, "r")?.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Typeface.createFromFile(fontDoc.uri.path!!)
|
||||||
|
}
|
||||||
|
this.typeface = typeface
|
||||||
|
}.onFailure {
|
||||||
|
// 字体加载失败,使用默认字体
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@@ -5,6 +5,7 @@ import androidx.activity.result.contract.ActivityResultContracts
|
|||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
@@ -90,7 +91,10 @@ fun BackgroundImageManageSheet(
|
|||||||
.aspectRatio(16f / 9f)
|
.aspectRatio(16f / 9f)
|
||||||
.padding(16.dp)
|
.padding(16.dp)
|
||||||
) {
|
) {
|
||||||
Box(contentAlignment = Alignment.Center) {
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
AppIcon(
|
AppIcon(
|
||||||
imageVector = Icons.Default.Add,
|
imageVector = Icons.Default.Add,
|
||||||
contentDescription = "Add",
|
contentDescription = "Add",
|
||||||
|
|||||||
@@ -0,0 +1,153 @@
|
|||||||
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.AutoAwesome
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Edit
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.toMutableStateList
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.toArgb
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.core.graphics.ColorUtils
|
||||||
|
import io.legado.app.help.config.TagColorGenerator
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.MediumOutlinedIconButton
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
|
import io.legado.app.ui.widget.components.card.TextCard
|
||||||
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun LabelColorManageSheet(
|
||||||
|
show: Boolean,
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
val primaryColor = LegadoTheme.colorScheme.primary
|
||||||
|
val themeColor = ThemeConfig.themeColor
|
||||||
|
val tagColors = remember(show) {
|
||||||
|
ThemeConfig.getCustomTagColors().toMutableStateList()
|
||||||
|
}
|
||||||
|
var showColorPicker by remember { mutableStateOf(false) }
|
||||||
|
var editingIndex by remember { mutableIntStateOf(-1) }
|
||||||
|
var editingTextColor by remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "管理标签颜色",
|
||||||
|
startAction = {
|
||||||
|
MediumOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
val baseColor = if (themeColor != 0) Color(themeColor) else primaryColor
|
||||||
|
val generatedColors = TagColorGenerator.generateTagColors(baseColor)
|
||||||
|
tagColors.clear()
|
||||||
|
tagColors.addAll(generatedColors)
|
||||||
|
ThemeConfig.saveCustomTagColors(tagColors)
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.AutoAwesome
|
||||||
|
)
|
||||||
|
},
|
||||||
|
endAction = {
|
||||||
|
MediumOutlinedIconButton(
|
||||||
|
onClick = {
|
||||||
|
tagColors.add(TagColorPair(0, 0))
|
||||||
|
editingIndex = tagColors.size - 1
|
||||||
|
editingTextColor = 0
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Add
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
items(tagColors.size) { index ->
|
||||||
|
val colorPair = tagColors[index]
|
||||||
|
val label = "标签 ${index + 1}"
|
||||||
|
NormalCard(
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
containerColor = LegadoTheme.colorScheme.onSheetContent
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.SpaceBetween,
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
TextCard(
|
||||||
|
text = label,
|
||||||
|
backgroundColor = if (colorPair.bgColor != 0) Color(colorPair.bgColor) else LegadoTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
contentColor = if (colorPair.textColor != 0) Color(colorPair.textColor) else LegadoTheme.colorScheme.primary,
|
||||||
|
cornerRadius = 4.dp,
|
||||||
|
horizontalPadding = 8.dp,
|
||||||
|
verticalPadding = 4.dp,
|
||||||
|
textStyle = LegadoTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
Row(
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = {
|
||||||
|
editingIndex = index
|
||||||
|
editingTextColor = colorPair.textColor
|
||||||
|
showColorPicker = true
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Edit
|
||||||
|
)
|
||||||
|
SmallIconButton(
|
||||||
|
onClick = {
|
||||||
|
tagColors.removeAt(index)
|
||||||
|
ThemeConfig.saveCustomTagColors(tagColors)
|
||||||
|
},
|
||||||
|
imageVector = Icons.Default.Delete
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showColorPicker && editingIndex in tagColors.indices) {
|
||||||
|
ColorPickerSheet(
|
||||||
|
show = true,
|
||||||
|
initialColor = editingTextColor,
|
||||||
|
onDismissRequest = { showColorPicker = false },
|
||||||
|
onColorSelected = { selectedColor ->
|
||||||
|
val hsl = FloatArray(3)
|
||||||
|
ColorUtils.colorToHSL(selectedColor, hsl)
|
||||||
|
hsl[1] = (hsl[1] * 0.4f).coerceAtMost(0.35f)
|
||||||
|
hsl[2] = 0.90f
|
||||||
|
val bgColor = Color.hsl(hsl[0], hsl[1], hsl[2]).toArgb()
|
||||||
|
tagColors[editingIndex] = TagColorPair(
|
||||||
|
textColor = selectedColor,
|
||||||
|
bgColor = bgColor
|
||||||
|
)
|
||||||
|
ThemeConfig.saveCustomTagColors(tagColors)
|
||||||
|
showColorPicker = false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
package io.legado.app.ui.config.themeConfig
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Add
|
||||||
|
import androidx.compose.material.icons.filled.Close
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.widget.components.button.SmallTonalIconButton
|
||||||
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
|
import io.legado.app.ui.widget.components.icon.AppIcon
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
private data class NavIconDestination(
|
||||||
|
val key: String,
|
||||||
|
val label: String,
|
||||||
|
val path: String,
|
||||||
|
val onSetPath: (String) -> Unit,
|
||||||
|
)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun NavIconManageSheet(
|
||||||
|
show: Boolean,
|
||||||
|
onDismissRequest: () -> Unit
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
var activeDest by remember { mutableStateOf<String?>(null) }
|
||||||
|
|
||||||
|
val destinations = listOf(
|
||||||
|
NavIconDestination("bookshelf", "书架", MainConfig.navIconBookshelf) { MainConfig.navIconBookshelf = it },
|
||||||
|
NavIconDestination("explore", "发现", MainConfig.navIconExplore) { MainConfig.navIconExplore = it },
|
||||||
|
NavIconDestination("rss", "订阅", MainConfig.navIconRss) { MainConfig.navIconRss = it },
|
||||||
|
NavIconDestination("my", "我的", MainConfig.navIconMy) { MainConfig.navIconMy = it },
|
||||||
|
)
|
||||||
|
|
||||||
|
val selectImage = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.GetContent()
|
||||||
|
) { uri ->
|
||||||
|
val dest = activeDest ?: return@rememberLauncherForActivityResult
|
||||||
|
uri?.let {
|
||||||
|
val inputStream = context.contentResolver.openInputStream(it)
|
||||||
|
val iconDir = File(context.filesDir, "nav_icons")
|
||||||
|
iconDir.mkdirs()
|
||||||
|
val destFile = File(iconDir, "$dest.png")
|
||||||
|
inputStream?.use { input ->
|
||||||
|
destFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
destinations.firstOrNull { it.key == dest }?.onSetPath?.invoke(destFile.absolutePath)
|
||||||
|
}
|
||||||
|
activeDest = null
|
||||||
|
}
|
||||||
|
|
||||||
|
AppModalBottomSheet(
|
||||||
|
show = show,
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
title = "导航栏图标",
|
||||||
|
) {
|
||||||
|
Column(modifier = Modifier.fillMaxWidth().padding(bottom = 24.dp)) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||||
|
) {
|
||||||
|
destinations.forEach { dest ->
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f),
|
||||||
|
horizontalAlignment = Alignment.CenterHorizontally
|
||||||
|
) {
|
||||||
|
NormalCard(
|
||||||
|
onClick = {
|
||||||
|
activeDest = dest.key
|
||||||
|
selectImage.launch("image/png")
|
||||||
|
},
|
||||||
|
cornerRadius = 12.dp,
|
||||||
|
containerColor = LegadoTheme.colorScheme.surfaceContainerHigh,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.aspectRatio(1f)
|
||||||
|
) {
|
||||||
|
if (dest.path.isNotEmpty()) {
|
||||||
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
|
AsyncImage(
|
||||||
|
model = dest.path,
|
||||||
|
contentDescription = dest.label,
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentScale = ContentScale.Fit
|
||||||
|
)
|
||||||
|
SmallTonalIconButton(
|
||||||
|
onClick = { dest.onSetPath("") },
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.TopEnd)
|
||||||
|
.padding(4.dp)
|
||||||
|
.size(24.dp),
|
||||||
|
imageVector = Icons.Default.Close
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
AppIcon(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "添加${dest.label}图标",
|
||||||
|
modifier = Modifier.size(32.dp),
|
||||||
|
tint = LegadoTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
AppText(
|
||||||
|
text = dest.label,
|
||||||
|
modifier = Modifier.padding(top = 8.dp),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,7 +3,16 @@ package io.legado.app.ui.config.themeConfig
|
|||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.ui.config.prefDelegate
|
import io.legado.app.ui.config.prefDelegate
|
||||||
|
import io.legado.app.utils.GSON
|
||||||
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.putPrefString
|
||||||
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
data class TagColorPair(
|
||||||
|
val textColor: Int = 0,
|
||||||
|
val bgColor: Int = 0
|
||||||
|
)
|
||||||
|
|
||||||
object ThemeConfig {
|
object ThemeConfig {
|
||||||
|
|
||||||
@@ -17,6 +26,16 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var enableProgressiveBlur by prefDelegate(PreferKey.enableProgressiveBlur, false)
|
var enableProgressiveBlur by prefDelegate(PreferKey.enableProgressiveBlur, false)
|
||||||
|
|
||||||
|
var topBarBlurRadius by prefDelegate(PreferKey.topBarBlurRadius, 24)
|
||||||
|
|
||||||
|
var bottomBarBlurRadius by prefDelegate(PreferKey.bottomBarBlurRadius, 8)
|
||||||
|
|
||||||
|
var topBarBlurAlpha by prefDelegate(PreferKey.topBarBlurAlpha, 73)
|
||||||
|
|
||||||
|
var bottomBarBlurAlpha by prefDelegate(PreferKey.bottomBarBlurAlpha, 40)
|
||||||
|
|
||||||
|
var bottomBarLensRadius by prefDelegate(PreferKey.bottomBarLensRadius, 24f)
|
||||||
|
|
||||||
var useFlexibleTopAppBar by prefDelegate(PreferKey.useFlexibleTopAppBar, true)
|
var useFlexibleTopAppBar by prefDelegate(PreferKey.useFlexibleTopAppBar, true)
|
||||||
|
|
||||||
var paletteStyle by prefDelegate(PreferKey.paletteStyle, "tonalSpot")
|
var paletteStyle by prefDelegate(PreferKey.paletteStyle, "tonalSpot")
|
||||||
@@ -56,10 +75,48 @@ object ThemeConfig {
|
|||||||
postEvent(EventBus.RECREATE, "")
|
postEvent(EventBus.RECREATE, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
var cPrimary by prefDelegate(PreferKey.cPrimary, 0) {
|
var appFontPath: String?
|
||||||
|
get() = appCtx.getPrefString(PreferKey.appFontPath)
|
||||||
|
set(value) {
|
||||||
|
appCtx.putPrefString(PreferKey.appFontPath, value)
|
||||||
postEvent(EventBus.RECREATE, "")
|
postEvent(EventBus.RECREATE, "")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var cPrimary by prefDelegate(PreferKey.cPrimary, 0)
|
||||||
|
|
||||||
|
var enableDeepPersonalization by prefDelegate(PreferKey.enableDeepPersonalization, false)
|
||||||
|
|
||||||
|
var themeColor by prefDelegate(PreferKey.themeColor, 0)
|
||||||
|
|
||||||
|
var secondaryThemeColor by prefDelegate(PreferKey.secondaryThemeColor, 0)
|
||||||
|
|
||||||
|
var primaryTextColor by prefDelegate(PreferKey.primaryTextColor, 0)
|
||||||
|
|
||||||
|
var secondaryTextColor by prefDelegate(PreferKey.secondaryTextColor, 0)
|
||||||
|
|
||||||
|
var themeBackgroundColor by prefDelegate(PreferKey.themeBackgroundColor, 0)
|
||||||
|
|
||||||
|
var labelContainerColor by prefDelegate(PreferKey.labelContainerColor, 0)
|
||||||
|
|
||||||
|
var enableContainerBorder by prefDelegate(PreferKey.enableContainerBorder, false)
|
||||||
|
|
||||||
|
var containerBorderWidth by prefDelegate(PreferKey.containerBorderWidth, 1f)
|
||||||
|
|
||||||
|
var containerBorderStyle by prefDelegate(PreferKey.containerBorderStyle, "solid")
|
||||||
|
|
||||||
|
var containerBorderColor by prefDelegate(PreferKey.containerBorderColor, 0)
|
||||||
|
|
||||||
|
var containerBorderDashWidth by prefDelegate(PreferKey.containerBorderDashWidth, 4f)
|
||||||
|
|
||||||
|
var enableItemDivider by prefDelegate(PreferKey.enableItemDivider, false)
|
||||||
|
|
||||||
|
var itemDividerWidth by prefDelegate(PreferKey.itemDividerWidth, 1f)
|
||||||
|
|
||||||
|
var itemDividerLength by prefDelegate(PreferKey.itemDividerLength, 80f)
|
||||||
|
var itemDividerColor by prefDelegate(PreferKey.itemDividerColor, 0)
|
||||||
|
|
||||||
|
var bookInfoInputColor by prefDelegate(PreferKey.bookInfoInputColor, 0)
|
||||||
|
|
||||||
var cNPrimary by prefDelegate(PreferKey.cNPrimary, 0) {
|
var cNPrimary by prefDelegate(PreferKey.cNPrimary, 0) {
|
||||||
postEvent(EventBus.RECREATE, "")
|
postEvent(EventBus.RECREATE, "")
|
||||||
}
|
}
|
||||||
@@ -70,6 +127,28 @@ object ThemeConfig {
|
|||||||
|
|
||||||
var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
|
var launcherIcon by prefDelegate(PreferKey.launcherIcon, "ic_launcher")
|
||||||
|
|
||||||
|
var enableCustomTagColors by prefDelegate(PreferKey.enableCustomTagColors, false)
|
||||||
|
|
||||||
|
var customTagColorsJson: String?
|
||||||
|
get() = appCtx.getPrefString(PreferKey.customTagColors)
|
||||||
|
set(value) {
|
||||||
|
appCtx.putPrefString(PreferKey.customTagColors, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getCustomTagColors(): List<TagColorPair> {
|
||||||
|
return try {
|
||||||
|
val json = customTagColorsJson
|
||||||
|
if (json.isNullOrBlank()) emptyList()
|
||||||
|
else GSON.fromJson(json, Array<TagColorPair>::class.java).toList()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
emptyList()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun saveCustomTagColors(colors: List<TagColorPair>) {
|
||||||
|
customTagColorsJson = GSON.toJson(colors)
|
||||||
|
}
|
||||||
|
|
||||||
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
var showDiscovery by prefDelegate(PreferKey.showDiscovery, true)
|
||||||
|
|
||||||
var showRss by prefDelegate(PreferKey.showRss, true)
|
var showRss by prefDelegate(PreferKey.showRss, true)
|
||||||
|
|||||||
@@ -2,8 +2,13 @@ package io.legado.app.ui.config.themeConfig
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
import android.os.Handler
|
import android.os.Handler
|
||||||
import android.os.Looper
|
import android.os.Looper
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
import androidx.compose.animation.AnimatedVisibility
|
import androidx.compose.animation.AnimatedVisibility
|
||||||
import androidx.compose.animation.core.animateDpAsState
|
import androidx.compose.animation.core.animateDpAsState
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
@@ -26,18 +31,23 @@ import androidx.compose.foundation.layout.size
|
|||||||
import androidx.compose.foundation.layout.width
|
import androidx.compose.foundation.layout.width
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
import androidx.compose.foundation.lazy.LazyRow
|
import androidx.compose.foundation.lazy.LazyRow
|
||||||
|
import androidx.compose.foundation.lazy.grid.GridCells
|
||||||
|
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.foundation.shape.CircleShape
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
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.BrightnessMedium
|
import androidx.compose.material.icons.filled.BrightnessMedium
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
import androidx.compose.material.icons.filled.DarkMode
|
import androidx.compose.material.icons.filled.DarkMode
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.LightMode
|
import androidx.compose.material.icons.filled.LightMode
|
||||||
import androidx.compose.material3.ButtonGroupDefaults
|
import androidx.compose.material3.ButtonGroupDefaults
|
||||||
import androidx.compose.material3.Card
|
import androidx.compose.material3.Card
|
||||||
import androidx.compose.material3.CardDefaults
|
import androidx.compose.material3.CardDefaults
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -68,8 +78,11 @@ import com.google.android.material.color.DynamicColors
|
|||||||
import com.google.android.material.color.DynamicColorsOptions
|
import com.google.android.material.color.DynamicColorsOptions
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.AppContextWrapper
|
import io.legado.app.base.AppContextWrapper
|
||||||
|
import io.legado.app.constant.PreferKey
|
||||||
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.help.LauncherIconHelp
|
import io.legado.app.help.LauncherIconHelp
|
||||||
|
import io.legado.app.help.loadFontFiles
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
import io.legado.app.help.config.OldThemeConfig
|
||||||
import io.legado.app.lib.theme.ThemeStore
|
import io.legado.app.lib.theme.ThemeStore
|
||||||
@@ -81,8 +94,10 @@ import io.legado.app.ui.theme.adaptiveContentPadding
|
|||||||
import io.legado.app.ui.widget.components.AppScaffold
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
import io.legado.app.ui.widget.components.AppTextField
|
import io.legado.app.ui.widget.components.AppTextField
|
||||||
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
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.button.SmallIconButton
|
||||||
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
import io.legado.app.ui.widget.components.dialog.ColorPickerSheet
|
||||||
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
import io.legado.app.ui.widget.components.settingItem.DropdownListSettingItem
|
||||||
@@ -91,8 +106,12 @@ import io.legado.app.ui.widget.components.settingItem.SwitchSettingItem
|
|||||||
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
|
||||||
|
import io.legado.app.utils.FileDoc
|
||||||
|
import io.legado.app.utils.getPrefString
|
||||||
import io.legado.app.utils.postEvent
|
import io.legado.app.utils.postEvent
|
||||||
|
import io.legado.app.utils.putPrefString
|
||||||
import io.legado.app.utils.restart
|
import io.legado.app.utils.restart
|
||||||
|
import io.legado.app.utils.takePersistablePermissionSafely
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
import org.koin.androidx.compose.koinViewModel
|
import org.koin.androidx.compose.koinViewModel
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
@@ -104,6 +123,7 @@ import top.yukonga.miuix.kmp.basic.Text as MiuixText
|
|||||||
@Composable
|
@Composable
|
||||||
fun ThemeConfigScreen(
|
fun ThemeConfigScreen(
|
||||||
onBackClick: () -> Unit,
|
onBackClick: () -> Unit,
|
||||||
|
onNavigateToCustomTheme: () -> Unit,
|
||||||
viewModel: ThemeConfigViewModel = koinViewModel()
|
viewModel: ThemeConfigViewModel = koinViewModel()
|
||||||
) {
|
) {
|
||||||
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
@@ -114,16 +134,35 @@ fun ThemeConfigScreen(
|
|||||||
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
|
var selectedTheme by remember { mutableStateOf(ThemeConfig.appTheme) }
|
||||||
var useMiuixMonet by remember { mutableStateOf(ThemeConfig.useMiuixMonet) }
|
var useMiuixMonet by remember { mutableStateOf(ThemeConfig.useMiuixMonet) }
|
||||||
var showRestartDialog by remember { mutableStateOf(false) }
|
var showRestartDialog by remember { mutableStateOf(false) }
|
||||||
var showColorPicker by remember { mutableStateOf(false) }
|
|
||||||
var pickNightSeedColor by remember { mutableStateOf(false) }
|
|
||||||
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
var showLauncherIconPicker by remember { mutableStateOf(false) }
|
||||||
var showThemeListDialog by remember { mutableStateOf(false) }
|
var showBorderColorPicker by remember { mutableStateOf(false) }
|
||||||
var saveThemeKey by remember { mutableStateOf<String?>(null) }
|
var currentBorderColorKey by remember { mutableStateOf("containerBorderColor") }
|
||||||
var themeName by remember { mutableStateOf("") }
|
var showNavIconSheet by remember { mutableStateOf(false) }
|
||||||
|
var showFontSheet by remember { mutableStateOf(false) }
|
||||||
|
var fontItems by remember { mutableStateOf<List<FileDoc>>(emptyList()) }
|
||||||
|
var fontFolderUri by remember { mutableStateOf<Uri?>(null) }
|
||||||
|
|
||||||
|
fun loadFonts() {
|
||||||
|
fontItems = loadFontFiles(context, fontFolderUri)
|
||||||
|
}
|
||||||
|
remember {
|
||||||
|
val saved = context.getPrefString(PreferKey.fontFolder)
|
||||||
|
if (!saved.isNullOrEmpty()) fontFolderUri = Uri.parse(saved)
|
||||||
|
loadFonts()
|
||||||
|
}
|
||||||
|
|
||||||
|
val fontFolderLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocumentTree()
|
||||||
|
) { uri ->
|
||||||
|
if (uri != null) {
|
||||||
|
fontFolderUri = uri
|
||||||
|
uri.takePersistablePermissionSafely(context, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||||
|
context.putPrefString(PreferKey.fontFolder, uri.toString())
|
||||||
|
loadFonts()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
|
val fontScaleValue = remember { mutableFloatStateOf(ThemeConfig.fontScale.toFloat()) }
|
||||||
val primaryColorValue = remember { mutableIntStateOf(ThemeConfig.cPrimary) }
|
|
||||||
val nightPrimaryColorValue = remember { mutableIntStateOf(ThemeConfig.cNPrimary) }
|
|
||||||
|
|
||||||
AppScaffold(
|
AppScaffold(
|
||||||
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
@@ -166,8 +205,8 @@ fun ThemeConfigScreen(
|
|||||||
isDark = isDarkTheme,
|
isDark = isDarkTheme,
|
||||||
isAmoled = ThemeConfig.isPureBlack,
|
isAmoled = ThemeConfig.isPureBlack,
|
||||||
paletteStyle = ThemeConfig.paletteStyle,
|
paletteStyle = ThemeConfig.paletteStyle,
|
||||||
customLightSeedColor = primaryColorValue.intValue,
|
customLightSeedColor = ThemeConfig.cPrimary,
|
||||||
customNightSeedColor = nightPrimaryColorValue.intValue
|
customNightSeedColor = ThemeConfig.cNPrimary
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -259,8 +298,8 @@ fun ThemeConfigScreen(
|
|||||||
isDark = isDarkTheme,
|
isDark = isDarkTheme,
|
||||||
isAmoled = ThemeConfig.isPureBlack,
|
isAmoled = ThemeConfig.isPureBlack,
|
||||||
paletteStyle = ThemeConfig.paletteStyle,
|
paletteStyle = ThemeConfig.paletteStyle,
|
||||||
customLightSeedColor = primaryColorValue.intValue,
|
customLightSeedColor = ThemeConfig.cPrimary,
|
||||||
customNightSeedColor = nightPrimaryColorValue.intValue,
|
customNightSeedColor = ThemeConfig.cNPrimary,
|
||||||
onThemeSelected = { theme ->
|
onThemeSelected = { theme ->
|
||||||
if (theme == "13") {
|
if (theme == "13") {
|
||||||
val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
|
val hasLightBg = !ThemeConfig.bgImageLight.isNullOrEmpty()
|
||||||
@@ -287,6 +326,16 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup {
|
SplicedColumnGroup {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.font_setting),
|
||||||
|
onClick = { showFontSheet = true }
|
||||||
|
)
|
||||||
|
if (selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)) {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = stringResource(R.string.custom_theme),
|
||||||
|
onClick = onNavigateToCustomTheme
|
||||||
|
)
|
||||||
|
}
|
||||||
DropdownListSettingItem(
|
DropdownListSettingItem(
|
||||||
title = stringResource(R.string.compose_engine),
|
title = stringResource(R.string.compose_engine),
|
||||||
selectedValue = ThemeConfig.composeEngine,
|
selectedValue = ThemeConfig.composeEngine,
|
||||||
@@ -327,55 +376,6 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
val showCustomThemeOptions = selectedTheme == "12" && (!isMiuixEngine || useMiuixMonet)
|
|
||||||
if (showCustomThemeOptions) {
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.custom_theme)) {
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = stringResource(R.string.seed_color),
|
|
||||||
description = stringResource(R.string.day),
|
|
||||||
option = formatColorOption(primaryColorValue.intValue)
|
|
||||||
?: stringResource(R.string.click_to_select),
|
|
||||||
onClick = {
|
|
||||||
pickNightSeedColor = false
|
|
||||||
showColorPicker = true
|
|
||||||
},
|
|
||||||
trailingContent = { ColorSwatch(colorValue = primaryColorValue.intValue) }
|
|
||||||
)
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = stringResource(R.string.seed_color),
|
|
||||||
description = stringResource(R.string.night),
|
|
||||||
option = formatColorOption(nightPrimaryColorValue.intValue)
|
|
||||||
?: stringResource(R.string.click_to_select),
|
|
||||||
onClick = {
|
|
||||||
pickNightSeedColor = true
|
|
||||||
showColorPicker = true
|
|
||||||
},
|
|
||||||
trailingContent = { ColorSwatch(colorValue = nightPrimaryColorValue.intValue) }
|
|
||||||
)
|
|
||||||
DropdownListSettingItem(
|
|
||||||
title = stringResource(R.string.palette_style),
|
|
||||||
selectedValue = ThemeConfig.paletteStyle,
|
|
||||||
displayEntries = stringArrayResource(R.array.paletteStyle),
|
|
||||||
entryValues = stringArrayResource(R.array.paletteStyle_value),
|
|
||||||
onValueChange = { ThemeConfig.paletteStyle = it }
|
|
||||||
)
|
|
||||||
DropdownListSettingItem(
|
|
||||||
title = stringResource(R.string.preferred_contrast),
|
|
||||||
selectedValue = ThemeConfig.customContrast,
|
|
||||||
displayEntries = stringArrayResource(R.array.customContrast),
|
|
||||||
entryValues = stringArrayResource(R.array.customContrast_value),
|
|
||||||
onValueChange = { ThemeConfig.customContrast = it }
|
|
||||||
)
|
|
||||||
DropdownListSettingItem(
|
|
||||||
title = stringResource(R.string.material_version),
|
|
||||||
selectedValue = ThemeConfig.materialVersion,
|
|
||||||
displayEntries = stringArrayResource(R.array.materialVersion),
|
|
||||||
entryValues = stringArrayResource(R.array.materialVersion_value),
|
|
||||||
onValueChange = { ThemeConfig.materialVersion = it }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
SplicedColumnGroup(title = stringResource(R.string.main_activity)) {
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
title = stringResource(R.string.show_discovery),
|
title = stringResource(R.string.show_discovery),
|
||||||
@@ -417,6 +417,14 @@ fun ThemeConfigScreen(
|
|||||||
checked = ThemeConfig.useFloatingBottomBarLiquidGlass,
|
checked = ThemeConfig.useFloatingBottomBarLiquidGlass,
|
||||||
onCheckedChange = { ThemeConfig.useFloatingBottomBarLiquidGlass = it }
|
onCheckedChange = { ThemeConfig.useFloatingBottomBarLiquidGlass = it }
|
||||||
)
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "底栏模糊强度",
|
||||||
|
description = "控制液态玻璃的扭曲程度",
|
||||||
|
value = ThemeConfig.bottomBarLensRadius,
|
||||||
|
defaultValue = 24f,
|
||||||
|
valueRange = 0f..50f,
|
||||||
|
onValueChange = { ThemeConfig.bottomBarLensRadius = it }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
DropdownListSettingItem(
|
DropdownListSettingItem(
|
||||||
title = stringResource(R.string.tabletInterface),
|
title = stringResource(R.string.tabletInterface),
|
||||||
@@ -441,22 +449,6 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.share)) {
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = stringResource(R.string.theme_list),
|
|
||||||
description = stringResource(R.string.theme_list_summary),
|
|
||||||
onClick = { showThemeListDialog = true }
|
|
||||||
)
|
|
||||||
ClickableSettingItem(
|
|
||||||
title = stringResource(R.string.save_theme_config),
|
|
||||||
description = stringResource(R.string.save_day_theme_summary),
|
|
||||||
onClick = {
|
|
||||||
saveThemeKey = "saveDayTheme"
|
|
||||||
themeName = ""
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
|
SplicedColumnGroup(title = stringResource(R.string.compose_related)) {
|
||||||
if (!isMiuixEngine) {
|
if (!isMiuixEngine) {
|
||||||
SwitchSettingItem(
|
SwitchSettingItem(
|
||||||
@@ -485,6 +477,38 @@ fun ThemeConfigScreen(
|
|||||||
onCheckedChange = { ThemeConfig.enableProgressiveBlur = it }
|
onCheckedChange = { ThemeConfig.enableProgressiveBlur = it }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
if (ThemeConfig.enableBlur) {
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "顶栏模糊半径",
|
||||||
|
description = "模糊半径越大,系统运行越卡顿",
|
||||||
|
value = ThemeConfig.topBarBlurRadius.toFloat(),
|
||||||
|
defaultValue = 24f,
|
||||||
|
valueRange = 0f..30f,
|
||||||
|
onValueChange = { ThemeConfig.topBarBlurRadius = it.toInt() }
|
||||||
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "底栏模糊半径",
|
||||||
|
description = "模糊半径越大,系统运行越卡顿",
|
||||||
|
value = ThemeConfig.bottomBarBlurRadius.toFloat(),
|
||||||
|
defaultValue = 8f,
|
||||||
|
valueRange = 0f..10f,
|
||||||
|
onValueChange = { ThemeConfig.bottomBarBlurRadius = it.toInt() }
|
||||||
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "顶栏模糊透明度",
|
||||||
|
value = ThemeConfig.topBarBlurAlpha.toFloat(),
|
||||||
|
defaultValue = 73f,
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { ThemeConfig.topBarBlurAlpha = it.toInt() }
|
||||||
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "底栏模糊透明度",
|
||||||
|
value = ThemeConfig.bottomBarBlurAlpha.toFloat(),
|
||||||
|
defaultValue = 40f,
|
||||||
|
valueRange = 0f..100f,
|
||||||
|
onValueChange = { ThemeConfig.bottomBarBlurAlpha = it.toInt() }
|
||||||
|
)
|
||||||
|
}
|
||||||
AnimatedVisibility(visible = !isMiuixEngine && !ThemeConfig.enableBlur) {
|
AnimatedVisibility(visible = !isMiuixEngine && !ThemeConfig.enableBlur) {
|
||||||
Column(
|
Column(
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||||
@@ -579,6 +603,126 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Border settings
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup(title = "容器设置") {
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "显示容器边框",
|
||||||
|
checked = ThemeConfig.enableContainerBorder,
|
||||||
|
onCheckedChange = { ThemeConfig.enableContainerBorder = it }
|
||||||
|
)
|
||||||
|
if (ThemeConfig.enableContainerBorder) {
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "边框粗细",
|
||||||
|
description = "${ThemeConfig.containerBorderWidth}dp",
|
||||||
|
value = ThemeConfig.containerBorderWidth,
|
||||||
|
defaultValue = 1f,
|
||||||
|
valueRange = 0f..5f,
|
||||||
|
steps = 49,
|
||||||
|
onValueChange = { ThemeConfig.containerBorderWidth = it }
|
||||||
|
)
|
||||||
|
DropdownListSettingItem(
|
||||||
|
title = "边框样式",
|
||||||
|
selectedValue = ThemeConfig.containerBorderStyle,
|
||||||
|
displayEntries = arrayOf("实线", "虚线"),
|
||||||
|
entryValues = arrayOf("solid", "dashed"),
|
||||||
|
onValueChange = { ThemeConfig.containerBorderStyle = it }
|
||||||
|
)
|
||||||
|
if (ThemeConfig.containerBorderStyle == "dashed") {
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "虚线间隔",
|
||||||
|
description = "${ThemeConfig.containerBorderDashWidth}dp",
|
||||||
|
value = ThemeConfig.containerBorderDashWidth,
|
||||||
|
defaultValue = 4f,
|
||||||
|
valueRange = 1f..10f,
|
||||||
|
steps = 89,
|
||||||
|
onValueChange = { ThemeConfig.containerBorderDashWidth = it }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "边框颜色",
|
||||||
|
option = if (ThemeConfig.containerBorderColor != 0) "#${Integer.toHexString(ThemeConfig.containerBorderColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentBorderColorKey = "containerBorderColor"
|
||||||
|
showBorderColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(ThemeConfig.containerBorderColor))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
SwitchSettingItem(
|
||||||
|
title = "显示分割线",
|
||||||
|
checked = ThemeConfig.enableItemDivider,
|
||||||
|
onCheckedChange = { ThemeConfig.enableItemDivider = it }
|
||||||
|
)
|
||||||
|
if (ThemeConfig.enableItemDivider) {
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "分割线粗细",
|
||||||
|
description = "${ThemeConfig.itemDividerWidth}dp",
|
||||||
|
value = ThemeConfig.itemDividerWidth,
|
||||||
|
defaultValue = 1f,
|
||||||
|
valueRange = 0f..5f,
|
||||||
|
steps = 49,
|
||||||
|
onValueChange = { ThemeConfig.itemDividerWidth = it }
|
||||||
|
)
|
||||||
|
SliderSettingItem(
|
||||||
|
title = "分割线长度",
|
||||||
|
description = "${ThemeConfig.itemDividerLength.toInt()}%",
|
||||||
|
value = ThemeConfig.itemDividerLength,
|
||||||
|
defaultValue = 80f,
|
||||||
|
valueRange = 30f..100f,
|
||||||
|
steps = 14,
|
||||||
|
onValueChange = { ThemeConfig.itemDividerLength = it }
|
||||||
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "分割线颜色",
|
||||||
|
option = if (ThemeConfig.itemDividerColor != 0) "#${Integer.toHexString(ThemeConfig.itemDividerColor).uppercase()}" else stringResource(R.string.click_to_select),
|
||||||
|
onClick = {
|
||||||
|
currentBorderColorKey = "itemDividerColor"
|
||||||
|
showBorderColorPicker = true
|
||||||
|
},
|
||||||
|
trailingContent = {
|
||||||
|
if (ThemeConfig.itemDividerColor != 0) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.size(28.dp)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(Color(ThemeConfig.itemDividerColor))
|
||||||
|
.border(1.dp, MaterialTheme.colorScheme.outlineVariant, CircleShape)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Nav icon settings
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup(title = "导航栏图标设置") {
|
||||||
|
val customCount = listOf(
|
||||||
|
MainConfig.navIconBookshelf,
|
||||||
|
MainConfig.navIconExplore,
|
||||||
|
MainConfig.navIconRss,
|
||||||
|
MainConfig.navIconMy
|
||||||
|
).count { it.isNotEmpty() }
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "导航栏图标",
|
||||||
|
description = if (customCount > 0) "已设置 $customCount 个自定义图标" else "使用默认图标",
|
||||||
|
onClick = { showNavIconSheet = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,34 +754,13 @@ fun ThemeConfigScreen(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (showNavIconSheet) {
|
||||||
|
NavIconManageSheet(
|
||||||
|
show = true,
|
||||||
|
onDismissRequest = { showNavIconSheet = false }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ColorPickerSheet(
|
|
||||||
show = showColorPicker,
|
|
||||||
initialColor = if (pickNightSeedColor) {
|
|
||||||
nightPrimaryColorValue.value
|
|
||||||
} else {
|
|
||||||
primaryColorValue.value
|
|
||||||
},
|
|
||||||
onDismissRequest = { showColorPicker = false },
|
|
||||||
onColorSelected = { color ->
|
|
||||||
if (pickNightSeedColor) {
|
|
||||||
nightPrimaryColorValue.value = color
|
|
||||||
ThemeConfig.cNPrimary = color
|
|
||||||
} else {
|
|
||||||
primaryColorValue.value = color
|
|
||||||
ThemeConfig.cPrimary = color
|
|
||||||
ThemeStore.editTheme(context)
|
|
||||||
.primaryColor(color)
|
|
||||||
.apply()
|
|
||||||
DynamicColors.applyToActivitiesIfAvailable(
|
|
||||||
context.applicationContext as android.app.Application,
|
|
||||||
DynamicColorsOptions.Builder()
|
|
||||||
.setContentBasedSource(context.primaryColor)
|
|
||||||
.build()
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
LauncherIconPickerSheet(
|
LauncherIconPickerSheet(
|
||||||
show = showLauncherIconPicker,
|
show = showLauncherIconPicker,
|
||||||
@@ -649,36 +772,106 @@ fun ThemeConfigScreen(
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
ThemeListDialog(
|
ColorPickerSheet(
|
||||||
show = showThemeListDialog,
|
show = showBorderColorPicker,
|
||||||
onDismissRequest = { showThemeListDialog = false }
|
initialColor = when (currentBorderColorKey) {
|
||||||
|
"containerBorderColor" -> ThemeConfig.containerBorderColor
|
||||||
|
"itemDividerColor" -> ThemeConfig.itemDividerColor
|
||||||
|
else -> 0
|
||||||
|
},
|
||||||
|
onDismissRequest = { showBorderColorPicker = false },
|
||||||
|
onColorSelected = {
|
||||||
|
when (currentBorderColorKey) {
|
||||||
|
"containerBorderColor" -> ThemeConfig.containerBorderColor = it
|
||||||
|
"itemDividerColor" -> ThemeConfig.itemDividerColor = it
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
AppAlertDialog(
|
AppModalBottomSheet(
|
||||||
data = saveThemeKey,
|
show = showFontSheet,
|
||||||
onDismissRequest = { saveThemeKey = null },
|
onDismissRequest = { showFontSheet = false },
|
||||||
title = stringResource(R.string.theme_name),
|
title = stringResource(R.string.font_setting),
|
||||||
content = {
|
startAction = {
|
||||||
AppTextField(
|
SmallIconButton(
|
||||||
value = themeName,
|
imageVector = Icons.Default.Delete,
|
||||||
onValueChange = { themeName = it },
|
contentDescription = "清除",
|
||||||
label = "name",
|
onClick = {
|
||||||
singleLine = true,
|
fontFolderUri = null
|
||||||
modifier = Modifier.fillMaxWidth(),
|
context.putPrefString(PreferKey.fontFolder, "")
|
||||||
backgroundColor = LegadoTheme.colorScheme.surface
|
ThemeConfig.appFontPath = null
|
||||||
)
|
fontItems = emptyList()
|
||||||
},
|
showFontSheet = false
|
||||||
confirmText = stringResource(android.R.string.ok),
|
|
||||||
onConfirm = { key ->
|
|
||||||
when (key) {
|
|
||||||
"saveDayTheme" -> OldThemeConfig.saveDayTheme(context, themeName)
|
|
||||||
"saveNightTheme" -> OldThemeConfig.saveNightTheme(context, themeName)
|
|
||||||
}
|
}
|
||||||
saveThemeKey = null
|
|
||||||
},
|
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
|
||||||
onDismiss = { saveThemeKey = null }
|
|
||||||
)
|
)
|
||||||
|
},
|
||||||
|
endAction = {
|
||||||
|
SmallIconButton(
|
||||||
|
imageVector = Icons.Default.Add,
|
||||||
|
contentDescription = "选择文件夹",
|
||||||
|
onClick = { fontFolderLauncher.launch(null) }
|
||||||
|
)
|
||||||
|
},
|
||||||
|
content = {
|
||||||
|
if (fontItems.isEmpty()) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(120.dp),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
text = "没有字体文件",
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
LazyVerticalGrid(
|
||||||
|
columns = GridCells.Fixed(2),
|
||||||
|
modifier = Modifier.fillMaxWidth(),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(8.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(8.dp)
|
||||||
|
) {
|
||||||
|
fontItems.forEach { fontDoc ->
|
||||||
|
item {
|
||||||
|
Card(
|
||||||
|
modifier = Modifier.fillMaxWidth().height(100.dp),
|
||||||
|
onClick = {
|
||||||
|
ThemeConfig.appFontPath = fontDoc.uri.toString()
|
||||||
|
showFontSheet = false
|
||||||
|
},
|
||||||
|
colors = CardDefaults.cardColors(
|
||||||
|
containerColor = MaterialTheme.colorScheme.surfaceVariant
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
android.widget.TextView(ctx).apply {
|
||||||
|
text = fontDoc.name
|
||||||
|
textSize = 14f
|
||||||
|
gravity = android.view.Gravity.CENTER
|
||||||
|
maxLines = 2
|
||||||
|
ellipsize = android.text.TextUtils.TruncateAt.END
|
||||||
|
runCatching {
|
||||||
|
val typeface: Typeface? = if (fontDoc.uri.scheme == "content") {
|
||||||
|
ctx.contentResolver.openFileDescriptor(fontDoc.uri, "r")?.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Typeface.createFromFile(fontDoc.uri.path!!)
|
||||||
|
}
|
||||||
|
this.typeface = typeface
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@@ -961,27 +1154,6 @@ fun ThemeCard(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun formatColorOption(colorValue: Int): String? {
|
|
||||||
if (colorValue == 0) return null
|
|
||||||
return "#${Integer.toHexString(colorValue).uppercase()}"
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
private fun ColorSwatch(colorValue: Int) {
|
|
||||||
if (colorValue == 0) return
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.size(28.dp)
|
|
||||||
.clip(CircleShape)
|
|
||||||
.background(Color(colorValue))
|
|
||||||
.border(
|
|
||||||
1.dp,
|
|
||||||
MaterialTheme.colorScheme.outlineVariant,
|
|
||||||
CircleShape
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
data class ThemeColorPalette(
|
data class ThemeColorPalette(
|
||||||
val primary: Color,
|
val primary: Color,
|
||||||
val secondary: Color,
|
val secondary: Color,
|
||||||
@@ -1065,3 +1237,4 @@ private fun getThemeColors(
|
|||||||
onSurfaceVariant = colorScheme.onSurfaceVariant
|
onSurfaceVariant = colorScheme.onSurfaceVariant
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
package io.legado.app.ui.config.themeConfig
|
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
|
||||||
import androidx.compose.foundation.layout.Box
|
|
||||||
import androidx.compose.foundation.layout.Column
|
|
||||||
import androidx.compose.foundation.layout.Row
|
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
|
||||||
import androidx.compose.foundation.layout.padding
|
|
||||||
import androidx.compose.foundation.lazy.LazyColumn
|
|
||||||
import androidx.compose.foundation.lazy.itemsIndexed
|
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.filled.Delete
|
|
||||||
import androidx.compose.material.icons.filled.FileDownload
|
|
||||||
import androidx.compose.material.icons.filled.Share
|
|
||||||
import androidx.compose.material3.AlertDialog
|
|
||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.TextButton
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.compose.runtime.getValue
|
|
||||||
import androidx.compose.runtime.mutableIntStateOf
|
|
||||||
import androidx.compose.runtime.mutableStateOf
|
|
||||||
import androidx.compose.runtime.remember
|
|
||||||
import androidx.compose.runtime.setValue
|
|
||||||
import androidx.compose.ui.Alignment
|
|
||||||
import androidx.compose.ui.Modifier
|
|
||||||
import androidx.compose.ui.graphics.Color
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
|
||||||
import androidx.compose.ui.unit.dp
|
|
||||||
import androidx.core.graphics.toColorInt
|
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.help.config.OldThemeConfig
|
|
||||||
import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
|
||||||
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
|
||||||
import io.legado.app.ui.widget.components.button.MediumIconButton
|
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.getClipText
|
|
||||||
import io.legado.app.utils.share
|
|
||||||
import io.legado.app.utils.toastOnUi
|
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3Api::class)
|
|
||||||
@Composable
|
|
||||||
fun ThemeListDialog(
|
|
||||||
show: Boolean,
|
|
||||||
onDismissRequest: () -> Unit
|
|
||||||
) {
|
|
||||||
val context = LocalContext.current
|
|
||||||
var listVersion by remember { mutableIntStateOf(0) }
|
|
||||||
var deleteIndex by remember { mutableStateOf<Int?>(null) }
|
|
||||||
val themeList = remember(listVersion) { OldThemeConfig.configList.toList() }
|
|
||||||
|
|
||||||
AppModalBottomSheet(
|
|
||||||
show = show,
|
|
||||||
onDismissRequest = onDismissRequest,
|
|
||||||
title = stringResource(R.string.theme_list),
|
|
||||||
endAction = {
|
|
||||||
MediumIconButton(
|
|
||||||
onClick = {
|
|
||||||
val clipText = context.getClipText()
|
|
||||||
if (clipText != null && OldThemeConfig.addConfig(clipText)) {
|
|
||||||
listVersion++
|
|
||||||
} else {
|
|
||||||
context.toastOnUi("Import failed")
|
|
||||||
}
|
|
||||||
},
|
|
||||||
imageVector = Icons.Default.FileDownload
|
|
||||||
)
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(bottom = 24.dp)
|
|
||||||
) {
|
|
||||||
LazyColumn(
|
|
||||||
modifier = Modifier.fillMaxWidth(),
|
|
||||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
|
||||||
) {
|
|
||||||
itemsIndexed(themeList, key = { _, item -> item.themeName }) { index, item ->
|
|
||||||
GlassCard(
|
|
||||||
onClick = {
|
|
||||||
OldThemeConfig.applyConfig(context, item)
|
|
||||||
},
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth(),
|
|
||||||
cornerRadius = 20.dp,
|
|
||||||
containerColor = if (item.primaryColor.toColorInt() == context.primaryColor) {
|
|
||||||
MaterialTheme.colorScheme.secondaryContainer
|
|
||||||
} else {
|
|
||||||
MaterialTheme.colorScheme.surfaceContainer
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxWidth()
|
|
||||||
.padding(start = 16.dp, end = 8.dp, top = 12.dp, bottom = 12.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically
|
|
||||||
) {
|
|
||||||
Box(
|
|
||||||
modifier = Modifier
|
|
||||||
.padding(end = 12.dp)
|
|
||||||
.background(
|
|
||||||
color = Color(
|
|
||||||
item.primaryColor.toColorInt()
|
|
||||||
),
|
|
||||||
shape = MaterialTheme.shapes.large
|
|
||||||
)
|
|
||||||
.padding(8.dp)
|
|
||||||
)
|
|
||||||
AppText(
|
|
||||||
text = item.themeName,
|
|
||||||
style = LegadoTheme.typography.bodyLarge,
|
|
||||||
modifier = Modifier.weight(1f)
|
|
||||||
)
|
|
||||||
IconButton(
|
|
||||||
onClick = {
|
|
||||||
val json = GSON.toJson(item)
|
|
||||||
context.share(json, "主题分享")
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Share,
|
|
||||||
contentDescription = stringResource(R.string.share)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { deleteIndex = index }) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Default.Delete,
|
|
||||||
contentDescription = stringResource(R.string.delete)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
AppAlertDialog(
|
|
||||||
data = deleteIndex,
|
|
||||||
onDismissRequest = { deleteIndex = null },
|
|
||||||
title = stringResource(R.string.delete),
|
|
||||||
confirmText = stringResource(android.R.string.ok),
|
|
||||||
onConfirm = { index ->
|
|
||||||
OldThemeConfig.delConfig(index)
|
|
||||||
listVersion++
|
|
||||||
deleteIndex = null
|
|
||||||
},
|
|
||||||
dismissText = stringResource(android.R.string.cancel),
|
|
||||||
onDismiss = { deleteIndex = null }
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,372 @@
|
|||||||
|
package io.legado.app.ui.config.themePack
|
||||||
|
|
||||||
|
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||||
|
import androidx.activity.result.contract.ActivityResultContracts
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.lazy.LazyColumn
|
||||||
|
import androidx.compose.foundation.lazy.items
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.filled.Delete
|
||||||
|
import androidx.compose.material.icons.filled.Share
|
||||||
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.rememberCoroutineScope
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.help.config.ThemePackConfig
|
||||||
|
import io.legado.app.ui.theme.adaptiveContentPadding
|
||||||
|
import io.legado.app.ui.widget.components.AppScaffold
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnGroup
|
||||||
|
import io.legado.app.ui.widget.components.alert.AppAlertDialog
|
||||||
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.ClickableSettingItem
|
||||||
|
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.GlassTopAppBarDefaults
|
||||||
|
import io.legado.app.ui.widget.components.topbar.TopBarNavigationButton
|
||||||
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.io.File
|
||||||
|
|
||||||
|
@OptIn(ExperimentalMaterial3Api::class)
|
||||||
|
@Composable
|
||||||
|
fun ThemePackScreen(
|
||||||
|
onBackClick: () -> Unit
|
||||||
|
) {
|
||||||
|
val scrollBehavior = GlassTopAppBarDefaults.defaultScrollBehavior()
|
||||||
|
val context = LocalContext.current
|
||||||
|
val scope = rememberCoroutineScope()
|
||||||
|
var listVersion by remember { mutableIntStateOf(0) }
|
||||||
|
var showAddDialog by remember { mutableStateOf(false) }
|
||||||
|
var newThemeName by remember { mutableStateOf("") }
|
||||||
|
var deleteTarget by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
||||||
|
var applyTarget by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
||||||
|
var exportingPack by remember { mutableStateOf<ThemePackConfig.ThemePack?>(null) }
|
||||||
|
|
||||||
|
val packList = remember(listVersion) { ThemePackConfig.packList.toList() }
|
||||||
|
|
||||||
|
val exportLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.CreateDocument("application/zip")
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
val pack = exportingPack ?: return@let
|
||||||
|
exportingPack = null
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val tempFile = File(context.cacheDir, "theme_export_${pack.name}.zip")
|
||||||
|
ThemePackConfig.exportToZip(pack, tempFile)
|
||||||
|
context.contentResolver.openOutputStream(uri)?.use { os ->
|
||||||
|
tempFile.inputStream().use { it.copyTo(os) }
|
||||||
|
}
|
||||||
|
tempFile.delete()
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("导出成功")
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("导出失败: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val importLauncher = rememberLauncherForActivityResult(
|
||||||
|
ActivityResultContracts.OpenDocument()
|
||||||
|
) { uri ->
|
||||||
|
uri?.let {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
val tempFile = File(context.cacheDir, "theme_import_${System.currentTimeMillis()}.zip")
|
||||||
|
context.contentResolver.openInputStream(it)?.use { input ->
|
||||||
|
tempFile.outputStream().use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val pack = ThemePackConfig.importFromZip(tempFile)
|
||||||
|
tempFile.delete()
|
||||||
|
if (pack != null) {
|
||||||
|
listVersion++
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("导入成功: ${pack.name}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("导入失败: 无效的主题包")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.onFailure {
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("导入失败: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppScaffold(
|
||||||
|
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
|
||||||
|
topBar = {
|
||||||
|
GlassMediumFlexibleTopAppBar(
|
||||||
|
title = stringResource(R.string.theme_pack),
|
||||||
|
scrollBehavior = scrollBehavior,
|
||||||
|
navigationIcon = {
|
||||||
|
TopBarNavigationButton(onClick = onBackClick)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
) { paddingValues ->
|
||||||
|
LazyColumn(
|
||||||
|
modifier = Modifier.fillMaxSize(),
|
||||||
|
contentPadding = adaptiveContentPadding(
|
||||||
|
top = paddingValues.calculateTopPadding(),
|
||||||
|
bottom = 120.dp
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
item {
|
||||||
|
SplicedColumnGroup {
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "新增主题",
|
||||||
|
description = "保存当前设置为新主题",
|
||||||
|
onClick = {
|
||||||
|
newThemeName = ""
|
||||||
|
showAddDialog = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
ClickableSettingItem(
|
||||||
|
title = "导入主题",
|
||||||
|
description = "从zip包导入主题",
|
||||||
|
onClick = {
|
||||||
|
importLauncher.launch(arrayOf("application/zip", "application/octet-stream"))
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (packList.isNotEmpty()) {
|
||||||
|
item {
|
||||||
|
AppText(
|
||||||
|
text = "主题列表",
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
modifier = Modifier.padding(start = 16.dp, top = 16.dp, bottom = 8.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
items(packList, key = { it.folderName }) { pack ->
|
||||||
|
ThemePackItem(
|
||||||
|
pack = pack,
|
||||||
|
onApply = { applyTarget = pack },
|
||||||
|
onExport = { exportingPack = pack },
|
||||||
|
onDelete = { deleteTarget = pack }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
data = if (showAddDialog) "add" else null,
|
||||||
|
onDismissRequest = { showAddDialog = false },
|
||||||
|
title = "新增主题",
|
||||||
|
confirmText = "保存",
|
||||||
|
onConfirm = {
|
||||||
|
if (newThemeName.isNotBlank()) {
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
ThemePackConfig.createFromCurrent(newThemeName)
|
||||||
|
listVersion++
|
||||||
|
}
|
||||||
|
showAddDialog = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissText = "取消",
|
||||||
|
onDismiss = { showAddDialog = false },
|
||||||
|
content = {
|
||||||
|
io.legado.app.ui.widget.components.AppTextField(
|
||||||
|
value = newThemeName,
|
||||||
|
onValueChange = { newThemeName = it },
|
||||||
|
placeholder = { AppText(text = "请输入主题名称") },
|
||||||
|
singleLine = true,
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
data = applyTarget,
|
||||||
|
onDismissRequest = { applyTarget = null },
|
||||||
|
title = "应用主题",
|
||||||
|
confirmText = "应用",
|
||||||
|
onConfirm = { pack ->
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
ThemePackConfig.applyPack(pack)
|
||||||
|
withContext(Dispatchers.Main) {
|
||||||
|
context.toastOnUi("主题已应用,请重启生效")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
applyTarget = null
|
||||||
|
},
|
||||||
|
dismissText = "取消",
|
||||||
|
onDismiss = { applyTarget = null },
|
||||||
|
text = "确定应用主题「${applyTarget?.name}」?应用后需要重启才能完全生效。"
|
||||||
|
)
|
||||||
|
|
||||||
|
AppAlertDialog(
|
||||||
|
data = deleteTarget,
|
||||||
|
onDismissRequest = { deleteTarget = null },
|
||||||
|
title = "删除主题",
|
||||||
|
confirmText = "删除",
|
||||||
|
onConfirm = { pack ->
|
||||||
|
scope.launch(Dispatchers.IO) {
|
||||||
|
ThemePackConfig.deletePack(pack)
|
||||||
|
listVersion++
|
||||||
|
}
|
||||||
|
deleteTarget = null
|
||||||
|
},
|
||||||
|
dismissText = "取消",
|
||||||
|
onDismiss = { deleteTarget = null },
|
||||||
|
text = "确定删除主题「${deleteTarget?.name}」?此操作不可恢复。"
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(exportingPack) {
|
||||||
|
exportingPack?.let { pack ->
|
||||||
|
kotlin.runCatching {
|
||||||
|
exportLauncher.launch("${pack.name}.zip")
|
||||||
|
}.onFailure {
|
||||||
|
exportingPack = null
|
||||||
|
context.toastOnUi("导出失败: ${it.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun ThemePackItem(
|
||||||
|
pack: ThemePackConfig.ThemePack,
|
||||||
|
onApply: () -> Unit,
|
||||||
|
onExport: () -> Unit,
|
||||||
|
onDelete: () -> Unit
|
||||||
|
) {
|
||||||
|
GlassCard(
|
||||||
|
onClick = onApply,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 16.dp, vertical = 4.dp),
|
||||||
|
cornerRadius = 20.dp
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
val packDir = remember { ThemePackConfig.getPackDir(pack) }
|
||||||
|
val bgFile = remember(pack.folderName) {
|
||||||
|
packDir.listFiles()?.firstOrNull {
|
||||||
|
it.isFile && (it.name.startsWith("bg_light") || it.name.startsWith("bg_dark"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val coverFile = remember(pack.folderName) {
|
||||||
|
packDir.listFiles()?.firstOrNull {
|
||||||
|
it.isFile && it.name.startsWith("cover_day_")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val previewFile = bgFile ?: coverFile
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.height(120.dp)
|
||||||
|
.clip(RoundedCornerShape(topStart = 20.dp, topEnd = 20.dp))
|
||||||
|
.background(
|
||||||
|
if (pack.themeColor != 0) Color(pack.themeColor)
|
||||||
|
else MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
if (previewFile != null) {
|
||||||
|
AsyncImage(
|
||||||
|
model = previewFile,
|
||||||
|
contentDescription = null,
|
||||||
|
contentScale = ContentScale.Crop,
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(start = 16.dp, end = 8.dp, top = 10.dp, bottom = 10.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
Column(
|
||||||
|
modifier = Modifier.weight(1f)
|
||||||
|
) {
|
||||||
|
AppText(
|
||||||
|
text = pack.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge
|
||||||
|
)
|
||||||
|
val hasCustomIcon = pack.navIconBookshelf.isNotEmpty() ||
|
||||||
|
pack.navIconExplore.isNotEmpty() ||
|
||||||
|
pack.navIconRss.isNotEmpty() ||
|
||||||
|
pack.navIconMy.isNotEmpty()
|
||||||
|
val hasCover = pack.defaultCover.isNotEmpty() || pack.defaultCoverDark.isNotEmpty()
|
||||||
|
val features = mutableListOf<String>()
|
||||||
|
if (pack.appFontPath != null) features.add("应用字体")
|
||||||
|
if (pack.bgImageLight != null || pack.bgImageDark != null) features.add("应用背景")
|
||||||
|
if (hasCover) features.add("封面图片")
|
||||||
|
if (hasCustomIcon) features.add("图标")
|
||||||
|
if (features.isNotEmpty()) {
|
||||||
|
AppText(
|
||||||
|
text = features.joinToString(" · "),
|
||||||
|
style = MaterialTheme.typography.bodySmall,
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onExport) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Share,
|
||||||
|
contentDescription = "导出",
|
||||||
|
modifier = Modifier.size(20.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
IconButton(onClick = onDelete) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.Delete,
|
||||||
|
contentDescription = "删除",
|
||||||
|
modifier = Modifier.size(20.dp),
|
||||||
|
tint = MaterialTheme.colorScheme.error
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -61,8 +61,11 @@ import io.legado.app.ui.config.backupConfig.BackupConfigScreen
|
|||||||
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
import io.legado.app.ui.config.coverConfig.CoverConfigScreen
|
||||||
import io.legado.app.ui.config.mainConfig.MainConfig
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
import io.legado.app.ui.config.otherConfig.OtherConfigScreen
|
||||||
|
import io.legado.app.ui.config.personalizationConfig.FontSelectScreen
|
||||||
|
import io.legado.app.ui.config.customTheme.CustomThemeScreen
|
||||||
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
import io.legado.app.ui.config.readConfig.ReadConfigScreen
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
|
import io.legado.app.ui.config.themeConfig.ThemeConfigScreen
|
||||||
|
import io.legado.app.ui.config.themePack.ThemePackScreen
|
||||||
import io.legado.app.ui.rss.article.MainRouteRssSort
|
import io.legado.app.ui.rss.article.MainRouteRssSort
|
||||||
import io.legado.app.ui.rss.article.RssSortRouteScreen
|
import io.legado.app.ui.rss.article.RssSortRouteScreen
|
||||||
import io.legado.app.ui.rss.read.MainRouteRssRead
|
import io.legado.app.ui.rss.read.MainRouteRssRead
|
||||||
@@ -97,6 +100,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
private const val ROUTE_SETTINGS_COVER = "settings/cover"
|
private const val ROUTE_SETTINGS_COVER = "settings/cover"
|
||||||
private const val ROUTE_SETTINGS_THEME = "settings/theme"
|
private const val ROUTE_SETTINGS_THEME = "settings/theme"
|
||||||
private const val ROUTE_SETTINGS_BACKUP = "settings/backup"
|
private const val ROUTE_SETTINGS_BACKUP = "settings/backup"
|
||||||
|
private const val ROUTE_SETTINGS_CUSTOM_THEME = "settings/custom_theme"
|
||||||
private const val ROUTE_IMPORT_LOCAL = "import/local"
|
private const val ROUTE_IMPORT_LOCAL = "import/local"
|
||||||
private const val ROUTE_IMPORT_REMOTE = "import/remote"
|
private const val ROUTE_IMPORT_REMOTE = "import/remote"
|
||||||
private const val ROUTE_CACHE = "cache"
|
private const val ROUTE_CACHE = "cache"
|
||||||
@@ -278,6 +282,12 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
@Serializable
|
@Serializable
|
||||||
private data object MainRouteSettingsBackup : MainRoute
|
private data object MainRouteSettingsBackup : MainRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data object MainRouteSettingsCustomTheme : MainRoute
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
private data object MainRouteSettingsThemePack : MainRoute
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
private data object MainRouteImportLocal : MainRoute
|
private data object MainRouteImportLocal : MainRoute
|
||||||
|
|
||||||
@@ -543,13 +553,27 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
entry<MainRouteSettingsTheme> {
|
entry<MainRouteSettingsTheme> {
|
||||||
ThemeConfigScreen(onBackClick = { navigateBack(backStack) })
|
ThemeConfigScreen(
|
||||||
|
onBackClick = { navigateBack(backStack) },
|
||||||
|
onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<MainRouteSettingsBackup> {
|
entry<MainRouteSettingsBackup> {
|
||||||
BackupConfigScreen(onBackClick = { navigateBack(backStack) })
|
BackupConfigScreen(onBackClick = { navigateBack(backStack) })
|
||||||
}
|
}
|
||||||
|
|
||||||
|
entry<MainRouteSettingsCustomTheme> {
|
||||||
|
CustomThemeScreen(
|
||||||
|
onBackClick = { navigateBack(backStack) },
|
||||||
|
onNavigateToThemePack = { backStack.add(MainRouteSettingsThemePack) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
entry<MainRouteSettingsThemePack> {
|
||||||
|
ThemePackScreen(onBackClick = { navigateBack(backStack) })
|
||||||
|
}
|
||||||
|
|
||||||
entry<MainRouteImportLocal> {
|
entry<MainRouteImportLocal> {
|
||||||
ImportBookScreen(
|
ImportBookScreen(
|
||||||
onBackClick = { navigateBack(backStack) }
|
onBackClick = { navigateBack(backStack) }
|
||||||
@@ -599,7 +623,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
}
|
}
|
||||||
|
|
||||||
DisposableEffect(lifecycleOwner, searchViewModel) {
|
DisposableEffect(lifecycleOwner, searchViewModel) {
|
||||||
searchViewModel.onIntent(SearchIntent.ResumeEngine)
|
|
||||||
val observer = LifecycleEventObserver { _, event ->
|
val observer = LifecycleEventObserver { _, event ->
|
||||||
when (event) {
|
when (event) {
|
||||||
Lifecycle.Event.ON_RESUME -> {
|
Lifecycle.Event.ON_RESUME -> {
|
||||||
@@ -616,7 +639,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
lifecycleOwner.lifecycle.addObserver(observer)
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
onDispose {
|
onDispose {
|
||||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||||
searchViewModel.onIntent(SearchIntent.PauseEngine)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,6 +667,15 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
initialSortUrl = route.sortUrl,
|
initialSortUrl = route.sortUrl,
|
||||||
onBackClick = { navigateBack(backStack) },
|
onBackClick = { navigateBack(backStack) },
|
||||||
onOpenRead = { title, origin, link, openUrl ->
|
onOpenRead = { title, origin, link, openUrl ->
|
||||||
|
if (link?.contains("@js:") == true) {
|
||||||
|
navigateToRoute(
|
||||||
|
backStack,
|
||||||
|
MainRouteRssSort(
|
||||||
|
sourceUrl = origin,
|
||||||
|
sortUrl = link
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
navigateToRoute(
|
navigateToRoute(
|
||||||
backStack,
|
backStack,
|
||||||
MainRouteRssRead(
|
MainRouteRssRead(
|
||||||
@@ -655,6 +686,7 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -668,7 +700,18 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
entry<MainRouteBookInfo> { route ->
|
entry<MainRouteBookInfo>(
|
||||||
|
metadata = NavDisplay.transitionSpec {
|
||||||
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
|
fadeOut(animationSpec = tween(300))
|
||||||
|
} + NavDisplay.popTransitionSpec {
|
||||||
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
|
fadeOut(animationSpec = tween(300))
|
||||||
|
} + NavDisplay.predictivePopTransitionSpec { _ ->
|
||||||
|
fadeIn(animationSpec = tween(300)) togetherWith
|
||||||
|
fadeOut(animationSpec = tween(300))
|
||||||
|
}
|
||||||
|
) { route ->
|
||||||
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
val bookInfoViewModel = koinViewModel<BookInfoViewModel>()
|
||||||
BookInfoRouteScreen(
|
BookInfoRouteScreen(
|
||||||
bookUrl = route.bookUrl,
|
bookUrl = route.bookUrl,
|
||||||
@@ -734,7 +777,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
MainRouteSettingsRead,
|
MainRouteSettingsRead,
|
||||||
MainRouteSettingsCover,
|
MainRouteSettingsCover,
|
||||||
MainRouteSettingsTheme,
|
MainRouteSettingsTheme,
|
||||||
MainRouteSettingsBackup -> {
|
MainRouteSettingsBackup,
|
||||||
|
MainRouteSettingsCustomTheme,
|
||||||
|
MainRouteSettingsThemePack -> {
|
||||||
backStack.clear()
|
backStack.clear()
|
||||||
backStack.add(MainRouteHome)
|
backStack.add(MainRouteHome)
|
||||||
backStack.add(MainRouteSettings)
|
backStack.add(MainRouteSettings)
|
||||||
@@ -969,6 +1014,16 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
|||||||
|
|
||||||
private fun resolveStartRoute(route: String?): MainRoute {
|
private fun resolveStartRoute(route: String?): MainRoute {
|
||||||
return when (route) {
|
return when (route) {
|
||||||
|
"main" -> MainRouteHome
|
||||||
|
"settings" -> MainRouteSettings
|
||||||
|
"settings/other" -> MainRouteSettingsOther
|
||||||
|
"settings/read" -> MainRouteSettingsRead
|
||||||
|
"settings/cover" -> MainRouteSettingsCover
|
||||||
|
"settings/theme" -> MainRouteSettingsTheme
|
||||||
|
"settings/backup" -> MainRouteSettingsBackup
|
||||||
|
"settings/custom_theme" -> MainRouteSettingsCustomTheme
|
||||||
|
"import/local" -> MainRouteImportLocal
|
||||||
|
"import/remote" -> MainRouteImportRemote
|
||||||
ROUTE_MAIN -> MainRouteHome
|
ROUTE_MAIN -> MainRouteHome
|
||||||
ROUTE_SETTINGS -> MainRouteSettings
|
ROUTE_SETTINGS -> MainRouteSettings
|
||||||
ROUTE_SETTINGS_OTHER -> MainRouteSettingsOther
|
ROUTE_SETTINGS_OTHER -> MainRouteSettingsOther
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ import androidx.compose.foundation.layout.fillMaxWidth
|
|||||||
import androidx.compose.foundation.layout.height
|
import androidx.compose.foundation.layout.height
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.Image
|
||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.foundation.pager.HorizontalPager
|
import androidx.compose.foundation.pager.HorizontalPager
|
||||||
import androidx.compose.foundation.pager.rememberPagerState
|
import androidx.compose.foundation.pager.rememberPagerState
|
||||||
@@ -57,12 +58,17 @@ import androidx.compose.ui.platform.LocalHapticFeedback
|
|||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
import dev.chrisbanes.haze.hazeSource
|
import dev.chrisbanes.haze.hazeSource
|
||||||
|
import androidx.compose.ui.graphics.asImageBitmap
|
||||||
|
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
|
import io.legado.app.ui.config.mainConfig.MainConfig
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
import io.legado.app.ui.main.bookshelf.BookshelfScreen
|
||||||
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
import io.legado.app.ui.main.bookshelf.BookshelfViewModel
|
||||||
import io.legado.app.ui.main.explore.ExploreScreen
|
import io.legado.app.ui.main.explore.ExploreScreen
|
||||||
@@ -149,7 +155,11 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val hazeState = remember { HazeState() }
|
val hazeState = remember { HazeState() }
|
||||||
val floatingBarSurfaceColor = MaterialTheme.colorScheme.surface
|
val floatingBarSurfaceColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
|
Color(ThemeConfig.secondaryThemeColor)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
val floatingBarBackdrop = rememberLayerBackdrop {
|
val floatingBarBackdrop = rememberLayerBackdrop {
|
||||||
drawRect(floatingBarSurfaceColor)
|
drawRect(floatingBarSurfaceColor)
|
||||||
drawContent()
|
drawContent()
|
||||||
@@ -270,7 +280,13 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
label = if (labelVisibilityMode != "unlabeled") {
|
label = if (labelVisibilityMode != "unlabeled") {
|
||||||
{ AppText(stringResource(destination.labelId)) }
|
val hasCustomIcon = when (destination) {
|
||||||
|
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||||
|
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||||
|
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||||
|
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||||
|
}
|
||||||
|
if (hasCustomIcon) null else {{ AppText(stringResource(destination.labelId)) }}
|
||||||
} else null
|
} else null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -280,67 +296,16 @@ fun MainScreen(
|
|||||||
AppScaffold(
|
AppScaffold(
|
||||||
modifier = Modifier.weight(1f),
|
modifier = Modifier.weight(1f),
|
||||||
bottomBar = {
|
bottomBar = {
|
||||||
if (!useRail && mainUiState.showBottomView) {
|
if (!useRail && mainUiState.showBottomView && !useFloatingBottomBar) {
|
||||||
if (useFloatingBottomBar) {
|
AppNavigationBar() {
|
||||||
Box(modifier = Modifier.fillMaxWidth()) {
|
|
||||||
FloatingBottomBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.align(Alignment.BottomCenter)
|
|
||||||
.clickable(
|
|
||||||
interactionSource = remember { MutableInteractionSource() },
|
|
||||||
indication = null,
|
|
||||||
onClick = {}
|
|
||||||
)
|
|
||||||
.padding(
|
|
||||||
start = 16.dp,
|
|
||||||
end = 16.dp,
|
|
||||||
bottom = 12.dp + WindowInsets.navigationBars
|
|
||||||
.asPaddingValues()
|
|
||||||
.calculateBottomPadding()
|
|
||||||
),
|
|
||||||
selectedIndex = { pagerState.targetPage },
|
|
||||||
onSelected = { index ->
|
|
||||||
coroutineScope.launch {
|
|
||||||
pagerState.animateScrollToPage(index)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
backdrop = floatingBarBackdrop,
|
|
||||||
tabsCount = destinations.size,
|
|
||||||
isBlurEnabled = useLiquidGlass
|
|
||||||
) {
|
|
||||||
destinations.forEachIndexed { index, destination ->
|
destinations.forEachIndexed { index, destination ->
|
||||||
val selected = pagerState.targetPage == index
|
val selected = pagerState.targetPage == index
|
||||||
FloatingBottomBarItem(
|
val customIconPath = when (destination) {
|
||||||
onClick = {
|
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
||||||
coroutineScope.launch {
|
MainDestination.Explore -> MainConfig.navIconExplore
|
||||||
pagerState.animateScrollToPage(index)
|
MainDestination.Rss -> MainConfig.navIconRss
|
||||||
|
MainDestination.My -> MainConfig.navIconMy
|
||||||
}
|
}
|
||||||
},
|
|
||||||
modifier = Modifier.defaultMinSize(minWidth = 76.dp)
|
|
||||||
) {
|
|
||||||
NavigationIcon(
|
|
||||||
destination = destination,
|
|
||||||
selected = selected
|
|
||||||
)
|
|
||||||
if (showLabel && (alwaysShowLabel || selected)) {
|
|
||||||
AppText(
|
|
||||||
text = stringResource(destination.labelId),
|
|
||||||
style = MaterialTheme.typography.labelSmall,
|
|
||||||
maxLines = 1,
|
|
||||||
overflow = TextOverflow.Ellipsis
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
AppNavigationBar(
|
|
||||||
modifier = Modifier
|
|
||||||
.regularHazeEffect(state = hazeState)
|
|
||||||
) {
|
|
||||||
destinations.forEachIndexed { index, destination ->
|
|
||||||
val selected = pagerState.targetPage == index
|
|
||||||
AppNavigationBarItem(
|
AppNavigationBarItem(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = {
|
onClick = {
|
||||||
@@ -358,22 +323,25 @@ fun MainScreen(
|
|||||||
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||||
blurAlpha = GlassDefaults.ThickBlurAlpha
|
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||||
),
|
),
|
||||||
m3ShowLabel = showLabel,
|
m3ShowLabel = showLabel && !customIconPath.isNotEmpty(),
|
||||||
m3AlwaysShowLabel = alwaysShowLabel
|
m3AlwaysShowLabel = alwaysShowLabel && !customIconPath.isNotEmpty(),
|
||||||
|
useCustomIcon = customIconPath.isNotEmpty()
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
},
|
},
|
||||||
contentWindowInsets = WindowInsets(0)
|
contentWindowInsets = WindowInsets(0)
|
||||||
) { _ ->
|
) { _ ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier.fillMaxSize()
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.then(
|
||||||
|
if (useLiquidGlass) {
|
||||||
|
Modifier
|
||||||
.hazeSource(hazeState)
|
.hazeSource(hazeState)
|
||||||
.then(
|
.layerBackdrop(floatingBarBackdrop)
|
||||||
if (useFloatingBottomBar) {
|
|
||||||
Modifier.layerBackdrop(floatingBarBackdrop)
|
|
||||||
} else {
|
} else {
|
||||||
Modifier
|
Modifier
|
||||||
}
|
}
|
||||||
@@ -426,6 +394,79 @@ fun MainScreen(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!useRail && mainUiState.showBottomView && useFloatingBottomBar) {
|
||||||
|
Box(modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.fillMaxWidth()
|
||||||
|
) {
|
||||||
|
FloatingBottomBar(
|
||||||
|
modifier = Modifier
|
||||||
|
.align(Alignment.BottomCenter)
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
onClick = {}
|
||||||
|
)
|
||||||
|
.padding(
|
||||||
|
start = 16.dp,
|
||||||
|
end = 16.dp,
|
||||||
|
bottom = 12.dp + WindowInsets.navigationBars
|
||||||
|
.asPaddingValues()
|
||||||
|
.calculateBottomPadding()
|
||||||
|
),
|
||||||
|
selectedIndex = { pagerState.targetPage },
|
||||||
|
onSelected = { index ->
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
backdrop = floatingBarBackdrop,
|
||||||
|
tabsCount = destinations.size,
|
||||||
|
isBlurEnabled = useLiquidGlass,
|
||||||
|
hasCustomIcons = destinations.any { dest ->
|
||||||
|
when (dest) {
|
||||||
|
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||||
|
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||||
|
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||||
|
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
destinations.forEachIndexed { index, destination ->
|
||||||
|
val selected = pagerState.targetPage == index
|
||||||
|
val hasCustomIcon = when (destination) {
|
||||||
|
MainDestination.Bookshelf -> MainConfig.navIconBookshelf.isNotEmpty()
|
||||||
|
MainDestination.Explore -> MainConfig.navIconExplore.isNotEmpty()
|
||||||
|
MainDestination.Rss -> MainConfig.navIconRss.isNotEmpty()
|
||||||
|
MainDestination.My -> MainConfig.navIconMy.isNotEmpty()
|
||||||
|
}
|
||||||
|
FloatingBottomBarItem(
|
||||||
|
onClick = {
|
||||||
|
coroutineScope.launch {
|
||||||
|
pagerState.animateScrollToPage(index)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.defaultMinSize(minWidth = 76.dp)
|
||||||
|
) {
|
||||||
|
NavigationIcon(
|
||||||
|
destination = destination,
|
||||||
|
selected = selected
|
||||||
|
)
|
||||||
|
if (!hasCustomIcon && showLabel && (alwaysShowLabel || selected)) {
|
||||||
|
AppText(
|
||||||
|
text = stringResource(destination.labelId),
|
||||||
|
style = MaterialTheme.typography.labelSmall,
|
||||||
|
maxLines = 1,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -474,6 +515,33 @@ private fun NavigationIcon(
|
|||||||
selected: Boolean,
|
selected: Boolean,
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
) {
|
) {
|
||||||
|
val customIconPath = when (destination) {
|
||||||
|
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
||||||
|
MainDestination.Explore -> MainConfig.navIconExplore
|
||||||
|
MainDestination.Rss -> MainConfig.navIconRss
|
||||||
|
MainDestination.My -> MainConfig.navIconMy
|
||||||
|
}
|
||||||
|
if (customIconPath.isNotEmpty()) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val bitmap = remember(customIconPath) {
|
||||||
|
kotlin.runCatching {
|
||||||
|
android.graphics.BitmapFactory.decodeFile(customIconPath)
|
||||||
|
}.getOrNull()
|
||||||
|
}
|
||||||
|
if (bitmap != null) {
|
||||||
|
Image(
|
||||||
|
painter = remember(bitmap) {
|
||||||
|
BitmapPainter(bitmap.asImageBitmap())
|
||||||
|
},
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = modifier.size(40.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
val icon = AppIcons.mainDestination(destination, selected)
|
val icon = AppIcons.mainDestination(destination, selected)
|
||||||
AppIcon(icon, contentDescription = null, modifier = modifier)
|
AppIcon(icon, contentDescription = null, modifier = modifier)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val icon = AppIcons.mainDestination(destination, selected)
|
||||||
|
AppIcon(icon, contentDescription = null, modifier = modifier)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
|
|
||||||
|
@Stable
|
||||||
|
data class BookGroupUi(
|
||||||
|
val groupId: Long,
|
||||||
|
val groupName: String,
|
||||||
|
val cover: String?,
|
||||||
|
val order: Int,
|
||||||
|
val enableRefresh: Boolean,
|
||||||
|
val show: Boolean,
|
||||||
|
val bookSort: Int
|
||||||
|
)
|
||||||
|
|
||||||
|
fun BookGroup.toBookGroupUi() = BookGroupUi(
|
||||||
|
groupId = groupId,
|
||||||
|
groupName = groupName,
|
||||||
|
cover = cover,
|
||||||
|
order = order,
|
||||||
|
enableRefresh = enableRefresh,
|
||||||
|
show = show,
|
||||||
|
bookSort = bookSort
|
||||||
|
)
|
||||||
@@ -5,10 +5,12 @@ import androidx.compose.animation.ExperimentalSharedTransitionApi
|
|||||||
import androidx.compose.animation.SharedTransitionScope
|
import androidx.compose.animation.SharedTransitionScope
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.FlowRow
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
import androidx.compose.foundation.layout.aspectRatio
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
@@ -24,19 +26,29 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.ui.Alignment
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawWithContent
|
||||||
import androidx.compose.ui.draw.shadow
|
import androidx.compose.ui.draw.shadow
|
||||||
|
import androidx.compose.ui.geometry.CornerRadius
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.geometry.Size
|
||||||
import androidx.compose.ui.graphics.Brush
|
import androidx.compose.ui.graphics.Brush
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.graphics.PathEffect
|
||||||
import androidx.compose.ui.graphics.Shadow
|
import androidx.compose.ui.graphics.Shadow
|
||||||
|
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
import androidx.compose.ui.text.style.TextAlign
|
import androidx.compose.ui.text.style.TextAlign
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.text.AnnotatedString
|
||||||
|
import androidx.compose.ui.text.SpanStyle
|
||||||
|
import androidx.compose.ui.text.buildAnnotatedString
|
||||||
|
import androidx.compose.ui.text.withStyle
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.card.NormalCard
|
import io.legado.app.ui.widget.components.card.NormalCard
|
||||||
@@ -44,6 +56,7 @@ import io.legado.app.ui.widget.components.card.TextCard
|
|||||||
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
import io.legado.app.ui.widget.components.cover.CoilBookCover
|
||||||
import io.legado.app.ui.widget.components.cover.BookshelfCover
|
import io.legado.app.ui.widget.components.cover.BookshelfCover
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
|
import io.legado.app.utils.splitNotBlank
|
||||||
import io.legado.app.utils.toTimeAgo
|
import io.legado.app.utils.toTimeAgo
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,24 +76,62 @@ fun BookshelfItem(
|
|||||||
titleEnd: @Composable (() -> Unit)? = null,
|
titleEnd: @Composable (() -> Unit)? = null,
|
||||||
subTitle: String? = null,
|
subTitle: String? = null,
|
||||||
desc: String? = null,
|
desc: String? = null,
|
||||||
|
descMaxLines: Int = 1,
|
||||||
extra: @Composable (RowScope.() -> Unit)? = null,
|
extra: @Composable (RowScope.() -> Unit)? = null,
|
||||||
|
bottomContent: @Composable (() -> Unit)? = null,
|
||||||
titleSmallFont: Boolean = false,
|
titleSmallFont: Boolean = false,
|
||||||
titleCenter: Boolean = true,
|
titleCenter: Boolean = true,
|
||||||
titleMaxLines: Int = 2,
|
titleMaxLines: Int = 2,
|
||||||
coverShadow: Boolean = false,
|
coverShadow: Boolean = false,
|
||||||
|
titleColor: Color? = null,
|
||||||
|
descAnnotated: AnnotatedString? = null,
|
||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: (() -> Unit)?
|
onLongClick: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
val selectedColor = if (isSelected) {
|
val containerColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
LegadoTheme.colorScheme.secondaryContainer
|
Color(ThemeConfig.secondaryThemeColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.cardContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
val borderWidth = ThemeConfig.containerBorderWidth.dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
val borderStyle = ThemeConfig.containerBorderStyle
|
||||||
|
val enableBorder = ThemeConfig.enableDeepPersonalization && ThemeConfig.enableContainerBorder
|
||||||
|
|
||||||
|
val borderModifier = if (enableBorder && borderStyle == "solid") {
|
||||||
|
Modifier.border(borderWidth, borderColor, MaterialTheme.shapes.small)
|
||||||
|
} else if (enableBorder) {
|
||||||
|
val dashWidth = ThemeConfig.containerBorderDashWidth
|
||||||
|
val pathEffect = when (borderStyle) {
|
||||||
|
"dashed" -> PathEffect.dashPathEffect(floatArrayOf(dashWidth, dashWidth))
|
||||||
|
"dotted" -> PathEffect.dashPathEffect(floatArrayOf(dashWidth / 2, dashWidth))
|
||||||
|
else -> null
|
||||||
|
}
|
||||||
|
Modifier.drawWithContent {
|
||||||
|
drawContent()
|
||||||
|
val cornerRadius = CornerRadius(8.dp.toPx(), 8.dp.toPx())
|
||||||
|
val halfStroke = borderWidth.toPx() / 2
|
||||||
|
drawRoundRect(
|
||||||
|
color = borderColor,
|
||||||
|
topLeft = Offset(halfStroke, halfStroke),
|
||||||
|
size = Size(size.width - borderWidth.toPx(), size.height - borderWidth.toPx()),
|
||||||
|
cornerRadius = cornerRadius,
|
||||||
|
style = Stroke(width = borderWidth.toPx(), pathEffect = pathEffect)
|
||||||
|
)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
LegadoTheme.colorScheme.surface.copy(alpha = 0f)
|
LegadoTheme.colorScheme.surface.copy(alpha = 0f)
|
||||||
|
Modifier
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isGrid) {
|
if (isGrid) {
|
||||||
Box(
|
Box(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.clip(MaterialTheme.shapes.small)
|
|
||||||
.background(selectedColor)
|
|
||||||
.combinedClickable(
|
.combinedClickable(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
@@ -99,7 +150,7 @@ fun BookshelfItem(
|
|||||||
MaterialTheme.shapes.extraSmall
|
MaterialTheme.shapes.extraSmall
|
||||||
) else Modifier
|
) else Modifier
|
||||||
)
|
)
|
||||||
.clip(MaterialTheme.shapes.extraSmall) // 先阴影后裁剪
|
.clip(MaterialTheme.shapes.extraSmall)
|
||||||
) {
|
) {
|
||||||
cover(Modifier.fillMaxSize())
|
cover(Modifier.fillMaxSize())
|
||||||
if (gridStyle == 1) {
|
if (gridStyle == 1) {
|
||||||
@@ -146,14 +197,18 @@ fun BookshelfItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// 列表布局
|
|
||||||
Column {
|
Column {
|
||||||
NormalCard(
|
NormalCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(all = 4.dp),
|
.padding(all = 4.dp)
|
||||||
|
.then(borderModifier),
|
||||||
cornerRadius = 8.dp,
|
cornerRadius = 8.dp,
|
||||||
containerColor = selectedColor,
|
containerColor = if (isSelected) {
|
||||||
|
LegadoTheme.colorScheme.secondaryContainer
|
||||||
|
} else {
|
||||||
|
containerColor
|
||||||
|
},
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
onLongClick = onLongClick
|
onLongClick = onLongClick
|
||||||
) {
|
) {
|
||||||
@@ -177,7 +232,11 @@ fun BookshelfItem(
|
|||||||
) {
|
) {
|
||||||
AppText(
|
AppText(
|
||||||
text = title,
|
text = title,
|
||||||
style = LegadoTheme.typography.titleMediumEmphasized,
|
style = if (titleColor != null) {
|
||||||
|
LegadoTheme.typography.titleMediumEmphasized.copy(color = titleColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.typography.titleMediumEmphasized
|
||||||
|
},
|
||||||
maxLines = 1,
|
maxLines = 1,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
modifier = Modifier.weight(1f)
|
modifier = Modifier.weight(1f)
|
||||||
@@ -193,15 +252,24 @@ fun BookshelfItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
if (!isCompact) {
|
if (!isCompact) {
|
||||||
|
if (descAnnotated != null) {
|
||||||
|
AppText(
|
||||||
|
text = descAnnotated,
|
||||||
|
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||||
|
maxLines = descMaxLines,
|
||||||
|
overflow = TextOverflow.Ellipsis
|
||||||
|
)
|
||||||
|
} else {
|
||||||
desc?.let {
|
desc?.let {
|
||||||
AppText(
|
AppText(
|
||||||
text = it,
|
text = it,
|
||||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||||
maxLines = 1,
|
maxLines = descMaxLines,
|
||||||
overflow = TextOverflow.Ellipsis
|
overflow = TextOverflow.Ellipsis
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
extra?.let {
|
extra?.let {
|
||||||
Row(
|
Row(
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
@@ -210,6 +278,7 @@ fun BookshelfItem(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
bottomContent?.invoke()
|
||||||
}
|
}
|
||||||
if (BookshelfConfig.bookshelfShowDivider)
|
if (BookshelfConfig.bookshelfShowDivider)
|
||||||
HorizontalDivider(
|
HorizontalDivider(
|
||||||
@@ -232,7 +301,6 @@ fun BookGroupCover(
|
|||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(5f / 7f)
|
.aspectRatio(5f / 7f)
|
||||||
.clip(RoundedCornerShape(4.dp))
|
.clip(RoundedCornerShape(4.dp))
|
||||||
.background(LegadoTheme.colorScheme.surfaceContainer)
|
|
||||||
) {
|
) {
|
||||||
if (!coverPath.isNullOrBlank()) {
|
if (!coverPath.isNullOrBlank()) {
|
||||||
CoilBookCover(
|
CoilBookCover(
|
||||||
@@ -326,7 +394,7 @@ fun BookGroupCover(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookGroupItemGrid(
|
fun BookGroupItemGrid(
|
||||||
group: BookGroup,
|
group: BookGroupUi,
|
||||||
previewBooks: List<BookShelfItem>,
|
previewBooks: List<BookShelfItem>,
|
||||||
countText: String? = null,
|
countText: String? = null,
|
||||||
gridStyle: Int = 0,
|
gridStyle: Int = 0,
|
||||||
@@ -363,7 +431,7 @@ fun BookGroupItemGrid(
|
|||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun BookGroupItemList(
|
fun BookGroupItemList(
|
||||||
group: BookGroup,
|
group: BookGroupUi,
|
||||||
previewBooks: List<BookShelfItem>,
|
previewBooks: List<BookShelfItem>,
|
||||||
countText: String? = null,
|
countText: String? = null,
|
||||||
isCompact: Boolean = false,
|
isCompact: Boolean = false,
|
||||||
@@ -375,13 +443,27 @@ fun BookGroupItemList(
|
|||||||
onClick: () -> Unit,
|
onClick: () -> Unit,
|
||||||
onLongClick: (() -> Unit)?
|
onLongClick: (() -> Unit)?
|
||||||
) {
|
) {
|
||||||
|
val firstBookName = previewBooks.firstOrNull()?.name
|
||||||
|
val primaryColor = MaterialTheme.colorScheme.primary
|
||||||
|
val descAnnotated = if (firstBookName != null) {
|
||||||
|
buildAnnotatedString {
|
||||||
|
append("最近阅读:")
|
||||||
|
withStyle(SpanStyle(color = primaryColor, fontWeight = FontWeight.Medium)) {
|
||||||
|
append("《$firstBookName》")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
BookshelfItem(
|
BookshelfItem(
|
||||||
isGrid = false,
|
isGrid = false,
|
||||||
gridStyle = 0,
|
gridStyle = 0,
|
||||||
isCompact = isCompact,
|
isCompact = isCompact,
|
||||||
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||||
title = group.groupName,
|
title = group.groupName,
|
||||||
|
titleColor = primaryColor,
|
||||||
subTitle = countText,
|
subTitle = countText,
|
||||||
|
descAnnotated = descAnnotated,
|
||||||
titleSmallFont = titleSmallFont,
|
titleSmallFont = titleSmallFont,
|
||||||
titleCenter = titleCenter,
|
titleCenter = titleCenter,
|
||||||
titleMaxLines = titleMaxLines,
|
titleMaxLines = titleMaxLines,
|
||||||
@@ -485,6 +567,52 @@ fun BookItem(
|
|||||||
book.author
|
book.author
|
||||||
},
|
},
|
||||||
desc = book.durChapterTitle ?: "",
|
desc = book.durChapterTitle ?: "",
|
||||||
|
bottomContent = if (layoutMode == 0 && BookshelfConfig.showBookIntro) {
|
||||||
|
{
|
||||||
|
val kindList = book.kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }
|
||||||
|
val intro = book.intro?.takeIf { it.isNotBlank() }
|
||||||
|
val customTagColors = if (ThemeConfig.enableCustomTagColors) ThemeConfig.getCustomTagColors() else emptyList()
|
||||||
|
if (!kindList.isNullOrEmpty()) {
|
||||||
|
FlowRow(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 4.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
|
) {
|
||||||
|
kindList.forEachIndexed { index, label ->
|
||||||
|
val colorPair = if (customTagColors.isNotEmpty()) {
|
||||||
|
customTagColors[index % customTagColors.size]
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
TextCard(
|
||||||
|
text = label,
|
||||||
|
backgroundColor = if (colorPair != null && colorPair.bgColor != 0) Color(colorPair.bgColor) else LegadoTheme.colorScheme.surfaceContainerHighest,
|
||||||
|
contentColor = if (colorPair != null && colorPair.textColor != 0) Color(colorPair.textColor) else LegadoTheme.colorScheme.primary,
|
||||||
|
cornerRadius = 4.dp,
|
||||||
|
horizontalPadding = 6.dp,
|
||||||
|
verticalPadding = 2.dp,
|
||||||
|
textStyle = LegadoTheme.typography.labelSmall
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (intro != null) {
|
||||||
|
val maxLines = if (BookshelfConfig.bookshelfIntroMaxLines == 0) Int.MAX_VALUE else BookshelfConfig.bookshelfIntroMaxLines
|
||||||
|
AppText(
|
||||||
|
text = intro,
|
||||||
|
style = LegadoTheme.typography.bodySmall,
|
||||||
|
color = LegadoTheme.colorScheme.onSurfaceVariant,
|
||||||
|
maxLines = maxLines,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(horizontal = 4.dp, vertical = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else null,
|
||||||
extra = {
|
extra = {
|
||||||
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
|
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
|
||||||
AppText(
|
AppText(
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ data class BookShelfItem(
|
|||||||
val type: Int,
|
val type: Int,
|
||||||
val group: Long,
|
val group: Long,
|
||||||
val order: Int,
|
val order: Int,
|
||||||
val canUpdate: Boolean = true
|
val canUpdate: Boolean = true,
|
||||||
|
val intro: String? = null,
|
||||||
|
val kind: String? = null
|
||||||
) {
|
) {
|
||||||
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
|
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ import androidx.compose.foundation.verticalScroll
|
|||||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalConfiguration
|
import androidx.compose.ui.platform.LocalConfiguration
|
||||||
import androidx.compose.ui.res.stringArrayResource
|
import androidx.compose.ui.res.stringArrayResource
|
||||||
@@ -19,8 +23,11 @@ import androidx.compose.ui.res.stringResource
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
|
import io.legado.app.ui.config.themeConfig.LabelColorManageSheet
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
import io.legado.app.ui.widget.components.modalBottomSheet.AppModalBottomSheet
|
||||||
|
import io.legado.app.ui.widget.components.settingItem.CompactClickableSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
import io.legado.app.ui.widget.components.settingItem.CompactDropdownSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem
|
import io.legado.app.ui.widget.components.settingItem.CompactSliderSettingItem
|
||||||
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
||||||
@@ -33,6 +40,7 @@ fun BookshelfConfigSheet(
|
|||||||
) {
|
) {
|
||||||
val configuration = LocalConfiguration.current
|
val configuration = LocalConfiguration.current
|
||||||
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
var showLabelColorManage by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
AppModalBottomSheet(
|
AppModalBottomSheet(
|
||||||
title = stringResource(R.string.bookshelf_layout),
|
title = stringResource(R.string.bookshelf_layout),
|
||||||
@@ -234,6 +242,38 @@ fun BookshelfConfigSheet(
|
|||||||
onCheckedChange = { BookshelfConfig.showLastUpdateTime = it }
|
onCheckedChange = { BookshelfConfig.showLastUpdateTime = it }
|
||||||
)
|
)
|
||||||
|
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "列表模式显示更多",
|
||||||
|
checked = BookshelfConfig.showBookIntro,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { BookshelfConfig.showBookIntro = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = BookshelfConfig.showBookIntro) {
|
||||||
|
CompactSliderSettingItem(
|
||||||
|
title = "简介行数",
|
||||||
|
description = if (BookshelfConfig.bookshelfIntroMaxLines == 0) "显示全部简介" else "显示 ${BookshelfConfig.bookshelfIntroMaxLines} 行简介",
|
||||||
|
value = BookshelfConfig.bookshelfIntroMaxLines.toFloat(),
|
||||||
|
valueRange = 0f..10f,
|
||||||
|
steps = 10,
|
||||||
|
onValueChange = { BookshelfConfig.bookshelfIntroMaxLines = it.toInt() }
|
||||||
|
)
|
||||||
|
CompactSwitchSettingItem(
|
||||||
|
title = "自定义标签颜色",
|
||||||
|
checked = ThemeConfig.enableCustomTagColors,
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onCheckedChange = { ThemeConfig.enableCustomTagColors = it }
|
||||||
|
)
|
||||||
|
|
||||||
|
AnimatedVisibility(visible = ThemeConfig.enableCustomTagColors) {
|
||||||
|
CompactClickableSettingItem(
|
||||||
|
title = "管理标签颜色",
|
||||||
|
color = MaterialTheme.colorScheme.surface,
|
||||||
|
onClick = { showLabelColorManage = true }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CompactSwitchSettingItem(
|
CompactSwitchSettingItem(
|
||||||
title = stringResource(R.string.show_wait_up_count),
|
title = stringResource(R.string.show_wait_up_count),
|
||||||
checked = BookshelfConfig.showWaitUpCount,
|
checked = BookshelfConfig.showWaitUpCount,
|
||||||
@@ -265,5 +305,10 @@ fun BookshelfConfigSheet(
|
|||||||
onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
|
onValueChange = { BookshelfConfig.bookshelfRefreshingLimit = it.toInt() }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
LabelColorManageSheet(
|
||||||
|
show = showLabelColorManage,
|
||||||
|
onDismissRequest = { showLabelColorManage = false }
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -76,8 +76,8 @@ import androidx.compose.ui.window.Dialog
|
|||||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseRuleEvent
|
import io.legado.app.base.BaseRuleEvent
|
||||||
import io.legado.app.data.entities.BookGroup
|
|
||||||
import io.legado.app.ui.about.AppLogSheet
|
import io.legado.app.ui.about.AppLogSheet
|
||||||
|
import io.legado.app.data.entities.BookGroup
|
||||||
import io.legado.app.ui.book.info.GroupSelectSheet
|
import io.legado.app.ui.book.info.GroupSelectSheet
|
||||||
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
import io.legado.app.ui.config.bookshelfConfig.BookshelfConfig
|
||||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||||
@@ -212,17 +212,6 @@ fun BookshelfScreen(
|
|||||||
val latestGroups by rememberUpdatedState(uiState.groups)
|
val latestGroups by rememberUpdatedState(uiState.groups)
|
||||||
val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId)
|
val latestSelectedGroupId by rememberUpdatedState(uiState.selectedGroupId)
|
||||||
|
|
||||||
LaunchedEffect(uiState.groups, uiState.isSearch) {
|
|
||||||
if (!uiState.isSearch && uiState.groups.isNotEmpty()) {
|
|
||||||
val savedGroupId = BookshelfConfig.saveTabPosition
|
|
||||||
val savedGroupIndex = uiState.groups.indexOfFirst { it.groupId == savedGroupId }
|
|
||||||
if (savedGroupIndex >= 0 && savedGroupIndex != pagerState.currentPage) {
|
|
||||||
viewModel.changeGroup(savedGroupId)
|
|
||||||
pagerState.scrollToPage(savedGroupIndex)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
LaunchedEffect(pagerState) {
|
LaunchedEffect(pagerState) {
|
||||||
snapshotFlow { pagerState.settledPage }
|
snapshotFlow { pagerState.settledPage }
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
@@ -709,10 +698,6 @@ fun BookshelfScreen(
|
|||||||
paddingValues = paddingValues,
|
paddingValues = paddingValues,
|
||||||
books = uiState.items,
|
books = uiState.items,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
bookshelfLayoutMode = bookshelfLayoutMode,
|
|
||||||
bookshelfLayoutGrid = bookshelfLayoutGrid,
|
|
||||||
bookshelfLayoutList = bookshelfLayoutList,
|
|
||||||
isEditMode = isEditMode,
|
|
||||||
selectedBookUrls = selectedBookUrls,
|
selectedBookUrls = selectedBookUrls,
|
||||||
canReorderBooks = false,
|
canReorderBooks = false,
|
||||||
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
||||||
@@ -725,6 +710,7 @@ fun BookshelfScreen(
|
|||||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||||
onBookClick = onBookClick,
|
onBookClick = onBookClick,
|
||||||
onBookLongClick = onBookLongClick,
|
onBookLongClick = onBookLongClick,
|
||||||
|
isCurrentPage = true,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
)
|
)
|
||||||
@@ -732,17 +718,15 @@ fun BookshelfScreen(
|
|||||||
HorizontalPager(
|
HorizontalPager(
|
||||||
state = pagerState,
|
state = pagerState,
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = Modifier.fillMaxSize(),
|
||||||
beyondViewportPageCount = 1,
|
beyondViewportPageCount = 2,
|
||||||
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
|
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
|
||||||
) { pageIndex ->
|
) { pageIndex ->
|
||||||
val group = uiState.groups.getOrNull(pageIndex)
|
val group = uiState.groups.getOrNull(pageIndex)
|
||||||
if (group != null) {
|
if (group != null) {
|
||||||
val isSelectedGroup = group.groupId == uiState.selectedGroupId
|
val isSelectedGroup = group.groupId == uiState.selectedGroupId
|
||||||
val books = if (isSelectedGroup) {
|
val books = uiState.allGroupBooks[group.groupId]
|
||||||
uiState.items
|
?: if (isSelectedGroup) uiState.items
|
||||||
} else {
|
else emptyList()
|
||||||
emptyList()
|
|
||||||
}
|
|
||||||
val canReorderBooks = isEditMode &&
|
val canReorderBooks = isEditMode &&
|
||||||
!uiState.isSearch &&
|
!uiState.isSearch &&
|
||||||
(group.bookSort.takeIf { it >= 0 }
|
(group.bookSort.takeIf { it >= 0 }
|
||||||
@@ -752,10 +736,6 @@ fun BookshelfScreen(
|
|||||||
paddingValues = paddingValues,
|
paddingValues = paddingValues,
|
||||||
books = books,
|
books = books,
|
||||||
uiState = uiState,
|
uiState = uiState,
|
||||||
bookshelfLayoutMode = bookshelfLayoutMode,
|
|
||||||
bookshelfLayoutGrid = bookshelfLayoutGrid,
|
|
||||||
bookshelfLayoutList = bookshelfLayoutList,
|
|
||||||
isEditMode = isEditMode,
|
|
||||||
selectedBookUrls = selectedBookUrls,
|
selectedBookUrls = selectedBookUrls,
|
||||||
canReorderBooks = canReorderBooks,
|
canReorderBooks = canReorderBooks,
|
||||||
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
||||||
@@ -788,6 +768,7 @@ fun BookshelfScreen(
|
|||||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||||
onBookClick = onBookClick,
|
onBookClick = onBookClick,
|
||||||
onBookLongClick = onBookLongClick,
|
onBookLongClick = onBookLongClick,
|
||||||
|
isCurrentPage = isSelectedGroup,
|
||||||
sharedTransitionScope = sharedTransitionScope,
|
sharedTransitionScope = sharedTransitionScope,
|
||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
)
|
)
|
||||||
@@ -1022,10 +1003,6 @@ fun BookshelfPage(
|
|||||||
paddingValues: PaddingValues,
|
paddingValues: PaddingValues,
|
||||||
books: List<BookShelfItem>,
|
books: List<BookShelfItem>,
|
||||||
uiState: BookshelfUiState,
|
uiState: BookshelfUiState,
|
||||||
bookshelfLayoutMode: Int,
|
|
||||||
bookshelfLayoutGrid: Int,
|
|
||||||
bookshelfLayoutList: Int,
|
|
||||||
isEditMode: Boolean,
|
|
||||||
selectedBookUrls: Set<String>,
|
selectedBookUrls: Set<String>,
|
||||||
canReorderBooks: Boolean,
|
canReorderBooks: Boolean,
|
||||||
onToggleBookSelection: (BookShelfItem) -> Unit,
|
onToggleBookSelection: (BookShelfItem) -> Unit,
|
||||||
@@ -1038,10 +1015,12 @@ fun BookshelfPage(
|
|||||||
onGlobalSearch: () -> Unit,
|
onGlobalSearch: () -> Unit,
|
||||||
onBookClick: (BookShelfItem) -> Unit,
|
onBookClick: (BookShelfItem) -> Unit,
|
||||||
onBookLongClick: (BookShelfItem) -> Unit,
|
onBookLongClick: (BookShelfItem) -> Unit,
|
||||||
|
isCurrentPage: Boolean = true,
|
||||||
sharedTransitionScope: SharedTransitionScope? = null,
|
sharedTransitionScope: SharedTransitionScope? = null,
|
||||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||||
) {
|
) {
|
||||||
if (books.isEmpty()) {
|
if (books.isEmpty()) {
|
||||||
|
if (!isCurrentPage) return
|
||||||
if (uiState.isSearch) {
|
if (uiState.isSearch) {
|
||||||
EmptyMessage(
|
EmptyMessage(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
@@ -1068,6 +1047,14 @@ fun BookshelfPage(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val configuration = LocalConfiguration.current
|
||||||
|
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||||
|
val bookshelfLayoutMode =
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutModeLandscape else BookshelfConfig.bookshelfLayoutModePortrait
|
||||||
|
val bookshelfLayoutGrid =
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutGridLandscape else BookshelfConfig.bookshelfLayoutGridPortrait
|
||||||
|
val bookshelfLayoutList =
|
||||||
|
if (isLandscape) BookshelfConfig.bookshelfLayoutListLandscape else BookshelfConfig.bookshelfLayoutListPortrait
|
||||||
val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
val columns = if (bookshelfLayoutMode == 0) bookshelfLayoutList else bookshelfLayoutGrid
|
||||||
val isGridMode = bookshelfLayoutMode != 0
|
val isGridMode = bookshelfLayoutMode != 0
|
||||||
val totalHorizontalPadding =
|
val totalHorizontalPadding =
|
||||||
@@ -1148,7 +1135,7 @@ fun BookshelfPage(
|
|||||||
animatedVisibilityScope = animatedVisibilityScope,
|
animatedVisibilityScope = animatedVisibilityScope,
|
||||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
|
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
|
||||||
onClick = {
|
onClick = {
|
||||||
if (isEditMode) {
|
if (uiState.isEditMode) {
|
||||||
onToggleBookSelection(book)
|
onToggleBookSelection(book)
|
||||||
} else {
|
} else {
|
||||||
onBookClick(book)
|
onBookClick(book)
|
||||||
@@ -1158,7 +1145,7 @@ fun BookshelfPage(
|
|||||||
null
|
null
|
||||||
} else {
|
} else {
|
||||||
{
|
{
|
||||||
if (isEditMode) {
|
if (uiState.isEditMode) {
|
||||||
onToggleBookSelection(book)
|
onToggleBookSelection(book)
|
||||||
} else {
|
} else {
|
||||||
onBookLongClick(book)
|
onBookLongClick(book)
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.main.bookshelf
|
package io.legado.app.ui.main.bookshelf
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Stable
|
||||||
import io.legado.app.data.entities.BookGroup
|
import io.legado.app.data.entities.BookGroup
|
||||||
import io.legado.app.ui.widget.components.list.ListUiState
|
import io.legado.app.ui.widget.components.list.ListUiState
|
||||||
import kotlinx.collections.immutable.ImmutableList
|
import kotlinx.collections.immutable.ImmutableList
|
||||||
@@ -9,8 +10,9 @@ import kotlinx.collections.immutable.persistentListOf
|
|||||||
import kotlinx.collections.immutable.persistentMapOf
|
import kotlinx.collections.immutable.persistentMapOf
|
||||||
import kotlinx.collections.immutable.persistentSetOf
|
import kotlinx.collections.immutable.persistentSetOf
|
||||||
|
|
||||||
|
@Stable
|
||||||
data class BookshelfGroupSelectorState(
|
data class BookshelfGroupSelectorState(
|
||||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
val groups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||||
val selectedGroupIndex: Int = 0,
|
val selectedGroupIndex: Int = 0,
|
||||||
val selectedGroupId: Long = BookGroup.IdAll
|
val selectedGroupId: Long = BookGroup.IdAll
|
||||||
)
|
)
|
||||||
@@ -27,14 +29,15 @@ sealed interface BookshelfOverlay {
|
|||||||
data object BatchDownloadConfirmDialog : BookshelfOverlay
|
data object BatchDownloadConfirmDialog : BookshelfOverlay
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Stable
|
||||||
data class BookshelfUiState(
|
data class BookshelfUiState(
|
||||||
override val items: ImmutableList<BookShelfItem> = persistentListOf(),
|
override val items: ImmutableList<BookShelfItem> = persistentListOf(),
|
||||||
override val selectedIds: ImmutableSet<Any> = persistentSetOf(),
|
override val selectedIds: ImmutableSet<Any> = persistentSetOf(),
|
||||||
override val searchKey: String = "",
|
override val searchKey: String = "",
|
||||||
override val isSearch: Boolean = false,
|
override val isSearch: Boolean = false,
|
||||||
override val isLoading: Boolean = false,
|
override val isLoading: Boolean = false,
|
||||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
val groups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||||
val allGroups: ImmutableList<BookGroup> = persistentListOf(),
|
val allGroups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||||
val groupPreviews: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf(),
|
val groupPreviews: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf(),
|
||||||
val groupBookCounts: ImmutableMap<Long, Int> = persistentMapOf(),
|
val groupBookCounts: ImmutableMap<Long, Int> = persistentMapOf(),
|
||||||
val currentGroupBookCount: Int = 0,
|
val currentGroupBookCount: Int = 0,
|
||||||
@@ -56,5 +59,6 @@ data class BookshelfUiState(
|
|||||||
val subtitle: String? = null,
|
val subtitle: String? = null,
|
||||||
val currentGroupName: String? = null,
|
val currentGroupName: String? = null,
|
||||||
val draggingBooks: ImmutableList<BookShelfItem>? = null,
|
val draggingBooks: ImmutableList<BookShelfItem>? = null,
|
||||||
val pendingSavedBooks: ImmutableList<BookShelfItem>? = null
|
val pendingSavedBooks: ImmutableList<BookShelfItem>? = null,
|
||||||
|
val allGroupBooks: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf()
|
||||||
) : ListUiState<BookShelfItem>
|
) : ListUiState<BookShelfItem>
|
||||||
@@ -73,10 +73,13 @@ import kotlinx.coroutines.flow.SharingStarted
|
|||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.collect
|
import kotlinx.coroutines.flow.collect
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.combine
|
import kotlinx.coroutines.flow.combine
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flowOf
|
||||||
import kotlinx.coroutines.flow.flatMapLatest
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
import kotlinx.coroutines.flow.flow
|
import kotlinx.coroutines.flow.flow
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
import kotlinx.coroutines.flow.stateIn
|
import kotlinx.coroutines.flow.stateIn
|
||||||
@@ -158,10 +161,6 @@ class BookshelfViewModel(
|
|||||||
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
|
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
|
||||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||||
|
|
||||||
private val allBooksFlow = appDb.bookDao.flowBookShelf()
|
|
||||||
.distinctUntilChanged()
|
|
||||||
.flowOn(Dispatchers.Default)
|
|
||||||
|
|
||||||
private data class GroupPreviewState(
|
private data class GroupPreviewState(
|
||||||
val previews: ImmutableMap<Long, ImmutableList<BookShelfItem>>,
|
val previews: ImmutableMap<Long, ImmutableList<BookShelfItem>>,
|
||||||
val counts: ImmutableMap<Long, Int>,
|
val counts: ImmutableMap<Long, Int>,
|
||||||
@@ -173,7 +172,7 @@ class BookshelfViewModel(
|
|||||||
groupIdFlow
|
groupIdFlow
|
||||||
) { groups, selectedGroupId ->
|
) { groups, selectedGroupId ->
|
||||||
BookshelfGroupSelectorState(
|
BookshelfGroupSelectorState(
|
||||||
groups = groups.toImmutableList(),
|
groups = groups.map { it.toBookGroupUi() }.toImmutableList(),
|
||||||
selectedGroupIndex = groups.indexOfFirst { it.groupId == selectedGroupId }
|
selectedGroupIndex = groups.indexOfFirst { it.groupId == selectedGroupId }
|
||||||
.coerceAtLeast(0),
|
.coerceAtLeast(0),
|
||||||
selectedGroupId = selectedGroupId
|
selectedGroupId = selectedGroupId
|
||||||
@@ -197,6 +196,25 @@ class BookshelfViewModel(
|
|||||||
}
|
}
|
||||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
|
private val allGroupBooksFlow: StateFlow<Map<Long, List<BookShelfItem>>> = combine(
|
||||||
|
groupsFlow, sortConfigFlow
|
||||||
|
) { groups, sortConfig ->
|
||||||
|
groups to sortConfig
|
||||||
|
}.flatMapLatest { (groups, sortConfig) ->
|
||||||
|
if (groups.isEmpty()) {
|
||||||
|
flowOf(emptyMap())
|
||||||
|
} else {
|
||||||
|
val flows = groups.map { group ->
|
||||||
|
appDb.bookDao.flowBookShelfByGroup(group.groupId).map { books ->
|
||||||
|
group.groupId to sortBooks(books, group, sortConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
combine(flows) { it.toMap() }
|
||||||
|
}
|
||||||
|
}.flowOn(Dispatchers.Default)
|
||||||
|
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyMap())
|
||||||
|
|
||||||
private val visibleBooksFlow = combine(
|
private val visibleBooksFlow = combine(
|
||||||
booksFlow,
|
booksFlow,
|
||||||
searchKeyFlow,
|
searchKeyFlow,
|
||||||
@@ -213,14 +231,44 @@ class BookshelfViewModel(
|
|||||||
selectedBookUrls.intersect(visibleBookUrls)
|
selectedBookUrls.intersect(visibleBookUrls)
|
||||||
}.distinctUntilChanged()
|
}.distinctUntilChanged()
|
||||||
|
|
||||||
private val groupPreviewsFlow =
|
@OptIn(ExperimentalCoroutinesApi::class)
|
||||||
combine(
|
private val groupPreviewsFlow = combine(
|
||||||
groupsFlow,
|
groupsFlow,
|
||||||
allBooksFlow,
|
|
||||||
bookGroupStyleFlow,
|
bookGroupStyleFlow,
|
||||||
sortConfigFlow
|
appDb.bookDao.flowSystemGroupCounts()
|
||||||
) { groups, allBooks, bookGroupStyle, sortConfig ->
|
) { groups, bookGroupStyle, systemCounts ->
|
||||||
buildGroupPreviewState(groups, allBooks, bookGroupStyle, sortConfig)
|
Triple(groups, bookGroupStyle, systemCounts.associate { it.groupId to it.count })
|
||||||
|
}.flatMapLatest { (groups, bookGroupStyle, systemCountsMap) ->
|
||||||
|
if (bookGroupStyle !in 2..3) {
|
||||||
|
appDb.bookDao.flowAllBookShelfCount().map { count ->
|
||||||
|
GroupPreviewState(persistentMapOf(), persistentMapOf(), count)
|
||||||
|
}
|
||||||
|
} else if (groups.isEmpty()) {
|
||||||
|
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), 0))
|
||||||
|
} else {
|
||||||
|
val groupFlows = groups.map { group ->
|
||||||
|
val countFlow: Flow<Int> = if (group.groupId > 0) {
|
||||||
|
appDb.bookDao.flowUserGroupBookCount(group.groupId)
|
||||||
|
} else {
|
||||||
|
flowOf(systemCountsMap[group.groupId] ?: 0)
|
||||||
|
}
|
||||||
|
val previewFlow = appDb.bookDao.flowGroupPreview(group.groupId)
|
||||||
|
combine(countFlow, previewFlow) { count, preview ->
|
||||||
|
Triple(group.groupId, count, preview)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
combine(groupFlows) { results ->
|
||||||
|
var previews = persistentMapOf<Long, ImmutableList<BookShelfItem>>()
|
||||||
|
var counts = persistentMapOf<Long, Int>()
|
||||||
|
var allBookCount = 0
|
||||||
|
results.forEach { (groupId, count, preview) ->
|
||||||
|
counts = counts.put(groupId, count)
|
||||||
|
previews = previews.put(groupId, preview.toImmutableList())
|
||||||
|
if (groupId == BookGroup.IdAll) allBookCount = count
|
||||||
|
}
|
||||||
|
GroupPreviewState(previews, counts, allBookCount)
|
||||||
|
}
|
||||||
|
}
|
||||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||||
|
|
||||||
private val coreInternalStateFlow = combine(
|
private val coreInternalStateFlow = combine(
|
||||||
@@ -309,16 +357,28 @@ class BookshelfViewModel(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private val dataStateFlow = combine(
|
private val dataStateFlow = combine(
|
||||||
|
combine(
|
||||||
booksFlow,
|
booksFlow,
|
||||||
groupsFlow,
|
groupsFlow,
|
||||||
allGroupsFlow,
|
allGroupsFlow,
|
||||||
groupPreviewsFlow,
|
groupPreviewsFlow,
|
||||||
internalStateFlow
|
internalStateFlow
|
||||||
) { books, groups, allGroups, previews, internal ->
|
) { books, groups, allGroups, previews, internal ->
|
||||||
BookshelfDataState(books, groups, allGroups, previews, internal)
|
BookshelfDataCore(books, groups, allGroups, previews, internal)
|
||||||
|
},
|
||||||
|
allGroupBooksFlow
|
||||||
|
) { core, allGroupBooks ->
|
||||||
|
BookshelfDataState(
|
||||||
|
books = core.books,
|
||||||
|
groups = core.groups.map { it.toBookGroupUi() },
|
||||||
|
allGroups = core.allGroups.map { it.toBookGroupUi() },
|
||||||
|
previews = core.previews,
|
||||||
|
internal = core.internal,
|
||||||
|
allGroupBooks = allGroupBooks
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private data class BookshelfDataState(
|
private data class BookshelfDataCore(
|
||||||
val books: List<BookShelfItem>,
|
val books: List<BookShelfItem>,
|
||||||
val groups: List<BookGroup>,
|
val groups: List<BookGroup>,
|
||||||
val allGroups: List<BookGroup>,
|
val allGroups: List<BookGroup>,
|
||||||
@@ -326,6 +386,15 @@ class BookshelfViewModel(
|
|||||||
val internal: InternalState
|
val internal: InternalState
|
||||||
)
|
)
|
||||||
|
|
||||||
|
private data class BookshelfDataState(
|
||||||
|
val books: List<BookShelfItem>,
|
||||||
|
val groups: List<BookGroupUi>,
|
||||||
|
val allGroups: List<BookGroupUi>,
|
||||||
|
val previews: GroupPreviewState,
|
||||||
|
val internal: InternalState,
|
||||||
|
val allGroupBooks: Map<Long, List<BookShelfItem>>
|
||||||
|
)
|
||||||
|
|
||||||
val uiState: StateFlow<BookshelfUiState> = combine(
|
val uiState: StateFlow<BookshelfUiState> = combine(
|
||||||
dataStateFlow,
|
dataStateFlow,
|
||||||
interactionStateFlow
|
interactionStateFlow
|
||||||
@@ -383,7 +452,8 @@ class BookshelfViewModel(
|
|||||||
},
|
},
|
||||||
currentGroupName = currentGroupName,
|
currentGroupName = currentGroupName,
|
||||||
draggingBooks = interaction.draggingBooks?.toImmutableList(),
|
draggingBooks = interaction.draggingBooks?.toImmutableList(),
|
||||||
pendingSavedBooks = interaction.pendingSavedBooks?.toImmutableList()
|
pendingSavedBooks = interaction.pendingSavedBooks?.toImmutableList(),
|
||||||
|
allGroupBooks = data.allGroupBooks.mapValues { it.value.toImmutableList() }.toImmutableMap()
|
||||||
)
|
)
|
||||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfUiState())
|
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfUiState())
|
||||||
|
|
||||||
@@ -495,93 +565,6 @@ class BookshelfViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildGroupPreviewState(
|
|
||||||
groups: List<BookGroup>,
|
|
||||||
allBooks: List<BookShelfItem>,
|
|
||||||
bookGroupStyle: Int,
|
|
||||||
sortConfig: BookshelfSortConfig
|
|
||||||
): GroupPreviewState {
|
|
||||||
if (bookGroupStyle !in 2..3) {
|
|
||||||
return GroupPreviewState(persistentMapOf(), persistentMapOf(), allBooks.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
val buckets = HashMap<Long, MutableList<BookShelfItem>>(groups.size)
|
|
||||||
groups.forEach { group ->
|
|
||||||
buckets[group.groupId] = ArrayList()
|
|
||||||
}
|
|
||||||
|
|
||||||
val userGroups = groups.filter { it.groupId > 0 }
|
|
||||||
val sumUserGroupIds = userGroups.sumOf { it.groupId }
|
|
||||||
|
|
||||||
fun add(groupId: Long, book: BookShelfItem) {
|
|
||||||
buckets[groupId]?.add(book)
|
|
||||||
}
|
|
||||||
|
|
||||||
allBooks.forEach { book ->
|
|
||||||
add(BookGroup.IdAll, book)
|
|
||||||
if (book.isRootGroupBook(sumUserGroupIds)) add(BookGroup.IdRoot, book)
|
|
||||||
if (book.isLocal) add(BookGroup.IdLocal, book)
|
|
||||||
if (book.isAudio) add(BookGroup.IdAudio, book)
|
|
||||||
if (book.isNetNoneGroupBook(sumUserGroupIds)) add(BookGroup.IdNetNone, book)
|
|
||||||
if (book.isLocalNoneGroupBook(sumUserGroupIds)) add(BookGroup.IdLocalNone, book)
|
|
||||||
if (book.isImage) add(BookGroup.IdManga, book)
|
|
||||||
if ((book.type and BookType.text) > 0) add(BookGroup.IdText, book)
|
|
||||||
if ((book.type and BookType.updateError) > 0) add(BookGroup.IdError, book)
|
|
||||||
if (book.durChapterIndex == 0 && book.durChapterPos == 0) {
|
|
||||||
add(BookGroup.IdUnread, book)
|
|
||||||
}
|
|
||||||
if (book.totalChapterNum > 0 &&
|
|
||||||
book.durChapterIndex > 0 &&
|
|
||||||
book.durChapterIndex < book.totalChapterNum - 1
|
|
||||||
) {
|
|
||||||
add(BookGroup.IdReading, book)
|
|
||||||
}
|
|
||||||
if (book.totalChapterNum > 0 && book.durChapterIndex >= book.totalChapterNum - 1) {
|
|
||||||
add(BookGroup.IdReadFinished, book)
|
|
||||||
}
|
|
||||||
userGroups.forEach { group ->
|
|
||||||
if ((book.group and group.groupId) != 0L) {
|
|
||||||
add(group.groupId, book)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val previews = HashMap<Long, ImmutableList<BookShelfItem>>(groups.size)
|
|
||||||
val counts = HashMap<Long, Int>(groups.size)
|
|
||||||
groups.forEach { group ->
|
|
||||||
val groupBooks = buckets[group.groupId].orEmpty()
|
|
||||||
counts[group.groupId] = groupBooks.size
|
|
||||||
previews[group.groupId] = buildGroupPreview(sortBooks(groupBooks, group, sortConfig))
|
|
||||||
}
|
|
||||||
return GroupPreviewState(previews.toImmutableMap(), counts.toImmutableMap(), allBooks.size)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BookShelfItem.isRootGroupBook(sumUserGroupIds: Long): Boolean {
|
|
||||||
return (type and BookType.text) > 0 &&
|
|
||||||
(type and BookType.local) == 0 &&
|
|
||||||
(sumUserGroupIds and group) == 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BookShelfItem.isNetNoneGroupBook(sumUserGroupIds: Long): Boolean {
|
|
||||||
return (type and BookType.audio) == 0 &&
|
|
||||||
(type and BookType.local) == 0 &&
|
|
||||||
(sumUserGroupIds and group) == 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun BookShelfItem.isLocalNoneGroupBook(sumUserGroupIds: Long): Boolean {
|
|
||||||
return (type and BookType.local) > 0 &&
|
|
||||||
(sumUserGroupIds and group) == 0L
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun buildGroupPreview(sortedBooks: List<BookShelfItem>): ImmutableList<BookShelfItem> {
|
|
||||||
val booksWithCover = sortedBooks.filter { it.getDisplayCover() != null }
|
|
||||||
return if (booksWithCover.size >= 4) {
|
|
||||||
booksWithCover.take(4)
|
|
||||||
} else {
|
|
||||||
(booksWithCover + sortedBooks.filter { it.getDisplayCover() == null }).take(4)
|
|
||||||
}.toImmutableList()
|
|
||||||
}
|
|
||||||
|
|
||||||
fun changeGroup(groupId: Long) {
|
fun changeGroup(groupId: Long) {
|
||||||
if (groupIdFlow.value != groupId) {
|
if (groupIdFlow.value != groupId) {
|
||||||
groupIdFlow.value = groupId
|
groupIdFlow.value = groupId
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import androidx.compose.animation.animateColorAsState
|
|||||||
import androidx.compose.animation.core.FastOutSlowInEasing
|
import androidx.compose.animation.core.FastOutSlowInEasing
|
||||||
import androidx.compose.animation.core.animateFloatAsState
|
import androidx.compose.animation.core.animateFloatAsState
|
||||||
import androidx.compose.animation.core.tween
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
@@ -57,6 +58,7 @@ import io.legado.app.ui.book.search.SearchActivity
|
|||||||
import io.legado.app.ui.book.search.SearchScope
|
import io.legado.app.ui.book.search.SearchScope
|
||||||
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
import io.legado.app.ui.book.source.edit.BookSourceEditActivity
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
@@ -286,12 +288,22 @@ fun ExploreScreen(
|
|||||||
.align(Alignment.TopStart)
|
.align(Alignment.TopStart)
|
||||||
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp)
|
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp)
|
||||||
) { item ->
|
) { item ->
|
||||||
|
val enableBorder = ThemeConfig.enableDeepPersonalization && ThemeConfig.enableContainerBorder
|
||||||
|
val borderWidth = ThemeConfig.containerBorderWidth.dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.outline
|
||||||
|
}
|
||||||
TextCard(
|
TextCard(
|
||||||
text = item.bookSourceName,
|
text = item.bookSourceName,
|
||||||
textStyle = LegadoTheme.typography.labelMediumEmphasized,
|
textStyle = LegadoTheme.typography.labelMediumEmphasized,
|
||||||
cornerRadius = 12.dp,
|
cornerRadius = 12.dp,
|
||||||
horizontalPadding = 12.dp,
|
horizontalPadding = 12.dp,
|
||||||
verticalPadding = 8.dp,
|
verticalPadding = 8.dp,
|
||||||
|
border = if (enableBorder) {
|
||||||
|
BorderStroke(borderWidth, borderColor)
|
||||||
|
} else null,
|
||||||
onClick = {
|
onClick = {
|
||||||
scope.launch {
|
scope.launch {
|
||||||
val index =
|
val index =
|
||||||
@@ -362,12 +374,23 @@ fun ExploreSourceHeader(
|
|||||||
label = "CardColor"
|
label = "CardColor"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
val enableBorder = ThemeConfig.enableDeepPersonalization && ThemeConfig.enableContainerBorder
|
||||||
|
val borderWidth = ThemeConfig.containerBorderWidth.dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
|
||||||
GlassCard(
|
GlassCard(
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.padding(vertical = 4.dp),
|
.padding(vertical = 4.dp),
|
||||||
cornerRadius = 12.dp,
|
cornerRadius = 12.dp,
|
||||||
containerColor = containerColor
|
containerColor = containerColor,
|
||||||
|
border = if (enableBorder) {
|
||||||
|
BorderStroke(borderWidth, borderColor)
|
||||||
|
} else null
|
||||||
) {
|
) {
|
||||||
ListItem(
|
ListItem(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.ui.rss.article
|
package io.legado.app.ui.rss.article
|
||||||
|
|
||||||
import androidx.compose.foundation.background
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.border
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
import androidx.compose.foundation.layout.Arrangement
|
import androidx.compose.foundation.layout.Arrangement
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
@@ -64,6 +65,9 @@ import io.legado.app.ui.widget.components.EmptyMessage
|
|||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
import io.legado.app.ui.widget.components.cover.buildCoverImageRequest
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
import kotlinx.coroutines.Dispatchers.IO
|
import kotlinx.coroutines.Dispatchers.IO
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.flowOn
|
import kotlinx.coroutines.flow.flowOn
|
||||||
@@ -92,6 +96,7 @@ fun RssArticlesPage(
|
|||||||
rssUrl: String?,
|
rssUrl: String?,
|
||||||
rssSource: RssSource?,
|
rssSource: RssSource?,
|
||||||
viewModel: RssArticlesViewModel,
|
viewModel: RssArticlesViewModel,
|
||||||
|
searchKey: String? = null,
|
||||||
onRead: (RssArticle) -> Unit,
|
onRead: (RssArticle) -> Unit,
|
||||||
paddingValues: PaddingValues = PaddingValues(0.dp),
|
paddingValues: PaddingValues = PaddingValues(0.dp),
|
||||||
modifier: Modifier = Modifier
|
modifier: Modifier = Modifier
|
||||||
@@ -99,9 +104,10 @@ fun RssArticlesPage(
|
|||||||
val context = LocalContext.current
|
val context = LocalContext.current
|
||||||
val layout = remember(articleStyle) { articleStyle.toRssArticleLayout() }
|
val layout = remember(articleStyle) { articleStyle.toRssArticleLayout() }
|
||||||
val loadState by viewModel.loadState.collectAsStateWithLifecycle()
|
val loadState by viewModel.loadState.collectAsStateWithLifecycle()
|
||||||
|
val isPreload = remember(rssSource) { rssSource?.preload == true }
|
||||||
|
|
||||||
LaunchedEffect(sortName, sortUrl) {
|
LaunchedEffect(sortName, sortUrl, searchKey) {
|
||||||
viewModel.init(sortName, sortUrl)
|
viewModel.init(sortName, sortUrl, searchKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
val articleFlow = remember(rssUrl, sortName) {
|
val articleFlow = remember(rssUrl, sortName) {
|
||||||
@@ -117,7 +123,11 @@ fun RssArticlesPage(
|
|||||||
val articles by articleFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
val articles by articleFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||||
|
|
||||||
LaunchedEffect(rssSource?.sourceUrl, sortName, sortUrl) {
|
LaunchedEffect(rssSource?.sourceUrl, sortName, sortUrl) {
|
||||||
rssSource?.let(viewModel::loadArticles)
|
rssSource?.let { source ->
|
||||||
|
if (isPreload) {
|
||||||
|
viewModel.loadArticles(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LaunchedEffect(loadState.errorMessage) {
|
LaunchedEffect(loadState.errorMessage) {
|
||||||
@@ -142,6 +152,7 @@ fun RssArticlesPage(
|
|||||||
LoadMoreDetector(
|
LoadMoreDetector(
|
||||||
state = listState,
|
state = listState,
|
||||||
enabled = loadState.canLoadMore,
|
enabled = loadState.canLoadMore,
|
||||||
|
preloadThreshold = if (isPreload) 5 else 3,
|
||||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||||
)
|
)
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
@@ -174,6 +185,7 @@ fun RssArticlesPage(
|
|||||||
GridLoadMoreDetector(
|
GridLoadMoreDetector(
|
||||||
state = gridState,
|
state = gridState,
|
||||||
enabled = loadState.canLoadMore,
|
enabled = loadState.canLoadMore,
|
||||||
|
preloadThreshold = if (isPreload) 5 else 3,
|
||||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||||
)
|
)
|
||||||
LazyVerticalGrid(
|
LazyVerticalGrid(
|
||||||
@@ -208,6 +220,7 @@ fun RssArticlesPage(
|
|||||||
StaggeredLoadMoreDetector(
|
StaggeredLoadMoreDetector(
|
||||||
state = staggeredState,
|
state = staggeredState,
|
||||||
enabled = loadState.canLoadMore,
|
enabled = loadState.canLoadMore,
|
||||||
|
preloadThreshold = if (isPreload) 5 else 3,
|
||||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||||
)
|
)
|
||||||
LazyVerticalStaggeredGrid(
|
LazyVerticalStaggeredGrid(
|
||||||
@@ -244,13 +257,14 @@ fun RssArticlesPage(
|
|||||||
private fun LoadMoreDetector(
|
private fun LoadMoreDetector(
|
||||||
state: LazyListState,
|
state: LazyListState,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
|
preloadThreshold: Int = 3,
|
||||||
onLoadMore: () -> Unit
|
onLoadMore: () -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(state, enabled) {
|
LaunchedEffect(state, enabled) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val info = state.layoutInfo
|
val info = state.layoutInfo
|
||||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - 3
|
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - preloadThreshold
|
||||||
}.collect { shouldLoad ->
|
}.collect { shouldLoad ->
|
||||||
if (shouldLoad) onLoadMore()
|
if (shouldLoad) onLoadMore()
|
||||||
}
|
}
|
||||||
@@ -261,13 +275,14 @@ private fun LoadMoreDetector(
|
|||||||
private fun GridLoadMoreDetector(
|
private fun GridLoadMoreDetector(
|
||||||
state: LazyGridState,
|
state: LazyGridState,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
|
preloadThreshold: Int = 3,
|
||||||
onLoadMore: () -> Unit
|
onLoadMore: () -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(state, enabled) {
|
LaunchedEffect(state, enabled) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val info = state.layoutInfo
|
val info = state.layoutInfo
|
||||||
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
val lastVisible = info.visibleItemsInfo.lastOrNull()?.index ?: -1
|
||||||
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - 3
|
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - preloadThreshold
|
||||||
}.collect { shouldLoad ->
|
}.collect { shouldLoad ->
|
||||||
if (shouldLoad) onLoadMore()
|
if (shouldLoad) onLoadMore()
|
||||||
}
|
}
|
||||||
@@ -278,13 +293,14 @@ private fun GridLoadMoreDetector(
|
|||||||
private fun StaggeredLoadMoreDetector(
|
private fun StaggeredLoadMoreDetector(
|
||||||
state: LazyStaggeredGridState,
|
state: LazyStaggeredGridState,
|
||||||
enabled: Boolean,
|
enabled: Boolean,
|
||||||
|
preloadThreshold: Int = 3,
|
||||||
onLoadMore: () -> Unit
|
onLoadMore: () -> Unit
|
||||||
) {
|
) {
|
||||||
LaunchedEffect(state, enabled) {
|
LaunchedEffect(state, enabled) {
|
||||||
snapshotFlow {
|
snapshotFlow {
|
||||||
val info = state.layoutInfo
|
val info = state.layoutInfo
|
||||||
val lastVisible = info.visibleItemsInfo.maxOfOrNull { it.index } ?: -1
|
val lastVisible = info.visibleItemsInfo.maxOfOrNull { it.index } ?: -1
|
||||||
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - 3
|
enabled && info.totalItemsCount > 0 && lastVisible >= info.totalItemsCount - preloadThreshold
|
||||||
}.collect { shouldLoad ->
|
}.collect { shouldLoad ->
|
||||||
if (shouldLoad) onLoadMore()
|
if (shouldLoad) onLoadMore()
|
||||||
}
|
}
|
||||||
@@ -340,11 +356,22 @@ private fun RssArticleItem(
|
|||||||
RssArticleLayout.Waterfall -> 3
|
RssArticleLayout.Waterfall -> 3
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 边框设置
|
||||||
|
val borderModifier = if (ThemeConfig.enableContainerBorder) {
|
||||||
|
Modifier.border(
|
||||||
|
width = Dp(ThemeConfig.containerBorderWidth),
|
||||||
|
color = Color(ThemeConfig.containerBorderColor),
|
||||||
|
shape = RoundedCornerShape(12.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
GlassCard(
|
GlassCard(
|
||||||
onClick = { onClick(article) },
|
onClick = { onClick(article) },
|
||||||
cornerRadius = 12.dp,
|
cornerRadius = 12.dp,
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
modifier = Modifier.fillMaxWidth()
|
modifier = Modifier.fillMaxWidth().then(borderModifier)
|
||||||
) {
|
) {
|
||||||
when (layout) {
|
when (layout) {
|
||||||
RssArticleLayout.List -> {
|
RssArticleLayout.List -> {
|
||||||
|
|||||||
@@ -24,21 +24,24 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
|||||||
private var nextPageUrl: String? = null
|
private var nextPageUrl: String? = null
|
||||||
var sortName: String = ""
|
var sortName: String = ""
|
||||||
var sortUrl: String = ""
|
var sortUrl: String = ""
|
||||||
|
var searchKey: String? = null
|
||||||
var page = 1
|
var page = 1
|
||||||
|
|
||||||
fun init(bundle: Bundle?) {
|
fun init(bundle: Bundle?) {
|
||||||
bundle?.let {
|
bundle?.let {
|
||||||
init(
|
init(
|
||||||
sortName = it.getString("sortName") ?: "",
|
sortName = it.getString("sortName") ?: "",
|
||||||
sortUrl = it.getString("sortUrl") ?: ""
|
sortUrl = it.getString("sortUrl") ?: "",
|
||||||
|
searchKey = it.getString("searchKey")
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun init(sortName: String, sortUrl: String) {
|
fun init(sortName: String, sortUrl: String, searchKey: String? = null) {
|
||||||
if (this.sortName == sortName && this.sortUrl == sortUrl) return
|
if (this.sortName == sortName && this.sortUrl == sortUrl && this.searchKey == searchKey) return
|
||||||
this.sortName = sortName
|
this.sortName = sortName
|
||||||
this.sortUrl = sortUrl
|
this.sortUrl = sortUrl
|
||||||
|
this.searchKey = searchKey
|
||||||
page = 1
|
page = 1
|
||||||
nextPageUrl = null
|
nextPageUrl = null
|
||||||
_loadState.value = RssArticlesLoadState()
|
_loadState.value = RssArticlesLoadState()
|
||||||
@@ -53,7 +56,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
|||||||
)
|
)
|
||||||
page = 1
|
page = 1
|
||||||
order = System.currentTimeMillis()
|
order = System.currentTimeMillis()
|
||||||
Rss.getArticles(viewModelScope, sortName, sortUrl, rssSource, page).onSuccess(IO) {
|
Rss.getArticles(viewModelScope, sortName, sortUrl, rssSource, page, searchKey).onSuccess(IO) {
|
||||||
nextPageUrl = it.second
|
nextPageUrl = it.second
|
||||||
val articles = it.first
|
val articles = it.first
|
||||||
articles.forEach { rssArticle ->
|
articles.forEach { rssArticle ->
|
||||||
@@ -97,7 +100,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
|||||||
)
|
)
|
||||||
|
|
||||||
page++
|
page++
|
||||||
Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page).onSuccess(IO) {
|
Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page, searchKey).onSuccess(IO) {
|
||||||
nextPageUrl = it.second
|
nextPageUrl = it.second
|
||||||
loadMoreSuccess(it.first)
|
loadMoreSuccess(it.first)
|
||||||
}.onError {
|
}.onError {
|
||||||
|
|||||||
@@ -0,0 +1,274 @@
|
|||||||
|
package io.legado.app.ui.rss.article
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
|
import android.webkit.ConsoleMessage
|
||||||
|
import android.webkit.WebChromeClient
|
||||||
|
import android.webkit.WebResourceRequest
|
||||||
|
import android.webkit.WebResourceResponse
|
||||||
|
import android.webkit.WebSettings
|
||||||
|
import android.webkit.WebView
|
||||||
|
import android.webkit.WebViewClient
|
||||||
|
import androidx.activity.compose.LocalActivity
|
||||||
|
import androidx.appcompat.app.AppCompatActivity
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
|
import androidx.compose.foundation.layout.fillMaxSize
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.layout.statusBars
|
||||||
|
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||||
|
import androidx.compose.material3.LinearProgressIndicator
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.DisposableEffect
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import io.legado.app.data.entities.RssSource
|
||||||
|
import io.legado.app.help.WebCacheManager
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.help.http.CookieManager
|
||||||
|
import io.legado.app.help.webView.WebJsExtensions
|
||||||
|
import java.io.ByteArrayInputStream
|
||||||
|
|
||||||
|
@SuppressLint("SetJavaScriptEnabled")
|
||||||
|
@Composable
|
||||||
|
fun RssStartPage(
|
||||||
|
rssSource: RssSource?,
|
||||||
|
onNavigateToArticles: (sortUrl: String?) -> Unit,
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = LocalActivity.current as? AppCompatActivity
|
||||||
|
var progress by remember { mutableIntStateOf(0) }
|
||||||
|
var isLoading by remember { mutableStateOf(true) }
|
||||||
|
var webViewRef by remember { mutableStateOf<WebView?>(null) }
|
||||||
|
|
||||||
|
val startHtml = rssSource?.startHtml
|
||||||
|
val startStyle = rssSource?.startStyle ?: rssSource?.style
|
||||||
|
val startJs = rssSource?.startJs
|
||||||
|
val preloadJs = rssSource?.preloadJs
|
||||||
|
val hasPreloadJs = !preloadJs.isNullOrBlank()
|
||||||
|
|
||||||
|
LaunchedEffect(startHtml) {
|
||||||
|
if (startHtml.isNullOrBlank()) {
|
||||||
|
onNavigateToArticles(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (startHtml.isNullOrBlank()) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
DisposableEffect(Unit) {
|
||||||
|
onDispose {
|
||||||
|
webViewRef?.destroy()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.windowInsetsPadding(WindowInsets.statusBars)
|
||||||
|
) {
|
||||||
|
if (isLoading && progress < 100) {
|
||||||
|
LinearProgressIndicator(
|
||||||
|
progress = { progress / 100f },
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
AndroidView(
|
||||||
|
factory = { ctx ->
|
||||||
|
WebView(ctx).apply {
|
||||||
|
webViewRef = this
|
||||||
|
|
||||||
|
settings.apply {
|
||||||
|
javaScriptEnabled = true
|
||||||
|
domStorageEnabled = true
|
||||||
|
mixedContentMode = WebSettings.MIXED_CONTENT_ALWAYS_ALLOW
|
||||||
|
useWideViewPort = true
|
||||||
|
loadWithOverviewMode = true
|
||||||
|
userAgentString = AppConfig.userAgent
|
||||||
|
cacheMode = if (rssSource?.cacheFirst == true) {
|
||||||
|
WebSettings.LOAD_CACHE_ELSE_NETWORK
|
||||||
|
} else {
|
||||||
|
WebSettings.LOAD_DEFAULT
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
CookieManager.applyToWebView(rssSource?.sourceUrl.orEmpty())
|
||||||
|
|
||||||
|
if (hasPreloadJs && activity != null && rssSource != null) {
|
||||||
|
val webJsExtensions = WebJsExtensions(
|
||||||
|
rssSource,
|
||||||
|
activity,
|
||||||
|
this,
|
||||||
|
callback = object : WebJsExtensions.Callback {
|
||||||
|
override fun upConfig(config: String) {}
|
||||||
|
override fun onNavigateToArticles(sortUrl: String?) {
|
||||||
|
activity.runOnUiThread {
|
||||||
|
onNavigateToArticles(sortUrl)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
addJavascriptInterface(webJsExtensions, WebJsExtensions.nameJava)
|
||||||
|
addJavascriptInterface(rssSource, WebJsExtensions.nameSource)
|
||||||
|
addJavascriptInterface(WebCacheManager, WebJsExtensions.nameCache)
|
||||||
|
}
|
||||||
|
|
||||||
|
addJavascriptInterface(object {
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun navigateToArticles() {
|
||||||
|
activity?.runOnUiThread {
|
||||||
|
onNavigateToArticles(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun getSourceUrl(): String {
|
||||||
|
return rssSource?.sourceUrl.orEmpty()
|
||||||
|
}
|
||||||
|
|
||||||
|
@android.webkit.JavascriptInterface
|
||||||
|
fun getSourceName(): String {
|
||||||
|
return rssSource?.sourceName.orEmpty()
|
||||||
|
}
|
||||||
|
}, "AndroidBridge")
|
||||||
|
|
||||||
|
var jsInjected = false
|
||||||
|
|
||||||
|
webViewClient = object : WebViewClient() {
|
||||||
|
override fun onPageFinished(view: WebView?, url: String?) {
|
||||||
|
super.onPageFinished(view, url)
|
||||||
|
isLoading = false
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun shouldInterceptRequest(
|
||||||
|
view: WebView?,
|
||||||
|
request: WebResourceRequest
|
||||||
|
): WebResourceResponse? {
|
||||||
|
val url = request.url.toString()
|
||||||
|
if (!jsInjected && url == WebJsExtensions.nameUrl) {
|
||||||
|
jsInjected = true
|
||||||
|
val jsContent = buildString {
|
||||||
|
append("(() => {")
|
||||||
|
append(WebJsExtensions.JS_INJECTION)
|
||||||
|
if (!preloadJs.isNullOrBlank()) {
|
||||||
|
append("\n")
|
||||||
|
append(preloadJs)
|
||||||
|
}
|
||||||
|
append("\n})();")
|
||||||
|
}
|
||||||
|
return WebResourceResponse(
|
||||||
|
"text/javascript",
|
||||||
|
"utf-8",
|
||||||
|
ByteArrayInputStream(jsContent.toByteArray())
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return super.shouldInterceptRequest(view, request)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
webChromeClient = object : WebChromeClient() {
|
||||||
|
override fun onProgressChanged(view: WebView?, newProgress: Int) {
|
||||||
|
progress = newProgress
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onConsoleMessage(consoleMessage: ConsoleMessage?): Boolean {
|
||||||
|
if (rssSource?.showWebLog == true) {
|
||||||
|
consoleMessage?.message()?.let { message ->
|
||||||
|
io.legado.app.constant.AppLog.put("StartPage: $message")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val htmlContent = buildStartPageHtml(startHtml, startStyle, startJs, hasPreloadJs)
|
||||||
|
loadDataWithBaseURL(
|
||||||
|
rssSource?.sourceUrl,
|
||||||
|
htmlContent,
|
||||||
|
"text/html",
|
||||||
|
"utf-8",
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
modifier = Modifier.fillMaxSize()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildStartPageHtml(
|
||||||
|
html: String,
|
||||||
|
style: String?,
|
||||||
|
startJs: String?,
|
||||||
|
hasPreloadJs: Boolean
|
||||||
|
): String {
|
||||||
|
val styleContent = style?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
"<style>$it</style>"
|
||||||
|
} ?: "<style>img{max-width:100% !important; width:auto; height:auto;}body{word-wrap:break-word; height:auto;max-width: 100%; width:auto;}</style>"
|
||||||
|
|
||||||
|
val jsScript = startJs?.takeIf { it.isNotBlank() }?.let {
|
||||||
|
"<script>$it</script>"
|
||||||
|
} ?: ""
|
||||||
|
|
||||||
|
val preloadScript = if (hasPreloadJs) {
|
||||||
|
WebJsExtensions.JS_URL
|
||||||
|
} else {
|
||||||
|
""
|
||||||
|
}
|
||||||
|
|
||||||
|
val htmlBuilder = StringBuilder()
|
||||||
|
|
||||||
|
val headIndex = html.indexOf("<head>")
|
||||||
|
if (headIndex >= 0) {
|
||||||
|
htmlBuilder.append(html, 0, headIndex + 6)
|
||||||
|
htmlBuilder.append(preloadScript)
|
||||||
|
htmlBuilder.append(html, headIndex + 6, html.length)
|
||||||
|
} else {
|
||||||
|
htmlBuilder.append("<head>")
|
||||||
|
htmlBuilder.append(preloadScript)
|
||||||
|
htmlBuilder.append("</head>")
|
||||||
|
htmlBuilder.append(html)
|
||||||
|
}
|
||||||
|
|
||||||
|
val result = htmlBuilder.toString()
|
||||||
|
|
||||||
|
val styleEndIndex = result.indexOf("</style>")
|
||||||
|
return if (styleEndIndex >= 0) {
|
||||||
|
val insertIndex = styleEndIndex + 8
|
||||||
|
buildString {
|
||||||
|
append(result, 0, insertIndex)
|
||||||
|
append(styleContent)
|
||||||
|
append(result, insertIndex, result.length)
|
||||||
|
append(jsScript)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val headEndIndex = result.indexOf("</head>")
|
||||||
|
if (headEndIndex >= 0) {
|
||||||
|
buildString {
|
||||||
|
append(result, 0, headEndIndex)
|
||||||
|
append(styleContent)
|
||||||
|
append(result, headEndIndex, result.length)
|
||||||
|
append(jsScript)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildString {
|
||||||
|
append(styleContent)
|
||||||
|
append(result)
|
||||||
|
append(jsScript)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ import io.legado.app.data.entities.BookSource
|
|||||||
import io.legado.app.data.entities.RssReadRecord
|
import io.legado.app.data.entities.RssReadRecord
|
||||||
import io.legado.app.data.entities.RssSource
|
import io.legado.app.data.entities.RssSource
|
||||||
import io.legado.app.help.JsExtensions
|
import io.legado.app.help.JsExtensions
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||||
import io.legado.app.ui.association.AddToBookshelfDialog
|
import io.legado.app.ui.association.AddToBookshelfDialog
|
||||||
import io.legado.app.ui.book.search.SearchActivity
|
import io.legado.app.ui.book.search.SearchActivity
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
@@ -72,10 +73,19 @@ open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) :
|
|||||||
activityRef.get()?.showDialogFragment(PhotoDialog(src, getSource()?.getKey()))
|
activityRef.get()?.showDialogFragment(PhotoDialog(src, getSource()?.getKey()))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun getStringList(rule: String?, mContent: Any? = null, isUrl: Boolean = false): List<String>? {
|
||||||
|
return AnalyzeRule(null, source = getSource()).getStringList(rule, mContent, isUrl)
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmOverloads
|
||||||
|
fun getString(ruleStr: String?, mContent: Any? = null, isUrl: Boolean = false): String {
|
||||||
|
return AnalyzeRule(null, source = getSource()).getString(ruleStr, mContent, isUrl)
|
||||||
|
}
|
||||||
|
|
||||||
@JavascriptInterface
|
@JavascriptInterface
|
||||||
@JvmOverloads
|
@JvmOverloads
|
||||||
fun open(name: String, url: String? = null, title: String? = null, origin: String? = null) {
|
open fun open(name: String, url: String? = null, title: String? = null, origin: String? = null) {
|
||||||
val activity = activityRef.get() ?: return
|
val activity = activityRef.get() ?: return
|
||||||
activity.lifecycleScope.launch(IO) {
|
activity.lifecycleScope.launch(IO) {
|
||||||
val source = getSource() ?: return@launch
|
val source = getSource() ?: return@launch
|
||||||
@@ -151,7 +161,7 @@ open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) :
|
|||||||
origin = sourceUrl,
|
origin = sourceUrl,
|
||||||
readTime = System.currentTimeMillis()
|
readTime = System.currentTimeMillis()
|
||||||
)
|
)
|
||||||
appDb.rssReadRecordDao.insertRecord(rssReadRecord) //留下历史记录
|
appDb.rssReadRecordDao.insertRecord(rssReadRecord)
|
||||||
withContext(Main) {
|
withContext(Main) {
|
||||||
activity.startActivity(
|
activity.startActivity(
|
||||||
MainActivity.createRssReadIntent(
|
MainActivity.createRssReadIntent(
|
||||||
|
|||||||
@@ -1,16 +1,17 @@
|
|||||||
package io.legado.app.ui.rss.source.debug
|
package io.legado.app.ui.rss.source.debug
|
||||||
|
|
||||||
|
import android.annotation.SuppressLint
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.Menu
|
import android.view.Menu
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.widget.SearchView
|
import android.view.View
|
||||||
|
import androidx.appcompat.widget.SearchView
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
import androidx.lifecycle.lifecycleScope
|
import androidx.lifecycle.lifecycleScope
|
||||||
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.databinding.ActivitySourceDebugBinding
|
import io.legado.app.databinding.ActivitySourceDebugBinding
|
||||||
//import io.legado.app.lib.theme.accentColor
|
import io.legado.app.help.source.sortUrls
|
||||||
//import io.legado.app.lib.theme.primaryColor
|
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
import io.legado.app.ui.widget.dialog.TextDialog
|
||||||
import io.legado.app.utils.applyNavigationBarPadding
|
import io.legado.app.utils.applyNavigationBarPadding
|
||||||
import io.legado.app.utils.gone
|
import io.legado.app.utils.gone
|
||||||
@@ -19,6 +20,8 @@ import io.legado.app.utils.toastOnUi
|
|||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import splitties.views.onClick
|
||||||
|
import splitties.views.onLongClick
|
||||||
|
|
||||||
|
|
||||||
class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSourceDebugModel>() {
|
class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSourceDebugModel>() {
|
||||||
@@ -28,10 +31,12 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
|||||||
|
|
||||||
private val adapter by lazy { RssSourceDebugAdapter(this) }
|
private val adapter by lazy { RssSourceDebugAdapter(this) }
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
initRecyclerView()
|
initRecyclerView()
|
||||||
initSearchView()
|
initSearchView()
|
||||||
|
initHelpView()
|
||||||
viewModel.observe { state, msg ->
|
viewModel.observe { state, msg ->
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
adapter.addItem(msg)
|
adapter.addItem(msg)
|
||||||
@@ -41,7 +46,7 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
viewModel.initData(intent.getStringExtra("key")) {
|
viewModel.initData(intent.getStringExtra("key")) {
|
||||||
startDebug()
|
// initData 完成后的回调
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,21 +64,72 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initRecyclerView() {
|
private fun initRecyclerView() {
|
||||||
// binding.recyclerView.setEdgeEffectColor(primaryColor)
|
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.recyclerView.applyNavigationBarPadding()
|
binding.recyclerView.applyNavigationBarPadding()
|
||||||
// binding.rotateLoading.loadingColor = accentColor
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun initSearchView() {
|
private fun initSearchView() {
|
||||||
binding.titleBar.findViewById<SearchView>(R.id.search_view).gone()
|
val searchView = binding.titleBar.findViewById<SearchView>(R.id.search_view)
|
||||||
|
searchView.visible()
|
||||||
|
searchView.isSubmitButtonEnabled = true
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String?): Boolean {
|
||||||
|
searchView.clearFocus()
|
||||||
|
startDebug(query ?: "")
|
||||||
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun startDebug() {
|
override fun onQueryTextChange(newText: String?): Boolean {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressLint("SetTextI18n")
|
||||||
|
private fun initHelpView() {
|
||||||
|
binding.help.visible()
|
||||||
|
binding.textMy.onClick {
|
||||||
|
binding.titleBar.findViewById<SearchView>(R.id.search_view).setQuery(binding.textMy.text, true)
|
||||||
|
}
|
||||||
|
binding.textXt.onClick {
|
||||||
|
binding.titleBar.findViewById<SearchView>(R.id.search_view).setQuery(binding.textXt.text, true)
|
||||||
|
}
|
||||||
|
binding.textFx.onClick {
|
||||||
|
if (!binding.textFx.text.startsWith("ERROR:")) {
|
||||||
|
binding.titleBar.findViewById<SearchView>(R.id.search_view).setQuery(binding.textFx.text, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
binding.textContent.onClick {
|
||||||
|
val query = binding.titleBar.findViewById<SearchView>(R.id.search_view).query
|
||||||
|
if (!query.isNullOrBlank()) {
|
||||||
|
binding.titleBar.findViewById<SearchView>(R.id.search_view).setQuery(query, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
initSortKinds()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun initSortKinds() {
|
||||||
|
lifecycleScope.launch {
|
||||||
|
val sortKinds = viewModel.rssSource?.sortUrls()?.filter {
|
||||||
|
it.second.isNotBlank()
|
||||||
|
}
|
||||||
|
sortKinds?.firstOrNull()?.let {
|
||||||
|
binding.textFx.text = "${it.first}::${it.second}"
|
||||||
|
if (it.first.startsWith("ERROR:")) {
|
||||||
|
adapter.addItem("获取发现出错\n${it.second}")
|
||||||
|
binding.titleBar.findViewById<SearchView>(R.id.search_view).clearFocus()
|
||||||
|
return@launch
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startDebug(key: String) {
|
||||||
adapter.clearItems()
|
adapter.clearItems()
|
||||||
|
binding.help.gone() // 关闭调试选项面板
|
||||||
viewModel.rssSource?.let {
|
viewModel.rssSource?.let {
|
||||||
binding.rotateLoading.visible()
|
binding.rotateLoading.visible()
|
||||||
viewModel.startDebug(it)
|
viewModel.startDebug(it, key)
|
||||||
} ?: toastOnUi(R.string.error_no_source)
|
} ?: toastOnUi(R.string.error_no_source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -34,6 +34,15 @@ class RssSourceDebugModel(application: Application) : BaseViewModel(application)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun startDebug(source: RssSource, key: String) {
|
||||||
|
execute {
|
||||||
|
Debug.callback = this@RssSourceDebugModel
|
||||||
|
Debug.startDebug(this, source, key)
|
||||||
|
}.onStart {
|
||||||
|
// 可以在这里添加开始回调
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
override fun printLog(state: Int, msg: String) {
|
override fun printLog(state: Int, msg: String) {
|
||||||
when (state) {
|
when (state) {
|
||||||
10 -> listSrc = msg
|
10 -> listSrc = msg
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ class RssSourceEditActivity :
|
|||||||
}
|
}
|
||||||
private val adapter by lazy { RssSourceEditAdapter() }
|
private val adapter by lazy { RssSourceEditAdapter() }
|
||||||
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
|
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
|
||||||
|
private val startEntities: ArrayList<EditEntity> = ArrayList()
|
||||||
private val listEntities: ArrayList<EditEntity> = ArrayList()
|
private val listEntities: ArrayList<EditEntity> = ArrayList()
|
||||||
private val webViewEntities: ArrayList<EditEntity> = ArrayList()
|
private val webViewEntities: ArrayList<EditEntity> = ArrayList()
|
||||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||||
@@ -156,6 +157,8 @@ class RssSourceEditActivity :
|
|||||||
ErrorCorrectionLevel.L
|
ErrorCorrectionLevel.L
|
||||||
)
|
)
|
||||||
|
|
||||||
|
R.id.menu_log -> showDialogFragment<io.legado.app.ui.about.AppLogDialog>()
|
||||||
|
|
||||||
R.id.menu_help -> showHelp("ruleHelp")
|
R.id.menu_help -> showHelp("ruleHelp")
|
||||||
}
|
}
|
||||||
return super.onCompatOptionsItemSelected(item)
|
return super.onCompatOptionsItemSelected(item)
|
||||||
@@ -165,6 +168,9 @@ class RssSourceEditActivity :
|
|||||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||||
setText(R.string.source_tab_base)
|
setText(R.string.source_tab_base)
|
||||||
})
|
})
|
||||||
|
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||||
|
setText(R.string.source_tab_start)
|
||||||
|
})
|
||||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||||
setText(R.string.source_tab_list)
|
setText(R.string.source_tab_list)
|
||||||
})
|
})
|
||||||
@@ -199,8 +205,9 @@ class RssSourceEditActivity :
|
|||||||
|
|
||||||
private fun setEditEntities(tabPosition: Int?) {
|
private fun setEditEntities(tabPosition: Int?) {
|
||||||
when (tabPosition) {
|
when (tabPosition) {
|
||||||
1 -> adapter.editEntities = listEntities
|
1 -> adapter.editEntities = startEntities
|
||||||
2 -> adapter.editEntities = webViewEntities
|
2 -> adapter.editEntities = listEntities
|
||||||
|
3 -> adapter.editEntities = webViewEntities
|
||||||
else -> adapter.editEntities = sourceEntities
|
else -> adapter.editEntities = sourceEntities
|
||||||
}
|
}
|
||||||
binding.recyclerView.scrollToPosition(0)
|
binding.recyclerView.scrollToPosition(0)
|
||||||
@@ -212,6 +219,21 @@ class RssSourceEditActivity :
|
|||||||
binding.cbIsEnable.isChecked = rs.enabled
|
binding.cbIsEnable.isChecked = rs.enabled
|
||||||
binding.cbSingleUrl.isChecked = rs.singleUrl
|
binding.cbSingleUrl.isChecked = rs.singleUrl
|
||||||
binding.cbIsEnableCookie.isChecked = rs.enabledCookieJar == true
|
binding.cbIsEnableCookie.isChecked = rs.enabledCookieJar == true
|
||||||
|
binding.cbIsEnablePreload.isChecked = rs.preload
|
||||||
|
val typeCount = binding.spType.adapter?.count ?: 0
|
||||||
|
if (typeCount > 0 && rs.type !in 0..<typeCount) {
|
||||||
|
rs.type = 0
|
||||||
|
}
|
||||||
|
if (typeCount > 0) {
|
||||||
|
binding.spType.setSelection(rs.type)
|
||||||
|
}
|
||||||
|
val styleCount = binding.lyType.adapter?.count ?: 0
|
||||||
|
if (styleCount > 0 && rs.articleStyle !in 0..<styleCount) {
|
||||||
|
rs.articleStyle = 0
|
||||||
|
}
|
||||||
|
if (styleCount > 0) {
|
||||||
|
binding.lyType.setSelection(rs.articleStyle)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
sourceEntities.clear()
|
sourceEntities.clear()
|
||||||
sourceEntities.apply {
|
sourceEntities.apply {
|
||||||
@@ -220,6 +242,7 @@ class RssSourceEditActivity :
|
|||||||
add(EditEntity("sourceIcon", rs.sourceIcon, R.string.source_icon))
|
add(EditEntity("sourceIcon", rs.sourceIcon, R.string.source_icon))
|
||||||
add(EditEntity("sourceGroup", rs.sourceGroup, R.string.source_group))
|
add(EditEntity("sourceGroup", rs.sourceGroup, R.string.source_group))
|
||||||
add(EditEntity("sourceComment", rs.sourceComment, R.string.comment))
|
add(EditEntity("sourceComment", rs.sourceComment, R.string.comment))
|
||||||
|
add(EditEntity("searchUrl", rs.searchUrl, R.string.r_search_url))
|
||||||
add(EditEntity("sortUrl", rs.sortUrl, R.string.sort_url))
|
add(EditEntity("sortUrl", rs.sortUrl, R.string.sort_url))
|
||||||
add(EditEntity("loginUrl", rs.loginUrl, R.string.login_url))
|
add(EditEntity("loginUrl", rs.loginUrl, R.string.login_url))
|
||||||
add(EditEntity("loginUi", rs.loginUi, R.string.login_ui))
|
add(EditEntity("loginUi", rs.loginUi, R.string.login_ui))
|
||||||
@@ -230,6 +253,13 @@ class RssSourceEditActivity :
|
|||||||
add(EditEntity("concurrentRate", rs.concurrentRate, R.string.concurrent_rate))
|
add(EditEntity("concurrentRate", rs.concurrentRate, R.string.concurrent_rate))
|
||||||
add(EditEntity("jsLib", rs.jsLib, "jsLib"))
|
add(EditEntity("jsLib", rs.jsLib, "jsLib"))
|
||||||
}
|
}
|
||||||
|
startEntities.clear()
|
||||||
|
startEntities.apply {
|
||||||
|
add(EditEntity("startHtml", rs.startHtml, R.string.r_startHtml))
|
||||||
|
add(EditEntity("startStyle", rs.startStyle, R.string.r_startStyle))
|
||||||
|
add(EditEntity("startJs", rs.startJs, R.string.r_startJs))
|
||||||
|
add(EditEntity("preloadJs", rs.preloadJs, R.string.r_preloadJs))
|
||||||
|
}
|
||||||
listEntities.clear()
|
listEntities.clear()
|
||||||
listEntities.apply {
|
listEntities.apply {
|
||||||
add(EditEntity("ruleArticles", rs.ruleArticles, R.string.r_articles))
|
add(EditEntity("ruleArticles", rs.ruleArticles, R.string.r_articles))
|
||||||
@@ -258,6 +288,22 @@ class RssSourceEditActivity :
|
|||||||
EditEntity.ViewType.checkBox
|
EditEntity.ViewType.checkBox
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
add(
|
||||||
|
EditEntity(
|
||||||
|
"showWebLog",
|
||||||
|
rs.showWebLog.toString(),
|
||||||
|
R.string.load_with_web_log,
|
||||||
|
EditEntity.ViewType.checkBox
|
||||||
|
)
|
||||||
|
)
|
||||||
|
add(
|
||||||
|
EditEntity(
|
||||||
|
"cacheFirst",
|
||||||
|
rs.cacheFirst.toString(),
|
||||||
|
R.string.cache_first,
|
||||||
|
EditEntity.ViewType.checkBox
|
||||||
|
)
|
||||||
|
)
|
||||||
add(EditEntity("ruleContent", rs.ruleContent, R.string.r_content))
|
add(EditEntity("ruleContent", rs.ruleContent, R.string.r_content))
|
||||||
add(EditEntity("style", rs.style, R.string.r_style))
|
add(EditEntity("style", rs.style, R.string.r_style))
|
||||||
add(EditEntity("injectJs", rs.injectJs, R.string.r_inject_js))
|
add(EditEntity("injectJs", rs.injectJs, R.string.r_inject_js))
|
||||||
@@ -280,6 +326,9 @@ class RssSourceEditActivity :
|
|||||||
source.enabled = binding.cbIsEnable.isChecked
|
source.enabled = binding.cbIsEnable.isChecked
|
||||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||||
|
source.preload = binding.cbIsEnablePreload.isChecked
|
||||||
|
source.type = binding.spType.selectedItemPosition
|
||||||
|
source.articleStyle = binding.lyType.selectedItemPosition
|
||||||
sourceEntities.forEach {
|
sourceEntities.forEach {
|
||||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
@@ -295,10 +344,20 @@ class RssSourceEditActivity :
|
|||||||
"header" -> source.header = it.value
|
"header" -> source.header = it.value
|
||||||
"variableComment" -> source.variableComment = it.value
|
"variableComment" -> source.variableComment = it.value
|
||||||
"concurrentRate" -> source.concurrentRate = it.value
|
"concurrentRate" -> source.concurrentRate = it.value
|
||||||
|
"searchUrl" -> source.searchUrl = it.value
|
||||||
"sortUrl" -> source.sortUrl = it.value
|
"sortUrl" -> source.sortUrl = it.value
|
||||||
"jsLib" -> source.jsLib = it.value
|
"jsLib" -> source.jsLib = it.value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
startEntities.forEach {
|
||||||
|
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||||
|
when (it.key) {
|
||||||
|
"startHtml" -> source.startHtml = it.value
|
||||||
|
"startStyle" -> source.startStyle = it.value
|
||||||
|
"startJs" -> source.startJs = it.value
|
||||||
|
"preloadJs" -> source.preloadJs = it.value
|
||||||
|
}
|
||||||
|
}
|
||||||
listEntities.forEach {
|
listEntities.forEach {
|
||||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||||
when (it.key) {
|
when (it.key) {
|
||||||
@@ -327,6 +386,8 @@ class RssSourceEditActivity :
|
|||||||
when (it.key) {
|
when (it.key) {
|
||||||
"enableJs" -> source.enableJs = it.value.isTrue()
|
"enableJs" -> source.enableJs = it.value.isTrue()
|
||||||
"loadWithBaseUrl" -> source.loadWithBaseUrl = it.value.isTrue()
|
"loadWithBaseUrl" -> source.loadWithBaseUrl = it.value.isTrue()
|
||||||
|
"showWebLog" -> source.showWebLog = it.value.isTrue()
|
||||||
|
"cacheFirst" -> source.cacheFirst = it.value.isTrue()
|
||||||
"ruleContent" -> source.ruleContent =
|
"ruleContent" -> source.ruleContent =
|
||||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.layout.ContentScale
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
import org.koin.compose.koinInject
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppBackground(
|
fun AppBackground(
|
||||||
@@ -29,10 +30,10 @@ fun AppBackground(
|
|||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = bgImagePath,
|
model = bgImagePath,
|
||||||
contentDescription = null,
|
contentDescription = null,
|
||||||
|
imageLoader = koinInject(),
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
.blur(blur.dp),
|
.blur(blur.dp),
|
||||||
//TODO:低版本安卓使用coil-transformations
|
|
||||||
contentScale = ContentScale.Crop
|
contentScale = ContentScale.Crop
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,17 +7,145 @@ import androidx.compose.material3.MaterialExpressiveTheme
|
|||||||
import androidx.compose.material3.MotionScheme
|
import androidx.compose.material3.MotionScheme
|
||||||
import androidx.compose.material3.Shapes
|
import androidx.compose.material3.Shapes
|
||||||
import androidx.compose.material3.Typography
|
import androidx.compose.material3.Typography
|
||||||
|
import androidx.compose.material3.darkColorScheme
|
||||||
|
import androidx.compose.material3.lightColorScheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.compositeOver
|
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.platform.LocalContext
|
||||||
import androidx.compose.ui.res.colorResource
|
import androidx.compose.ui.res.colorResource
|
||||||
|
import androidx.compose.ui.text.font.FontFamily
|
||||||
|
import android.graphics.Typeface
|
||||||
|
import android.net.Uri
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||||
|
import io.legado.app.ui.theme.CustomColorScheme
|
||||||
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
|
||||||
|
// 用户自定义的颜色集合
|
||||||
|
data class UserColorPalette(
|
||||||
|
val primaryColor: Color, // 用户定义的主题色
|
||||||
|
val secondaryColor: Color, // 用户定义的次要主题色
|
||||||
|
val backgroundColor: Color, // 用户定义的背景色
|
||||||
|
val primaryFontColor: Color, // 用户定义的主要字体色
|
||||||
|
val secondaryFontColor: Color, // 用户定义的次要字体色
|
||||||
|
val labelContainerColor: Color // 用户定义的标签容器色
|
||||||
|
)
|
||||||
|
|
||||||
|
// 将用户自定义颜色映射到 Material Theme 的 ColorScheme
|
||||||
|
fun generateColorScheme(userPalette: UserColorPalette, isDark: Boolean): androidx.compose.material3.ColorScheme {
|
||||||
|
return if (isDark) {
|
||||||
|
darkColorScheme(
|
||||||
|
primary = userPalette.primaryColor,
|
||||||
|
onPrimary = userPalette.primaryFontColor, // 关键映射:主要字体色 -> onPrimary
|
||||||
|
primaryContainer = userPalette.labelContainerColor, // 关键映射:标签容器色 -> primaryContainer
|
||||||
|
onPrimaryContainer = userPalette.primaryFontColor,
|
||||||
|
|
||||||
|
secondary = userPalette.secondaryColor,
|
||||||
|
onSecondary = userPalette.secondaryFontColor,
|
||||||
|
secondaryContainer = userPalette.labelContainerColor,
|
||||||
|
onSecondaryContainer = userPalette.secondaryFontColor,
|
||||||
|
|
||||||
|
tertiary = userPalette.secondaryColor, // 可以将次要主题色也用作第三色
|
||||||
|
onTertiary = userPalette.secondaryFontColor,
|
||||||
|
|
||||||
|
background = userPalette.backgroundColor,
|
||||||
|
surface = userPalette.backgroundColor,
|
||||||
|
onBackground = userPalette.primaryFontColor,
|
||||||
|
onSurface = userPalette.primaryFontColor, // 关键映射:主要字体色 -> onSurface
|
||||||
|
surfaceVariant = userPalette.labelContainerColor,
|
||||||
|
onSurfaceVariant = userPalette.secondaryFontColor, // 关键映射:次要字体色 -> onSurfaceVariant
|
||||||
|
|
||||||
|
// 其他颜色
|
||||||
|
error = Color(0xFFB3261E),
|
||||||
|
onError = Color(0xFFFFFFFF),
|
||||||
|
errorContainer = Color(0xFFF9DEDC),
|
||||||
|
onErrorContainer = Color(0xFF410E0B),
|
||||||
|
outline = Color(0xFF79747E),
|
||||||
|
outlineVariant = Color(0xFFC4C7C5),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
|
|
||||||
|
// 表面颜色变体
|
||||||
|
surfaceBright = userPalette.backgroundColor,
|
||||||
|
surfaceDim = userPalette.backgroundColor,
|
||||||
|
surfaceContainer = userPalette.backgroundColor,
|
||||||
|
surfaceContainerHigh = userPalette.backgroundColor,
|
||||||
|
surfaceContainerHighest = userPalette.backgroundColor,
|
||||||
|
surfaceContainerLow = userPalette.labelContainerColor,
|
||||||
|
surfaceContainerLowest = userPalette.backgroundColor,
|
||||||
|
|
||||||
|
// 固定颜色
|
||||||
|
primaryFixed = userPalette.primaryColor,
|
||||||
|
primaryFixedDim = userPalette.primaryColor.copy(alpha = 0.8f),
|
||||||
|
onPrimaryFixed = userPalette.primaryFontColor,
|
||||||
|
onPrimaryFixedVariant = userPalette.primaryFontColor,
|
||||||
|
secondaryFixed = userPalette.secondaryColor,
|
||||||
|
secondaryFixedDim = userPalette.secondaryColor.copy(alpha = 0.8f),
|
||||||
|
onSecondaryFixed = userPalette.secondaryFontColor,
|
||||||
|
onSecondaryFixedVariant = userPalette.secondaryFontColor,
|
||||||
|
tertiaryFixed = userPalette.secondaryColor,
|
||||||
|
tertiaryFixedDim = userPalette.secondaryColor.copy(alpha = 0.8f),
|
||||||
|
onTertiaryFixed = userPalette.secondaryFontColor,
|
||||||
|
onTertiaryFixedVariant = userPalette.secondaryFontColor
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
lightColorScheme(
|
||||||
|
primary = userPalette.primaryColor,
|
||||||
|
onPrimary = userPalette.primaryFontColor,
|
||||||
|
primaryContainer = userPalette.labelContainerColor,
|
||||||
|
onPrimaryContainer = userPalette.primaryFontColor,
|
||||||
|
|
||||||
|
secondary = userPalette.secondaryColor,
|
||||||
|
onSecondary = userPalette.secondaryFontColor,
|
||||||
|
secondaryContainer = userPalette.labelContainerColor,
|
||||||
|
onSecondaryContainer = userPalette.secondaryFontColor,
|
||||||
|
|
||||||
|
tertiary = userPalette.secondaryColor,
|
||||||
|
onTertiary = userPalette.secondaryFontColor,
|
||||||
|
|
||||||
|
background = userPalette.backgroundColor,
|
||||||
|
surface = userPalette.backgroundColor,
|
||||||
|
onBackground = userPalette.primaryFontColor,
|
||||||
|
onSurface = userPalette.primaryFontColor,
|
||||||
|
surfaceVariant = userPalette.labelContainerColor,
|
||||||
|
onSurfaceVariant = userPalette.secondaryFontColor,
|
||||||
|
|
||||||
|
// 其他颜色
|
||||||
|
error = Color(0xFFB3261E),
|
||||||
|
onError = Color(0xFFFFFFFF),
|
||||||
|
errorContainer = Color(0xFFF9DEDC),
|
||||||
|
onErrorContainer = Color(0xFF410E0B),
|
||||||
|
outline = Color(0xFF79747E),
|
||||||
|
outlineVariant = Color(0xFFC4C7C5),
|
||||||
|
scrim = Color(0xFF000000),
|
||||||
|
|
||||||
|
// 表面颜色变体
|
||||||
|
surfaceBright = userPalette.backgroundColor,
|
||||||
|
surfaceDim = userPalette.backgroundColor,
|
||||||
|
surfaceContainer = userPalette.backgroundColor,
|
||||||
|
surfaceContainerHigh = userPalette.backgroundColor,
|
||||||
|
surfaceContainerHighest = userPalette.backgroundColor,
|
||||||
|
surfaceContainerLow = userPalette.labelContainerColor,
|
||||||
|
surfaceContainerLowest = userPalette.backgroundColor,
|
||||||
|
|
||||||
|
// 固定颜色
|
||||||
|
primaryFixed = userPalette.primaryColor,
|
||||||
|
primaryFixedDim = userPalette.primaryColor.copy(alpha = 0.8f),
|
||||||
|
onPrimaryFixed = userPalette.primaryFontColor,
|
||||||
|
onPrimaryFixedVariant = userPalette.primaryFontColor,
|
||||||
|
secondaryFixed = userPalette.secondaryColor,
|
||||||
|
secondaryFixedDim = userPalette.secondaryColor.copy(alpha = 0.8f),
|
||||||
|
onSecondaryFixed = userPalette.secondaryFontColor,
|
||||||
|
onSecondaryFixedVariant = userPalette.secondaryFontColor,
|
||||||
|
tertiaryFixed = userPalette.secondaryColor,
|
||||||
|
tertiaryFixedDim = userPalette.secondaryColor.copy(alpha = 0.8f),
|
||||||
|
onTertiaryFixed = userPalette.secondaryFontColor,
|
||||||
|
onTertiaryFixedVariant = userPalette.secondaryFontColor
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
@@ -35,12 +163,47 @@ fun AppTheme(
|
|||||||
val useMiuixMonet = ThemeConfig.useMiuixMonet
|
val useMiuixMonet = ThemeConfig.useMiuixMonet
|
||||||
val customPrimary = ThemeConfig.cPrimary
|
val customPrimary = ThemeConfig.cPrimary
|
||||||
val customNightPrimary = ThemeConfig.cNPrimary
|
val customNightPrimary = ThemeConfig.cNPrimary
|
||||||
|
val appFontPath = ThemeConfig.appFontPath
|
||||||
|
|
||||||
|
val enableDeepPersonalization = ThemeConfig.enableDeepPersonalization
|
||||||
|
val themeColor = ThemeConfig.themeColor
|
||||||
|
val secondaryThemeColor = ThemeConfig.secondaryThemeColor
|
||||||
|
val primaryTextColor = ThemeConfig.primaryTextColor
|
||||||
|
val secondaryTextColor = ThemeConfig.secondaryTextColor
|
||||||
|
val themeBackgroundColor = ThemeConfig.themeBackgroundColor
|
||||||
|
val customLabelContainerColor = ThemeConfig.labelContainerColor
|
||||||
|
|
||||||
val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(themeModeValue)
|
val colorSchemeMode = ThemeResolver.resolveColorSchemeMode(themeModeValue)
|
||||||
val miuixColorSchemeMode = remember(themeModeValue, useMiuixMonet) {
|
val miuixColorSchemeMode = remember(themeModeValue, useMiuixMonet) {
|
||||||
ThemeResolver.resolveMiuixColorSchemeMode(themeModeValue, useMiuixMonet)
|
ThemeResolver.resolveMiuixColorSchemeMode(themeModeValue, useMiuixMonet)
|
||||||
}
|
}
|
||||||
val paletteStyle =
|
|
||||||
remember(paletteStyleValue) { ThemeResolver.resolvePaletteStyle(paletteStyleValue) }
|
// 加载自定义字体
|
||||||
|
val customFontFamily = remember(ThemeConfig.appFontPath, context) {
|
||||||
|
if (!ThemeConfig.appFontPath.isNullOrEmpty()) {
|
||||||
|
try {
|
||||||
|
val uri = Uri.parse(ThemeConfig.appFontPath)
|
||||||
|
val typeface: Typeface? = if (uri.scheme == "content") {
|
||||||
|
context.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||||
|
Typeface.Builder(it.fileDescriptor).build()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Typeface.createFromFile(uri.path)
|
||||||
|
}
|
||||||
|
typeface?.let {
|
||||||
|
FontFamily(it)
|
||||||
|
}
|
||||||
|
} catch (e: Exception) {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val paletteStyle = remember(paletteStyleValue) {
|
||||||
|
ThemeResolver.resolvePaletteStyle(paletteStyleValue)
|
||||||
|
}
|
||||||
|
|
||||||
val colorScheme =
|
val colorScheme =
|
||||||
remember(
|
remember(
|
||||||
@@ -50,9 +213,33 @@ fun AppTheme(
|
|||||||
isPureBlack,
|
isPureBlack,
|
||||||
customPrimary,
|
customPrimary,
|
||||||
customNightPrimary,
|
customNightPrimary,
|
||||||
|
enableDeepPersonalization,
|
||||||
|
themeColor,
|
||||||
|
secondaryThemeColor,
|
||||||
|
primaryTextColor,
|
||||||
|
secondaryTextColor,
|
||||||
|
themeBackgroundColor,
|
||||||
|
customLabelContainerColor,
|
||||||
paletteStyleValue,
|
paletteStyleValue,
|
||||||
materialVersion
|
materialVersion
|
||||||
) {
|
) {
|
||||||
|
if (enableDeepPersonalization &&
|
||||||
|
(themeColor != 0 ||
|
||||||
|
secondaryThemeColor != 0 ||
|
||||||
|
primaryTextColor != 0 ||
|
||||||
|
secondaryTextColor != 0 ||
|
||||||
|
themeBackgroundColor != 0 ||
|
||||||
|
customLabelContainerColor != 0)) {
|
||||||
|
val userPalette = UserColorPalette(
|
||||||
|
primaryColor = if (themeColor != 0) Color(themeColor) else Color(0xFF6750A4),
|
||||||
|
secondaryColor = if (secondaryThemeColor != 0) Color(secondaryThemeColor) else Color(0xFF625B71),
|
||||||
|
backgroundColor = if (themeBackgroundColor != 0) Color(themeBackgroundColor) else Color(0xFFFEF7FF),
|
||||||
|
primaryFontColor = if (primaryTextColor != 0) Color(primaryTextColor) else Color(0xFF1C1B1F),
|
||||||
|
secondaryFontColor = if (secondaryTextColor != 0) Color(secondaryTextColor) else Color(0xFF49454F),
|
||||||
|
labelContainerColor = if (customLabelContainerColor != 0) Color(customLabelContainerColor) else Color(0xFFF7F2FA)
|
||||||
|
)
|
||||||
|
generateColorScheme(userPalette, darkTheme)
|
||||||
|
} else {
|
||||||
val customSeedColor = if (darkTheme) customNightPrimary else customPrimary
|
val customSeedColor = if (darkTheme) customNightPrimary else customPrimary
|
||||||
ThemeEngine.getColorScheme(
|
ThemeEngine.getColorScheme(
|
||||||
context = context,
|
context = context,
|
||||||
@@ -64,18 +251,15 @@ fun AppTheme(
|
|||||||
customSeedColor = customSeedColor
|
customSeedColor = customSeedColor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val customSeedColor = remember(
|
val themeSeedColor = remember(appThemeMode, colorScheme.primary) {
|
||||||
darkTheme,
|
if (appThemeMode == AppThemeMode.Custom) {
|
||||||
customPrimary,
|
|
||||||
customNightPrimary,
|
|
||||||
colorScheme.primary
|
|
||||||
) {
|
|
||||||
val seed = if (darkTheme) customNightPrimary else customPrimary
|
val seed = if (darkTheme) customNightPrimary else customPrimary
|
||||||
if (seed != 0) Color(seed) else colorScheme.primary
|
if (seed != 0) Color(seed) else colorScheme.primary
|
||||||
|
} else {
|
||||||
|
colorScheme.primary
|
||||||
}
|
}
|
||||||
val themeSeedColor = remember(appThemeMode, customSeedColor, colorScheme.primary) {
|
|
||||||
if (appThemeMode == AppThemeMode.Custom) customSeedColor else colorScheme.primary
|
|
||||||
}
|
}
|
||||||
val miuixPaletteStyle = remember(paletteStyleValue) {
|
val miuixPaletteStyle = remember(paletteStyleValue) {
|
||||||
ThemeResolver.resolveMiuixPaletteStyle(paletteStyleValue)
|
ThemeResolver.resolveMiuixPaletteStyle(paletteStyleValue)
|
||||||
@@ -111,7 +295,9 @@ fun AppTheme(
|
|||||||
themeColors.useDynamicColor &&
|
themeColors.useDynamicColor &&
|
||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||||
) {
|
) {
|
||||||
colorResource(id = android.R.color.system_accent1_500)
|
// colorResource 只能在 @Composable 上下文中调用
|
||||||
|
// 这里使用一个固定颜色作为替代
|
||||||
|
Color(0xFF6750A4)
|
||||||
} else {
|
} else {
|
||||||
themeSeedColor
|
themeSeedColor
|
||||||
}
|
}
|
||||||
@@ -142,13 +328,57 @@ fun AppTheme(
|
|||||||
|
|
||||||
MiuixTheme(controller = controller) {
|
MiuixTheme(controller = controller) {
|
||||||
val miuixStyles = MiuixTheme.textStyles
|
val miuixStyles = MiuixTheme.textStyles
|
||||||
val legadoTypography = remember(miuixStyles) {
|
val legadoTypography = remember<LegadoTypography>(miuixStyles, customFontFamily) {
|
||||||
miuixStylesToM3Typography(miuixStyles).toLegadoTypography()
|
val typography = miuixStylesToM3Typography(miuixStyles)
|
||||||
|
val baseLegadoTypography = typography.toLegadoTypography()
|
||||||
|
if (customFontFamily != null) {
|
||||||
|
baseLegadoTypography.copy(
|
||||||
|
headlineLarge = baseLegadoTypography.headlineLarge.copy(fontFamily = customFontFamily),
|
||||||
|
headlineLargeEmphasized = baseLegadoTypography.headlineLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
headlineMedium = baseLegadoTypography.headlineMedium.copy(fontFamily = customFontFamily),
|
||||||
|
headlineMediumEmphasized = baseLegadoTypography.headlineMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
headlineSmall = baseLegadoTypography.headlineSmall.copy(fontFamily = customFontFamily),
|
||||||
|
headlineSmallEmphasized = baseLegadoTypography.headlineSmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleLarge = baseLegadoTypography.titleLarge.copy(fontFamily = customFontFamily),
|
||||||
|
titleLargeEmphasized = baseLegadoTypography.titleLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleMedium = baseLegadoTypography.titleMedium.copy(fontFamily = customFontFamily),
|
||||||
|
titleMediumEmphasized = baseLegadoTypography.titleMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleSmall = baseLegadoTypography.titleSmall.copy(fontFamily = customFontFamily),
|
||||||
|
titleSmallEmphasized = baseLegadoTypography.titleSmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodyLarge = baseLegadoTypography.bodyLarge.copy(fontFamily = customFontFamily),
|
||||||
|
bodyLargeEmphasized = baseLegadoTypography.bodyLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodyMedium = baseLegadoTypography.bodyMedium.copy(fontFamily = customFontFamily),
|
||||||
|
bodyMediumEmphasized = baseLegadoTypography.bodyMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodySmall = baseLegadoTypography.bodySmall.copy(fontFamily = customFontFamily),
|
||||||
|
bodySmallEmphasized = baseLegadoTypography.bodySmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelLarge = baseLegadoTypography.labelLarge.copy(fontFamily = customFontFamily),
|
||||||
|
labelLargeEmphasized = baseLegadoTypography.labelLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelMedium = baseLegadoTypography.labelMedium.copy(fontFamily = customFontFamily),
|
||||||
|
labelMediumEmphasized = baseLegadoTypography.labelMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelSmall = baseLegadoTypography.labelSmall.copy(fontFamily = customFontFamily),
|
||||||
|
labelSmallEmphasized = baseLegadoTypography.labelSmallEmphasized.copy(fontFamily = customFontFamily)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
baseLegadoTypography
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
val miuixColorScheme = MiuixTheme.colorScheme
|
val miuixColorScheme = MiuixTheme.colorScheme
|
||||||
|
|
||||||
val mappedColorScheme = remember(miuixColorScheme) {
|
val mappedColorScheme = remember(miuixColorScheme) {
|
||||||
|
val customBgColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.themeBackgroundColor != 0) {
|
||||||
|
Color(ThemeConfig.themeBackgroundColor)
|
||||||
|
} else {
|
||||||
|
miuixColorScheme.background
|
||||||
|
}
|
||||||
|
val customFontColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.primaryTextColor != 0) {
|
||||||
|
Color(ThemeConfig.primaryTextColor)
|
||||||
|
} else {
|
||||||
|
miuixColorScheme.onSurface
|
||||||
|
}
|
||||||
|
val customTopBarColor = miuixColorScheme.surface
|
||||||
|
val customNavBarColor = miuixColorScheme.surface
|
||||||
|
|
||||||
LegadoColorScheme(
|
LegadoColorScheme(
|
||||||
primary = miuixColorScheme.primary,
|
primary = miuixColorScheme.primary,
|
||||||
onPrimary = miuixColorScheme.onPrimary,
|
onPrimary = miuixColorScheme.onPrimary,
|
||||||
@@ -166,13 +396,13 @@ fun AppTheme(
|
|||||||
tertiaryContainer = miuixColorScheme.primaryContainer,
|
tertiaryContainer = miuixColorScheme.primaryContainer,
|
||||||
onTertiaryContainer = miuixColorScheme.primaryVariant,
|
onTertiaryContainer = miuixColorScheme.primaryVariant,
|
||||||
|
|
||||||
background = miuixColorScheme.background,
|
background = customBgColor,
|
||||||
onBackground = miuixColorScheme.onBackground,
|
onBackground = customFontColor,
|
||||||
|
|
||||||
surface = miuixColorScheme.surface,
|
surface = miuixColorScheme.surface,
|
||||||
onSurface = miuixColorScheme.onSurface,
|
onSurface = customFontColor,
|
||||||
surfaceVariant = miuixColorScheme.surfaceVariant,
|
surfaceVariant = miuixColorScheme.surfaceVariant,
|
||||||
onSurfaceVariant = miuixColorScheme.onSurfaceSecondary,
|
onSurfaceVariant = customFontColor,
|
||||||
surfaceTint = miuixColorScheme.primary,
|
surfaceTint = miuixColorScheme.primary,
|
||||||
inverseSurface = miuixColorScheme.onSurface,
|
inverseSurface = miuixColorScheme.onSurface,
|
||||||
inverseOnSurface = miuixColorScheme.surface,
|
inverseOnSurface = miuixColorScheme.surface,
|
||||||
@@ -191,8 +421,7 @@ fun AppTheme(
|
|||||||
surfaceContainer = miuixColorScheme.surfaceContainer,
|
surfaceContainer = miuixColorScheme.surfaceContainer,
|
||||||
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
|
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
|
||||||
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
|
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
|
||||||
surfaceContainerLow = miuixColorScheme.secondaryContainer.copy(alpha = 0.32f)
|
surfaceContainerLow = miuixColorScheme.secondaryContainer.copy(alpha = 0.32f),
|
||||||
.compositeOver(miuixColorScheme.surface),
|
|
||||||
surfaceContainerLowest = miuixColorScheme.background,
|
surfaceContainerLowest = miuixColorScheme.background,
|
||||||
|
|
||||||
primaryFixed = miuixColorScheme.primaryContainer,
|
primaryFixed = miuixColorScheme.primaryContainer,
|
||||||
@@ -208,10 +437,9 @@ fun AppTheme(
|
|||||||
onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
|
onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
|
||||||
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
|
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
|
||||||
|
|
||||||
cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f)
|
cardContainer = miuixColorScheme.disabledPrimary,
|
||||||
.compositeOver(miuixColorScheme.surfaceContainer),
|
|
||||||
onCardContainer = miuixColorScheme.primary,
|
onCardContainer = miuixColorScheme.primary,
|
||||||
onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f),
|
onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,17 +451,86 @@ fun AppTheme(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val materialTypography = remember { Typography() }
|
val materialTypography = remember(customFontFamily) {
|
||||||
|
if (customFontFamily != null) {
|
||||||
|
Typography(
|
||||||
|
headlineLarge = Typography().headlineLarge.copy(fontFamily = customFontFamily),
|
||||||
|
headlineMedium = Typography().headlineMedium.copy(fontFamily = customFontFamily),
|
||||||
|
headlineSmall = Typography().headlineSmall.copy(fontFamily = customFontFamily),
|
||||||
|
titleLarge = Typography().titleLarge.copy(fontFamily = customFontFamily),
|
||||||
|
titleMedium = Typography().titleMedium.copy(fontFamily = customFontFamily),
|
||||||
|
titleSmall = Typography().titleSmall.copy(fontFamily = customFontFamily),
|
||||||
|
bodyLarge = Typography().bodyLarge.copy(fontFamily = customFontFamily),
|
||||||
|
bodyMedium = Typography().bodyMedium.copy(fontFamily = customFontFamily),
|
||||||
|
bodySmall = Typography().bodySmall.copy(fontFamily = customFontFamily),
|
||||||
|
labelLarge = Typography().labelLarge.copy(fontFamily = customFontFamily),
|
||||||
|
labelMedium = Typography().labelMedium.copy(fontFamily = customFontFamily),
|
||||||
|
labelSmall = Typography().labelSmall.copy(fontFamily = customFontFamily)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Typography()
|
||||||
|
}
|
||||||
|
}
|
||||||
MaterialExpressiveTheme(
|
MaterialExpressiveTheme(
|
||||||
colorScheme = colorScheme,
|
colorScheme = colorScheme,
|
||||||
typography = materialTypography,
|
typography = materialTypography,
|
||||||
motionScheme = MotionScheme.expressive(),
|
motionScheme = MotionScheme.expressive(),
|
||||||
shapes = Shapes()
|
shapes = Shapes()
|
||||||
) {
|
) {
|
||||||
val legadoTypography = remember(materialTypography) {
|
val legadoTypography = remember(materialTypography, customFontFamily) {
|
||||||
materialTypography.toLegadoTypography()
|
val baseLegadoTypography = materialTypography.toLegadoTypography()
|
||||||
|
if (customFontFamily != null) {
|
||||||
|
baseLegadoTypography.copy(
|
||||||
|
headlineLarge = baseLegadoTypography.headlineLarge.copy(fontFamily = customFontFamily),
|
||||||
|
headlineLargeEmphasized = baseLegadoTypography.headlineLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
headlineMedium = baseLegadoTypography.headlineMedium.copy(fontFamily = customFontFamily),
|
||||||
|
headlineMediumEmphasized = baseLegadoTypography.headlineMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
headlineSmall = baseLegadoTypography.headlineSmall.copy(fontFamily = customFontFamily),
|
||||||
|
headlineSmallEmphasized = baseLegadoTypography.headlineSmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleLarge = baseLegadoTypography.titleLarge.copy(fontFamily = customFontFamily),
|
||||||
|
titleLargeEmphasized = baseLegadoTypography.titleLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleMedium = baseLegadoTypography.titleMedium.copy(fontFamily = customFontFamily),
|
||||||
|
titleMediumEmphasized = baseLegadoTypography.titleMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
titleSmall = baseLegadoTypography.titleSmall.copy(fontFamily = customFontFamily),
|
||||||
|
titleSmallEmphasized = baseLegadoTypography.titleSmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodyLarge = baseLegadoTypography.bodyLarge.copy(fontFamily = customFontFamily),
|
||||||
|
bodyLargeEmphasized = baseLegadoTypography.bodyLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodyMedium = baseLegadoTypography.bodyMedium.copy(fontFamily = customFontFamily),
|
||||||
|
bodyMediumEmphasized = baseLegadoTypography.bodyMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
bodySmall = baseLegadoTypography.bodySmall.copy(fontFamily = customFontFamily),
|
||||||
|
bodySmallEmphasized = baseLegadoTypography.bodySmallEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelLarge = baseLegadoTypography.labelLarge.copy(fontFamily = customFontFamily),
|
||||||
|
labelLargeEmphasized = baseLegadoTypography.labelLargeEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelMedium = baseLegadoTypography.labelMedium.copy(fontFamily = customFontFamily),
|
||||||
|
labelMediumEmphasized = baseLegadoTypography.labelMediumEmphasized.copy(fontFamily = customFontFamily),
|
||||||
|
labelSmall = baseLegadoTypography.labelSmall.copy(fontFamily = customFontFamily),
|
||||||
|
labelSmallEmphasized = baseLegadoTypography.labelSmallEmphasized.copy(fontFamily = customFontFamily)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
baseLegadoTypography
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val semanticColors = remember(colorScheme) {
|
||||||
|
val customBgColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.themeBackgroundColor != 0) {
|
||||||
|
Color(ThemeConfig.themeBackgroundColor)
|
||||||
|
} else {
|
||||||
|
colorScheme.background
|
||||||
|
}
|
||||||
|
val customFontColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.primaryTextColor != 0) {
|
||||||
|
Color(ThemeConfig.primaryTextColor)
|
||||||
|
} else {
|
||||||
|
colorScheme.onSurface
|
||||||
|
}
|
||||||
|
val customTopBarColor = colorScheme.surface
|
||||||
|
val customNavBarColor = colorScheme.surface
|
||||||
|
|
||||||
|
colorScheme.toLegadoColorScheme(
|
||||||
|
customBgColor = customBgColor,
|
||||||
|
customFontColor = customFontColor,
|
||||||
|
customTopBarColor = customTopBarColor,
|
||||||
|
customNavBarColor = customNavBarColor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
val semanticColors = remember(colorScheme) { colorScheme.toLegadoColorScheme() }
|
|
||||||
|
|
||||||
CompositionLocalProvider(
|
CompositionLocalProvider(
|
||||||
LocalLegadoTypography provides legadoTypography,
|
LocalLegadoTypography provides legadoTypography,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.theme
|
package io.legado.app.ui.theme
|
||||||
|
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import dev.chrisbanes.haze.HazeProgressive
|
import dev.chrisbanes.haze.HazeProgressive
|
||||||
@@ -9,6 +10,9 @@ import dev.chrisbanes.haze.hazeSource
|
|||||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.hazeStyle.HazeLegado
|
import io.legado.app.ui.theme.hazeStyle.HazeLegado
|
||||||
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.widget.components.GlassDefaults
|
||||||
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 自动感知全局配置的 HazeSource
|
* 自动感知全局配置的 HazeSource
|
||||||
@@ -27,14 +31,19 @@ fun Modifier.responsiveHazeEffect(
|
|||||||
): Modifier {
|
): Modifier {
|
||||||
val enableBlur = ThemeConfig.enableBlur
|
val enableBlur = ThemeConfig.enableBlur
|
||||||
val enableProgressiveBlur = ThemeConfig.enableProgressiveBlur
|
val enableProgressiveBlur = ThemeConfig.enableProgressiveBlur
|
||||||
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
val containerColor = GlassDefaults.secondaryColorOr {
|
||||||
|
if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
|
||||||
if (!enableBlur) return this
|
if (!enableBlur) return this
|
||||||
|
|
||||||
val style = if (enableProgressiveBlur) {
|
val style = HazeLegado.custom(
|
||||||
HazeLegado.ultraThin()
|
containerColor = containerColor,
|
||||||
} else {
|
blurRadius = ThemeConfig.topBarBlurRadius,
|
||||||
HazeLegado.regular()
|
blurAlpha = ThemeConfig.topBarBlurAlpha
|
||||||
}
|
)
|
||||||
|
|
||||||
return this.hazeEffect(
|
return this.hazeEffect(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -57,10 +66,15 @@ fun Modifier.responsiveHazeEffectFixedStyle(
|
|||||||
state: HazeState
|
state: HazeState
|
||||||
): Modifier {
|
): Modifier {
|
||||||
val enableBlur = ThemeConfig.enableBlur
|
val enableBlur = ThemeConfig.enableBlur
|
||||||
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
val containerColor = GlassDefaults.secondaryColorOr {
|
||||||
|
if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
|
||||||
if (!enableBlur) return this
|
if (!enableBlur) return this
|
||||||
|
|
||||||
val style = HazeLegado.ultraThinPlus()
|
val style = HazeLegado.ultraThinPlus(containerColor = containerColor)
|
||||||
|
|
||||||
return this.hazeEffect(
|
return this.hazeEffect(
|
||||||
state = state,
|
state = state,
|
||||||
@@ -80,10 +94,17 @@ fun Modifier.responsiveHazeEffectFixedStyle(
|
|||||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||||
@Composable
|
@Composable
|
||||||
fun Modifier.regularHazeEffect(state: HazeState): Modifier {
|
fun Modifier.regularHazeEffect(state: HazeState): Modifier {
|
||||||
if (!ThemeConfig.enableBlur) return this
|
val enableBlur = ThemeConfig.enableBlur
|
||||||
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
val containerColor = GlassDefaults.secondaryColorOr {
|
||||||
|
if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface
|
||||||
|
else MaterialTheme.colorScheme.surface
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!enableBlur) return this
|
||||||
|
|
||||||
return this.hazeEffect(
|
return this.hazeEffect(
|
||||||
state = state,
|
state = state,
|
||||||
style = HazeLegado.ultraThin()
|
style = HazeLegado.ultraThin(containerColor = containerColor)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,12 @@ import top.yukonga.miuix.kmp.theme.ColorSchemeMode
|
|||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
import top.yukonga.miuix.kmp.theme.ThemeController
|
import top.yukonga.miuix.kmp.theme.ThemeController
|
||||||
|
|
||||||
fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
fun ColorScheme.toLegadoColorScheme(
|
||||||
|
customBgColor: Color = background,
|
||||||
|
customFontColor: Color = onSurface,
|
||||||
|
customTopBarColor: Color = surface,
|
||||||
|
customNavBarColor: Color = surface
|
||||||
|
): LegadoColorScheme {
|
||||||
return LegadoColorScheme(
|
return LegadoColorScheme(
|
||||||
primary = primary,
|
primary = primary,
|
||||||
onPrimary = onPrimary,
|
onPrimary = onPrimary,
|
||||||
@@ -32,10 +37,10 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
|||||||
onTertiary = onTertiary,
|
onTertiary = onTertiary,
|
||||||
tertiaryContainer = tertiaryContainer,
|
tertiaryContainer = tertiaryContainer,
|
||||||
onTertiaryContainer = onTertiaryContainer,
|
onTertiaryContainer = onTertiaryContainer,
|
||||||
background = background,
|
background = customBgColor,
|
||||||
onBackground = onBackground,
|
onBackground = customFontColor,
|
||||||
surface = surface,
|
surface = surface,
|
||||||
onSurface = onSurface,
|
onSurface = customFontColor,
|
||||||
surfaceVariant = surfaceVariant,
|
surfaceVariant = surfaceVariant,
|
||||||
onSurfaceVariant = onSurfaceVariant,
|
onSurfaceVariant = onSurfaceVariant,
|
||||||
surfaceTint = surfaceTint,
|
surfaceTint = surfaceTint,
|
||||||
@@ -73,6 +78,15 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
||||||
|
return toLegadoColorScheme(
|
||||||
|
customBgColor = background,
|
||||||
|
customFontColor = onSurface,
|
||||||
|
customTopBarColor = surface,
|
||||||
|
customNavBarColor = surface
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun ProvideColorSchemeOverride(
|
fun ProvideColorSchemeOverride(
|
||||||
colorScheme: ColorScheme,
|
colorScheme: ColorScheme,
|
||||||
|
|||||||
@@ -33,20 +33,6 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
|
|||||||
labelLarge = miuixStyles.footnote1.copy(fontSize = 14.sp), // 14.sp
|
labelLarge = miuixStyles.footnote1.copy(fontSize = 14.sp), // 14.sp
|
||||||
labelMedium = miuixStyles.footnote1, // 13.sp
|
labelMedium = miuixStyles.footnote1, // 13.sp
|
||||||
labelSmall = miuixStyles.footnote2, // 11.sp
|
labelSmall = miuixStyles.footnote2, // 11.sp
|
||||||
|
|
||||||
bodyLargeEmphasized = miuixStyles.paragraph.copy(fontWeight = FontWeight.Medium),
|
|
||||||
bodyMediumEmphasized = miuixStyles.body1.copy(fontWeight = FontWeight.Medium),
|
|
||||||
bodySmallEmphasized = miuixStyles.body2.copy(
|
|
||||||
fontSize = 12.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
),
|
|
||||||
|
|
||||||
labelLargeEmphasized = miuixStyles.button.copy(
|
|
||||||
fontSize = 14.sp,
|
|
||||||
fontWeight = FontWeight.Medium
|
|
||||||
),
|
|
||||||
labelMediumEmphasized = miuixStyles.footnote1.copy(fontWeight = FontWeight.Medium),
|
|
||||||
labelSmallEmphasized = miuixStyles.footnote2.copy(fontWeight = FontWeight.Medium)
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,28 +40,28 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
|
|||||||
fun Typography.toLegadoTypography(): LegadoTypography {
|
fun Typography.toLegadoTypography(): LegadoTypography {
|
||||||
return LegadoTypography(
|
return LegadoTypography(
|
||||||
headlineLarge = headlineLarge,
|
headlineLarge = headlineLarge,
|
||||||
headlineLargeEmphasized = headlineLargeEmphasized,
|
headlineLargeEmphasized = headlineLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
headlineMedium = headlineMedium,
|
headlineMedium = headlineMedium,
|
||||||
headlineMediumEmphasized = headlineMediumEmphasized,
|
headlineMediumEmphasized = headlineMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
headlineSmall = headlineSmall,
|
headlineSmall = headlineSmall,
|
||||||
headlineSmallEmphasized = headlineSmallEmphasized,
|
headlineSmallEmphasized = headlineSmall.copy(fontWeight = FontWeight.Medium),
|
||||||
titleLarge = titleLarge,
|
titleLarge = titleLarge,
|
||||||
titleLargeEmphasized = titleLargeEmphasized,
|
titleLargeEmphasized = titleLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
titleMedium = titleMedium,
|
titleMedium = titleMedium,
|
||||||
titleMediumEmphasized = titleMediumEmphasized,
|
titleMediumEmphasized = titleMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
titleSmall = titleSmall,
|
titleSmall = titleSmall,
|
||||||
titleSmallEmphasized = titleSmallEmphasized,
|
titleSmallEmphasized = titleSmall.copy(fontWeight = FontWeight.Medium),
|
||||||
bodyLarge = bodyLarge,
|
bodyLarge = bodyLarge,
|
||||||
bodyLargeEmphasized = bodyLargeEmphasized,
|
bodyLargeEmphasized = bodyLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
bodyMedium = bodyMedium,
|
bodyMedium = bodyMedium,
|
||||||
bodyMediumEmphasized = bodyMediumEmphasized,
|
bodyMediumEmphasized = bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
bodySmall = bodySmall,
|
bodySmall = bodySmall,
|
||||||
bodySmallEmphasized = bodySmallEmphasized,
|
bodySmallEmphasized = bodySmall.copy(fontWeight = FontWeight.Medium),
|
||||||
labelLarge = labelLarge,
|
labelLarge = labelLarge,
|
||||||
labelLargeEmphasized = labelLargeEmphasized,
|
labelLargeEmphasized = labelLarge.copy(fontWeight = FontWeight.Medium),
|
||||||
labelMedium = labelMedium,
|
labelMedium = labelMedium,
|
||||||
labelMediumEmphasized = labelMediumEmphasized,
|
labelMediumEmphasized = labelMedium.copy(fontWeight = FontWeight.Medium),
|
||||||
labelSmall = labelSmall,
|
labelSmall = labelSmall,
|
||||||
labelSmallEmphasized = labelSmallEmphasized
|
labelSmallEmphasized = labelSmall.copy(fontWeight = FontWeight.Medium)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ import androidx.compose.ui.graphics.luminance
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import dev.chrisbanes.haze.HazeStyle
|
import dev.chrisbanes.haze.HazeStyle
|
||||||
import dev.chrisbanes.haze.HazeTint
|
import dev.chrisbanes.haze.HazeTint
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
import io.legado.app.ui.theme.LegadoTheme.composeEngine
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
@@ -30,8 +31,8 @@ object HazeLegado {
|
|||||||
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
||||||
): HazeStyle = hazeLegado(
|
): HazeStyle = hazeLegado(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
lightAlpha = 0.35f,
|
lightAlpha = ThemeConfig.topBarBlurAlpha / 100f * 0.35f / 0.73f,
|
||||||
darkAlpha = 0.55f,
|
darkAlpha = ThemeConfig.topBarBlurAlpha / 100f * 0.55f / 0.8f,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
@@ -40,16 +41,30 @@ object HazeLegado {
|
|||||||
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
||||||
): HazeStyle = hazeLegado(
|
): HazeStyle = hazeLegado(
|
||||||
containerColor = containerColor,
|
containerColor = containerColor,
|
||||||
lightAlpha = 0.73f,
|
lightAlpha = ThemeConfig.topBarBlurAlpha / 100f,
|
||||||
darkAlpha = 0.8f,
|
darkAlpha = ThemeConfig.topBarBlurAlpha / 100f,
|
||||||
|
)
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
@ReadOnlyComposable
|
||||||
|
fun custom(
|
||||||
|
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
||||||
|
blurRadius: Int = ThemeConfig.topBarBlurRadius,
|
||||||
|
blurAlpha: Int = ThemeConfig.topBarBlurAlpha,
|
||||||
|
): HazeStyle = hazeLegado(
|
||||||
|
containerColor = containerColor,
|
||||||
|
blurRadius = blurRadius,
|
||||||
|
lightAlpha = blurAlpha / 100f,
|
||||||
|
darkAlpha = blurAlpha / 100f,
|
||||||
)
|
)
|
||||||
|
|
||||||
private fun hazeLegado(
|
private fun hazeLegado(
|
||||||
containerColor: Color,
|
containerColor: Color,
|
||||||
|
blurRadius: Int = 24,
|
||||||
lightAlpha: Float,
|
lightAlpha: Float,
|
||||||
darkAlpha: Float,
|
darkAlpha: Float,
|
||||||
): HazeStyle = HazeStyle(
|
): HazeStyle = HazeStyle(
|
||||||
blurRadius = 24.dp,
|
blurRadius = blurRadius.dp,
|
||||||
backgroundColor = containerColor,
|
backgroundColor = containerColor,
|
||||||
tint = HazeTint(
|
tint = HazeTint(
|
||||||
containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) lightAlpha else darkAlpha),
|
containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) lightAlpha else darkAlpha),
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ class SimpleCounterView @JvmOverloads constructor(
|
|||||||
var max: Int
|
var max: Int
|
||||||
get() = _max
|
get() = _max
|
||||||
set(value) {
|
set(value) {
|
||||||
_max = value
|
_max = value.coerceAtLeast(1)
|
||||||
_progress = _progress.coerceAtMost(_max)
|
_progress = _progress.coerceAtMost(_max)
|
||||||
updateValue()
|
updateValue()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,34 @@
|
|||||||
package io.legado.app.ui.widget.components
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.RowScope
|
import androidx.compose.foundation.layout.RowScope
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.size
|
||||||
|
import androidx.compose.foundation.layout.width
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
import androidx.compose.material3.BottomAppBarDefaults
|
import androidx.compose.material3.BottomAppBarDefaults
|
||||||
import androidx.compose.material3.NavigationBar
|
import androidx.compose.material3.NavigationBar
|
||||||
import androidx.compose.material3.NavigationBarItem
|
import androidx.compose.material3.NavigationBarItem
|
||||||
import androidx.compose.material3.NavigationBarItemDefaults
|
import androidx.compose.material3.NavigationBarItemDefaults
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.ripple
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
|
import io.legado.app.ui.theme.LocalHazeState
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.theme.regularHazeEffect
|
||||||
import top.yukonga.miuix.kmp.basic.NavigationBarDisplayMode
|
import top.yukonga.miuix.kmp.basic.NavigationBarDisplayMode
|
||||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||||
import top.yukonga.miuix.kmp.basic.NavigationBar as MiuixNavigationBar
|
import top.yukonga.miuix.kmp.basic.NavigationBar as MiuixNavigationBar
|
||||||
@@ -26,29 +42,43 @@ fun AppNavigationBar(
|
|||||||
) {
|
) {
|
||||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||||
val opacity = (ThemeConfig.bottomBarOpacity.coerceIn(0, 100)) / 100f
|
val opacity = (ThemeConfig.bottomBarOpacity.coerceIn(0, 100)) / 100f
|
||||||
|
val hazeState = LocalHazeState.current
|
||||||
|
val hazeModifier = if (hazeState != null) {
|
||||||
|
Modifier.regularHazeEffect(hazeState)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
|
||||||
if (isMiuix) {
|
if (isMiuix) {
|
||||||
val baseColor = GlassDefaults.glassColor(
|
val baseColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
|
Color(ThemeConfig.secondaryThemeColor)
|
||||||
|
} else {
|
||||||
|
GlassDefaults.glassColor(
|
||||||
noBlurColor = MiuixTheme.colorScheme.surface,
|
noBlurColor = MiuixTheme.colorScheme.surface,
|
||||||
blurAlpha = GlassDefaults.DefaultBlurAlpha
|
blurAlpha = GlassDefaults.TransparentAlpha
|
||||||
)
|
)
|
||||||
|
}
|
||||||
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
||||||
|
|
||||||
MiuixNavigationBar(
|
MiuixNavigationBar(
|
||||||
modifier = modifier,
|
modifier = modifier.then(hazeModifier),
|
||||||
color = finalColor,
|
color = finalColor,
|
||||||
mode = miuixMode,
|
mode = miuixMode,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
val baseColor = GlassDefaults.glassColor(
|
val baseColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
|
Color(ThemeConfig.secondaryThemeColor)
|
||||||
|
} else {
|
||||||
|
GlassDefaults.glassColor(
|
||||||
noBlurColor = BottomAppBarDefaults.containerColor,
|
noBlurColor = BottomAppBarDefaults.containerColor,
|
||||||
blurAlpha = GlassDefaults.DefaultBlurAlpha
|
blurAlpha = GlassDefaults.TransparentAlpha
|
||||||
)
|
)
|
||||||
|
}
|
||||||
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
||||||
|
|
||||||
NavigationBar(
|
NavigationBar(
|
||||||
modifier = modifier,
|
modifier = modifier.then(hazeModifier),
|
||||||
containerColor = finalColor,
|
containerColor = finalColor,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
@@ -66,10 +96,35 @@ fun RowScope.AppNavigationBarItem(
|
|||||||
m3IndicatorColor: Color,
|
m3IndicatorColor: Color,
|
||||||
m3ShowLabel: Boolean,
|
m3ShowLabel: Boolean,
|
||||||
m3AlwaysShowLabel: Boolean,
|
m3AlwaysShowLabel: Boolean,
|
||||||
|
useCustomIcon: Boolean = false,
|
||||||
) {
|
) {
|
||||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||||
|
val useCustomIconBox = useCustomIcon && ThemeConfig.useFloatingBottomBar
|
||||||
|
|
||||||
if (isMiuix) {
|
if (useCustomIconBox) {
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.weight(1f)
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clickable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = ripple(bounded = false, radius = 32.dp),
|
||||||
|
onClick = onClick
|
||||||
|
),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
if (selected) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.width(64.dp)
|
||||||
|
.height(32.dp)
|
||||||
|
.clip(RoundedCornerShape(16.dp))
|
||||||
|
.background(m3IndicatorColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
m3Icon()
|
||||||
|
}
|
||||||
|
} else if (isMiuix) {
|
||||||
MiuixNavigationBarItem(
|
MiuixNavigationBarItem(
|
||||||
selected = selected,
|
selected = selected,
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
|
|||||||
@@ -15,9 +15,14 @@ import androidx.compose.runtime.Composable
|
|||||||
import androidx.compose.runtime.CompositionLocalProvider
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
import androidx.compose.runtime.remember
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.blur
|
||||||
import androidx.compose.ui.graphics.Color
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import androidx.compose.ui.layout.ContentScale
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import coil.compose.AsyncImage
|
||||||
import dev.chrisbanes.haze.HazeState
|
import dev.chrisbanes.haze.HazeState
|
||||||
|
import dev.chrisbanes.haze.hazeSource
|
||||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.LocalHazeState
|
import io.legado.app.ui.theme.LocalHazeState
|
||||||
@@ -70,8 +75,10 @@ fun AppScaffold(
|
|||||||
FabPosition.Center -> MiuixFabPosition.Center
|
FabPosition.Center -> MiuixFabPosition.Center
|
||||||
else -> MiuixFabPosition.End
|
else -> MiuixFabPosition.End
|
||||||
}
|
}
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
BackgroundImageContent(isDark = isDark, hazeState = hazeState)
|
||||||
MiuixScaffold(
|
MiuixScaffold(
|
||||||
modifier = modifier,
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
topBar(hazeState)
|
topBar(hazeState)
|
||||||
},
|
},
|
||||||
@@ -98,10 +105,13 @@ fun AppScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
|
Box(modifier = modifier.fillMaxSize()) {
|
||||||
|
BackgroundImageContent(isDark = isDark, hazeState = hazeState)
|
||||||
Scaffold(
|
Scaffold(
|
||||||
modifier = modifier,
|
modifier = Modifier.fillMaxSize(),
|
||||||
topBar = {
|
topBar = {
|
||||||
topBar(hazeState)
|
topBar(hazeState)
|
||||||
},
|
},
|
||||||
@@ -131,4 +141,44 @@ fun AppScaffold(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun BackgroundImageContent(
|
||||||
|
isDark: Boolean,
|
||||||
|
hazeState: HazeState
|
||||||
|
) {
|
||||||
|
val hasImageBg = ThemeConfig.hasImageBg(isDark)
|
||||||
|
val bgImagePath = if (isDark) ThemeConfig.bgImageDark else ThemeConfig.bgImageLight
|
||||||
|
val blur = if (isDark) {
|
||||||
|
ThemeConfig.bgImageNBlurring
|
||||||
|
} else {
|
||||||
|
ThemeConfig.bgImageBlurring
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasImageBg && !bgImagePath.isNullOrBlank()) {
|
||||||
|
if (ThemeConfig.enableBlur) {
|
||||||
|
AsyncImage(
|
||||||
|
model = bgImagePath,
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = org.koin.compose.koinInject(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.hazeSource(hazeState),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
AsyncImage(
|
||||||
|
model = bgImagePath,
|
||||||
|
contentDescription = null,
|
||||||
|
imageLoader = org.koin.compose.koinInject(),
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxSize()
|
||||||
|
.blur(blur.dp),
|
||||||
|
contentScale = ContentScale.Crop
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -65,6 +65,7 @@ import com.kyant.backdrop.shadow.Shadow
|
|||||||
import com.kyant.capsule.ContinuousCapsule
|
import com.kyant.capsule.ContinuousCapsule
|
||||||
import io.legado.app.ui.animation.DampedDragAnimation
|
import io.legado.app.ui.animation.DampedDragAnimation
|
||||||
import io.legado.app.ui.animation.InteractiveHighlight
|
import io.legado.app.ui.animation.InteractiveHighlight
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -110,12 +111,19 @@ fun FloatingBottomBar(
|
|||||||
backdrop: Backdrop,
|
backdrop: Backdrop,
|
||||||
tabsCount: Int,
|
tabsCount: Int,
|
||||||
isBlurEnabled: Boolean = true,
|
isBlurEnabled: Boolean = true,
|
||||||
|
hasCustomIcons: Boolean = false,
|
||||||
content: @Composable RowScope.() -> Unit
|
content: @Composable RowScope.() -> Unit
|
||||||
) {
|
) {
|
||||||
val isInLightTheme = !LegadoTheme.isDark
|
val isInLightTheme = !LegadoTheme.isDark
|
||||||
val accentColor = LegadoTheme.colorScheme.primary
|
val accentColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.themeColor != 0) {
|
||||||
val containerColor = if (isBlurEnabled) {
|
Color(ThemeConfig.themeColor)
|
||||||
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.4f)
|
} else {
|
||||||
|
LegadoTheme.colorScheme.primary
|
||||||
|
}
|
||||||
|
val containerColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
|
Color(ThemeConfig.secondaryThemeColor).copy(alpha = if (isBlurEnabled) ThemeConfig.bottomBarBlurAlpha / 100f else 1f)
|
||||||
|
} else if (isBlurEnabled) {
|
||||||
|
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = ThemeConfig.bottomBarBlurAlpha / 100f)
|
||||||
} else {
|
} else {
|
||||||
LegadoTheme.colorScheme.surfaceContainer
|
LegadoTheme.colorScheme.surfaceContainer
|
||||||
}
|
}
|
||||||
@@ -250,8 +258,8 @@ fun FloatingBottomBar(
|
|||||||
effects = {
|
effects = {
|
||||||
if (isBlurEnabled) {
|
if (isBlurEnabled) {
|
||||||
vibrancy()
|
vibrancy()
|
||||||
blur(8f.dp.toPx())
|
blur(ThemeConfig.bottomBarBlurRadius.toFloat().dp.toPx())
|
||||||
lens(24f.dp.toPx(), 24f.dp.toPx())
|
lens(ThemeConfig.bottomBarLensRadius.dp.toPx(), ThemeConfig.bottomBarLensRadius.dp.toPx())
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
highlight = {
|
highlight = {
|
||||||
@@ -307,8 +315,8 @@ fun FloatingBottomBar(
|
|||||||
if (isBlurEnabled) {
|
if (isBlurEnabled) {
|
||||||
val progress = dampedDragAnimation.pressProgress
|
val progress = dampedDragAnimation.pressProgress
|
||||||
vibrancy()
|
vibrancy()
|
||||||
blur(8f.dp.toPx())
|
blur(ThemeConfig.bottomBarBlurRadius.toFloat().dp.toPx())
|
||||||
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
|
lens(ThemeConfig.bottomBarLensRadius.dp.toPx() * progress, ThemeConfig.bottomBarLensRadius.dp.toPx() * progress)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
highlight = {
|
highlight = {
|
||||||
@@ -331,7 +339,10 @@ fun FloatingBottomBar(
|
|||||||
)
|
)
|
||||||
.height(56.dp)
|
.height(56.dp)
|
||||||
.padding(horizontal = 4.dp)
|
.padding(horizontal = 4.dp)
|
||||||
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
|
.then(
|
||||||
|
if (hasCustomIcons) Modifier
|
||||||
|
else Modifier.graphicsLayer(colorFilter = ColorFilter.tint(accentColor))
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
content = content
|
content = content
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -20,7 +20,15 @@ object GlassDefaults {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 默认的玻璃效果透明度值,方便全局修改
|
@Composable
|
||||||
|
fun secondaryColorOr(fallback: @Composable () -> Color): Color {
|
||||||
|
return if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||||
|
Color(ThemeConfig.secondaryThemeColor)
|
||||||
|
} else {
|
||||||
|
fallback()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
val DefaultBlurAlpha = 0.36f
|
val DefaultBlurAlpha = 0.36f
|
||||||
val ThickBlurAlpha = 0.72f
|
val ThickBlurAlpha = 0.72f
|
||||||
val TransparentAlpha = 0f
|
val TransparentAlpha = 0f
|
||||||
|
|||||||
@@ -1,35 +1,67 @@
|
|||||||
package io.legado.app.ui.widget.components
|
package io.legado.app.ui.widget.components
|
||||||
|
|
||||||
import androidx.compose.animation.animateContentSize
|
import androidx.compose.animation.animateContentSize
|
||||||
|
import androidx.compose.foundation.border
|
||||||
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.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.CompositionLocalProvider
|
||||||
|
import androidx.compose.runtime.compositionLocalOf
|
||||||
|
import androidx.compose.runtime.mutableIntStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.draw.clip
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.widget.components.divider.SettingItemDivider
|
||||||
import io.legado.app.ui.widget.components.title.AdaptiveTitle
|
import io.legado.app.ui.widget.components.title.AdaptiveTitle
|
||||||
import top.yukonga.miuix.kmp.basic.Card as MiuixCard
|
import top.yukonga.miuix.kmp.basic.Card as MiuixCard
|
||||||
|
|
||||||
/**
|
val LocalSplicedColumnGroupState = compositionLocalOf { SplicedColumnGroupState(
|
||||||
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
enableItemDivider = false,
|
||||||
* Edit by @Kudomaga
|
currentIndex = { 0 },
|
||||||
* @param title the title of the group
|
incrementIndex = { }
|
||||||
* @param content a list of composable that will be displayed in the group
|
) }
|
||||||
* @param modifier Modifier
|
|
||||||
*/
|
data class SplicedColumnGroupState(
|
||||||
|
val enableItemDivider: Boolean,
|
||||||
|
val currentIndex: () -> Int,
|
||||||
|
val incrementIndex: () -> Unit
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun SplicedColumnGroup(
|
fun SplicedColumnGroup(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
title: String = "",
|
title: String = "",
|
||||||
content: @Composable ColumnScope.() -> Unit,
|
items: @Composable ColumnScope.() -> Unit,
|
||||||
) {
|
) {
|
||||||
val composeEngine = LegadoTheme.composeEngine
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
val enableBorder = ThemeConfig.enableContainerBorder
|
||||||
|
val borderWidth = ThemeConfig.containerBorderWidth.dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else {
|
||||||
|
MaterialTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
val enableItemDivider = ThemeConfig.enableItemDivider
|
||||||
|
val currentIndex = remember { mutableIntStateOf(0) }
|
||||||
|
|
||||||
|
val groupState = remember {
|
||||||
|
SplicedColumnGroupState(
|
||||||
|
enableItemDivider = enableItemDivider,
|
||||||
|
currentIndex = { currentIndex.intValue },
|
||||||
|
incrementIndex = { currentIndex.intValue++ }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
Column(modifier = modifier.padding(top = 8.dp, bottom = 8.dp)) {
|
Column(modifier = modifier.padding(top = 8.dp, bottom = 8.dp)) {
|
||||||
if (title.isNotEmpty()) {
|
if (title.isNotEmpty()) {
|
||||||
AdaptiveTitle(
|
AdaptiveTitle(
|
||||||
@@ -38,6 +70,7 @@ fun SplicedColumnGroup(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
CompositionLocalProvider(LocalSplicedColumnGroupState provides groupState) {
|
||||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||||
MiuixCard {
|
MiuixCard {
|
||||||
Column(
|
Column(
|
||||||
@@ -46,20 +79,61 @@ fun SplicedColumnGroup(
|
|||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
.clip(RoundedCornerShape(16.dp))
|
.clip(RoundedCornerShape(16.dp))
|
||||||
) {
|
) {
|
||||||
content()
|
currentIndex.intValue = 0
|
||||||
|
items()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxWidth()
|
.fillMaxWidth()
|
||||||
.animateContentSize()
|
.animateContentSize()
|
||||||
.clip(RoundedCornerShape(16.dp)),
|
.clip(RoundedCornerShape(16.dp))
|
||||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
.then(
|
||||||
|
if (enableBorder) {
|
||||||
|
Modifier.border(
|
||||||
|
width = borderWidth,
|
||||||
|
color = borderColor,
|
||||||
|
shape = RoundedCornerShape(16.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Modifier
|
||||||
|
}
|
||||||
|
),
|
||||||
|
verticalArrangement = if (enableBorder) {
|
||||||
|
Arrangement.spacedBy(0.dp)
|
||||||
|
} else {
|
||||||
|
Arrangement.spacedBy(2.dp)
|
||||||
|
}
|
||||||
) {
|
) {
|
||||||
content()
|
currentIndex.intValue = 0
|
||||||
|
items()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SplicedColumnDivider() {
|
||||||
|
val groupState = LocalSplicedColumnGroupState.current
|
||||||
|
if (groupState.enableItemDivider && groupState.currentIndex() > 0) {
|
||||||
|
SettingItemDivider()
|
||||||
|
}
|
||||||
|
groupState.incrementIndex()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItemWithDivider(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
content: @Composable ColumnScope.() -> Unit
|
||||||
|
) {
|
||||||
|
val groupState = LocalSplicedColumnGroupState.current
|
||||||
|
if (groupState.enableItemDivider && groupState.currentIndex() > 0) {
|
||||||
|
SettingItemDivider()
|
||||||
|
}
|
||||||
|
Column(modifier = modifier.fillMaxWidth()) {
|
||||||
|
groupState.incrementIndex()
|
||||||
|
content()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package io.legado.app.ui.widget.components.card
|
|||||||
|
|
||||||
import androidx.compose.foundation.BorderStroke
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.combinedClickable
|
import androidx.compose.foundation.combinedClickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.ColumnScope
|
import androidx.compose.foundation.layout.ColumnScope
|
||||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
@@ -40,7 +41,26 @@ private fun BaseCard(
|
|||||||
) {
|
) {
|
||||||
val resolvedContainerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer)
|
val resolvedContainerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer)
|
||||||
.let { it.copy(alpha = it.alpha * alpha) }
|
.let { it.copy(alpha = it.alpha * alpha) }
|
||||||
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
val isTransparent = containerColor == Color.Transparent
|
||||||
|
val shape = RoundedCornerShape(cornerRadius)
|
||||||
|
|
||||||
|
if (isTransparent) {
|
||||||
|
val clickableModifier = if (onClick != null || onLongClick != null) {
|
||||||
|
modifier
|
||||||
|
.clip(shape)
|
||||||
|
.combinedClickable(
|
||||||
|
onClick = { onClick?.invoke() },
|
||||||
|
onLongClick = onLongClick
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
modifier.clip(shape)
|
||||||
|
}
|
||||||
|
Box(
|
||||||
|
modifier = clickableModifier
|
||||||
|
) {
|
||||||
|
Column(content = content)
|
||||||
|
}
|
||||||
|
} else if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
|
||||||
val colors = MiuixCardDefaults.defaultColors(
|
val colors = MiuixCardDefaults.defaultColors(
|
||||||
color = resolvedContainerColor,
|
color = resolvedContainerColor,
|
||||||
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
|
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.widget.components.card
|
package io.legado.app.ui.widget.components.card
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
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.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -35,7 +36,8 @@ fun TextCard(
|
|||||||
verticalPadding: Dp = 4.dp,
|
verticalPadding: Dp = 4.dp,
|
||||||
iconSize: Dp = 14.dp,
|
iconSize: Dp = 14.dp,
|
||||||
spacing: Dp = 4.dp,
|
spacing: Dp = 4.dp,
|
||||||
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized
|
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized,
|
||||||
|
border: BorderStroke? = null
|
||||||
) {
|
) {
|
||||||
val defaultBackground = LegadoTheme.colorScheme.surfaceContainer
|
val defaultBackground = LegadoTheme.colorScheme.surfaceContainer
|
||||||
val defaultContent = LegadoTheme.colorScheme.onSurface
|
val defaultContent = LegadoTheme.colorScheme.onSurface
|
||||||
@@ -48,7 +50,8 @@ fun TextCard(
|
|||||||
cornerRadius = cornerRadius,
|
cornerRadius = cornerRadius,
|
||||||
containerColor = finalBackgroundColor,
|
containerColor = finalBackgroundColor,
|
||||||
contentColor = finalContentColor,
|
contentColor = finalContentColor,
|
||||||
onClick = onClick
|
onClick = onClick,
|
||||||
|
border = border
|
||||||
) {
|
) {
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.padding(
|
modifier = Modifier.padding(
|
||||||
|
|||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package io.legado.app.ui.widget.components.divider
|
||||||
|
|
||||||
|
import androidx.compose.foundation.background
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.CircleShape
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
import androidx.compose.ui.unit.Dp
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun SettingItemDivider(
|
||||||
|
modifier: Modifier = Modifier
|
||||||
|
) {
|
||||||
|
if (!ThemeConfig.enableItemDivider) return
|
||||||
|
|
||||||
|
val thickness = ThemeConfig.itemDividerWidth.dp
|
||||||
|
val lengthPercent = ThemeConfig.itemDividerLength / 100f
|
||||||
|
val dividerColor = if (ThemeConfig.itemDividerColor != 0) {
|
||||||
|
Color(ThemeConfig.itemDividerColor)
|
||||||
|
} else {
|
||||||
|
Color.Gray.copy(alpha = 0.3f)
|
||||||
|
}
|
||||||
|
|
||||||
|
Box(
|
||||||
|
modifier = modifier
|
||||||
|
.fillMaxWidth(),
|
||||||
|
contentAlignment = Alignment.Center
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth(lengthPercent)
|
||||||
|
.height(thickness)
|
||||||
|
.clip(CircleShape)
|
||||||
|
.background(dividerColor)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
package io.legado.app.ui.widget.components.explore
|
package io.legado.app.ui.widget.components.explore
|
||||||
|
|
||||||
|
import androidx.compose.foundation.BorderStroke
|
||||||
import androidx.compose.foundation.layout.Box
|
import androidx.compose.foundation.layout.Box
|
||||||
import androidx.compose.foundation.layout.fillMaxWidth
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
@@ -16,6 +17,7 @@ import androidx.compose.ui.text.style.TextOverflow
|
|||||||
import androidx.compose.ui.unit.Dp
|
import androidx.compose.ui.unit.Dp
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.widget.components.card.GlassCard
|
import io.legado.app.ui.widget.components.card.GlassCard
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
@@ -38,13 +40,24 @@ fun ExploreKindItem(
|
|||||||
|
|
||||||
val cornerRadius = 12.dp
|
val cornerRadius = 12.dp
|
||||||
|
|
||||||
|
val enableBorder = ThemeConfig.enableDeepPersonalization && ThemeConfig.enableContainerBorder
|
||||||
|
val borderWidth = (ThemeConfig.containerBorderWidth - 0.8f).coerceAtLeast(0.1f).dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
androidx.compose.ui.graphics.Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else {
|
||||||
|
LegadoTheme.colorScheme.outline
|
||||||
|
}
|
||||||
|
|
||||||
if (isClickable) {
|
if (isClickable) {
|
||||||
GlassCard(
|
GlassCard(
|
||||||
onClick = onClick,
|
onClick = onClick,
|
||||||
cornerRadius = cornerRadius,
|
cornerRadius = cornerRadius,
|
||||||
containerColor = backgroundColor,
|
containerColor = backgroundColor,
|
||||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||||
modifier = modifier
|
modifier = modifier,
|
||||||
|
border = if (enableBorder) {
|
||||||
|
BorderStroke(borderWidth, borderColor)
|
||||||
|
} else null
|
||||||
) {
|
) {
|
||||||
KindText(
|
KindText(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
@@ -58,7 +71,9 @@ fun ExploreKindItem(
|
|||||||
containerColor = backgroundColor,
|
containerColor = backgroundColor,
|
||||||
contentColor = LegadoTheme.colorScheme.primary,
|
contentColor = LegadoTheme.colorScheme.primary,
|
||||||
modifier = modifier,
|
modifier = modifier,
|
||||||
border = CardDefaults.outlinedCardBorder()
|
border = if (enableBorder) {
|
||||||
|
BorderStroke(borderWidth, borderColor)
|
||||||
|
} else BorderStroke(1.dp, LegadoTheme.colorScheme.outline)
|
||||||
) {
|
) {
|
||||||
KindText(
|
KindText(
|
||||||
text = displayText,
|
text = displayText,
|
||||||
|
|||||||
+7
-2
@@ -39,6 +39,7 @@ import io.legado.app.App
|
|||||||
import io.legado.app.data.entities.rule.ExploreKind
|
import io.legado.app.data.entities.rule.ExploreKind
|
||||||
import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
|
import io.legado.app.ui.widget.components.explore.ExploreKindUiUseCase
|
||||||
import io.legado.app.help.source.getExploreInfoMap
|
import io.legado.app.help.source.getExploreInfoMap
|
||||||
|
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenu
|
||||||
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
@@ -321,7 +322,11 @@ private fun ExploreKindCompactTextField(
|
|||||||
val interactionSource = remember { MutableInteractionSource() }
|
val interactionSource = remember { MutableInteractionSource() }
|
||||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||||
val shape = RoundedCornerShape(10.dp)
|
val shape = RoundedCornerShape(10.dp)
|
||||||
val borderColor = if (isFocused) {
|
val enableBorder = ThemeConfig.enableDeepPersonalization && ThemeConfig.enableContainerBorder
|
||||||
|
val borderWidth = ThemeConfig.containerBorderWidth.dp
|
||||||
|
val borderColor = if (ThemeConfig.containerBorderColor != 0) {
|
||||||
|
Color(ThemeConfig.containerBorderColor)
|
||||||
|
} else if (isFocused) {
|
||||||
LegadoTheme.colorScheme.primary
|
LegadoTheme.colorScheme.primary
|
||||||
} else {
|
} else {
|
||||||
Color.Transparent
|
Color.Transparent
|
||||||
@@ -338,7 +343,7 @@ private fun ExploreKindCompactTextField(
|
|||||||
.height(34.dp)
|
.height(34.dp)
|
||||||
.clip(shape)
|
.clip(shape)
|
||||||
.background(backgroundColor)
|
.background(backgroundColor)
|
||||||
.border(width = 1.dp, color = borderColor, shape = shape),
|
.border(width = borderWidth, color = borderColor, shape = shape),
|
||||||
decorationBox = { innerTextField ->
|
decorationBox = { innerTextField ->
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
|
|||||||
@@ -96,7 +96,7 @@ object AppIcons {
|
|||||||
|
|
||||||
val Replay: ImageVector
|
val Replay: ImageVector
|
||||||
@Composable
|
@Composable
|
||||||
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
|
get() = if (isMiuix) MiuixIcons.Refresh else Icons.Default.Replay
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
fun mainDestination(destination: MainDestination, selected: Boolean): ImageVector {
|
||||||
|
|||||||
+3
@@ -9,6 +9,7 @@ import androidx.compose.ui.Modifier
|
|||||||
import androidx.compose.ui.graphics.vector.ImageVector
|
import androidx.compose.ui.graphics.vector.ImageVector
|
||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnDivider
|
||||||
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
||||||
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
|
|
||||||
@@ -24,6 +25,8 @@ fun ClickableSettingItem(
|
|||||||
onClick: () -> Unit
|
onClick: () -> Unit
|
||||||
) {
|
) {
|
||||||
val composeEngine = LegadoTheme.composeEngine
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
SplicedColumnDivider()
|
||||||
|
|
||||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||||
ArrowPreference(
|
ArrowPreference(
|
||||||
title = title,
|
title = title,
|
||||||
|
|||||||
+38
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
|||||||
import androidx.compose.foundation.layout.size
|
import androidx.compose.foundation.layout.size
|
||||||
import androidx.compose.material.icons.Icons
|
import androidx.compose.material.icons.Icons
|
||||||
import androidx.compose.material.icons.filled.Check
|
import androidx.compose.material.icons.filled.Check
|
||||||
|
import androidx.compose.material.icons.filled.ChevronRight
|
||||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||||
import androidx.compose.material3.Icon
|
import androidx.compose.material3.Icon
|
||||||
import androidx.compose.material3.MaterialTheme
|
import androidx.compose.material3.MaterialTheme
|
||||||
@@ -32,6 +33,8 @@ import io.legado.app.ui.widget.components.ValueStepper
|
|||||||
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.menuItem.RoundDropdownMenuItem
|
import io.legado.app.ui.widget.components.menuItem.RoundDropdownMenuItem
|
||||||
import top.yukonga.miuix.kmp.basic.BasicComponent
|
import top.yukonga.miuix.kmp.basic.BasicComponent
|
||||||
|
import top.yukonga.miuix.kmp.basic.BasicComponentDefaults
|
||||||
|
import top.yukonga.miuix.kmp.preference.ArrowPreference
|
||||||
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
import top.yukonga.miuix.kmp.preference.SwitchPreference
|
||||||
import top.yukonga.miuix.kmp.preference.WindowDropdownPreference
|
import top.yukonga.miuix.kmp.preference.WindowDropdownPreference
|
||||||
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
|
import top.yukonga.miuix.kmp.basic.Slider as MiuixSlider
|
||||||
@@ -251,3 +254,38 @@ fun CompactSwitchSettingItem(
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun CompactClickableSettingItem(
|
||||||
|
title: String,
|
||||||
|
description: String? = null,
|
||||||
|
imageVector: ImageVector? = null,
|
||||||
|
color: Color? = MaterialTheme.colorScheme.surface,
|
||||||
|
shape: Shape = MaterialTheme.shapes.small,
|
||||||
|
onClick: () -> Unit
|
||||||
|
) {
|
||||||
|
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||||
|
ArrowPreference(
|
||||||
|
title = title,
|
||||||
|
summary = description,
|
||||||
|
insideMargin = BasicComponentDefaults.InsideMargin,
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
SettingItem(
|
||||||
|
title = title,
|
||||||
|
description = description,
|
||||||
|
imageVector = imageVector,
|
||||||
|
color = color,
|
||||||
|
shape = shape,
|
||||||
|
trailingContent = {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Default.ChevronRight,
|
||||||
|
contentDescription = null,
|
||||||
|
tint = MaterialTheme.colorScheme.onSurfaceVariant
|
||||||
|
)
|
||||||
|
},
|
||||||
|
onClick = onClick
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import io.legado.app.R
|
|||||||
import io.legado.app.ui.theme.LegadoTheme
|
import io.legado.app.ui.theme.LegadoTheme
|
||||||
import io.legado.app.ui.theme.ThemeResolver
|
import io.legado.app.ui.theme.ThemeResolver
|
||||||
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
import io.legado.app.ui.widget.components.button.ConfirmDismissButtonsRow
|
||||||
|
import io.legado.app.ui.widget.components.SplicedColumnDivider
|
||||||
import io.legado.app.ui.widget.components.text.AppText
|
import io.legado.app.ui.widget.components.text.AppText
|
||||||
import top.yukonga.miuix.kmp.basic.BasicComponent
|
import top.yukonga.miuix.kmp.basic.BasicComponent
|
||||||
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
|
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
|
||||||
@@ -46,6 +47,7 @@ fun InputSettingItem(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val composeEngine = LegadoTheme.composeEngine
|
val composeEngine = LegadoTheme.composeEngine
|
||||||
|
SplicedColumnDivider()
|
||||||
|
|
||||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||||
Column(modifier = Modifier.fillMaxWidth()) {
|
Column(modifier = Modifier.fillMaxWidth()) {
|
||||||
|
|||||||
Loaded 100 of 131 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user