更多自定义设置 @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:
@@ -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/\")",
|
||||
"WebSearch",
|
||||
"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
|
||||
|
||||
```bash
|
||||
# Quick compile check (Kotlin only, no dex/package — fastest for verifying code compiles)
|
||||
.\gradlew.bat :app:compileAppDebugKotlin
|
||||
|
||||
# Assemble all variants
|
||||
./gradlew assembleAppRelease
|
||||
|
||||
|
||||
@@ -278,5 +278,6 @@ dependencies {
|
||||
implementation(libs.miuix.shapes.android)
|
||||
implementation(libs.capsule)
|
||||
implementation(libs.backdrop)
|
||||
implementation(libs.lyricViewx)
|
||||
implementation(libs.timber)
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@ package io.legado.app.constant
|
||||
object PreferKey {
|
||||
const val language = "language"
|
||||
const val fontScale = "fontScale"
|
||||
const val appFontPath = "appFontPath"
|
||||
const val appTheme = "app_theme"
|
||||
const val themeMode = "themeMode"
|
||||
const val userAgent = "userAgent"
|
||||
@@ -101,6 +102,7 @@ object PreferKey {
|
||||
const val replaceEnableDefault = "replaceEnableDefault"
|
||||
const val showBrightnessView = "showBrightnessView"
|
||||
const val useUnderline = "useUnderline"
|
||||
const val regexColorRules = "regexColorRules"
|
||||
const val adaptSpecialStyle = "adaptSpecialStyle"
|
||||
const val autoClearExpired = "autoClearExpired"
|
||||
const val autoChangeSource = "autoChangeSource"
|
||||
@@ -193,9 +195,32 @@ object PreferKey {
|
||||
const val bottomBarOpacity = "bottomBarOpacity"
|
||||
const val enableBlur = "enableBlur"
|
||||
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 cBackground = "colorBackground"
|
||||
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 bgImageBlurring = "backgroundImageBlurring"
|
||||
const val cNPrimary = "colorPrimaryNight"
|
||||
@@ -274,8 +299,16 @@ object PreferKey {
|
||||
|
||||
const val showTip = "showTip"
|
||||
const val showBookCount = "showBookCount"
|
||||
const val showBookIntro = "showBookIntro"
|
||||
const val bookshelfIntroMaxLines = "bookshelfIntroMaxLines"
|
||||
const val sliderVibrator = "sliderVibrator"
|
||||
const val selectVibrator = "selectVibrator"
|
||||
const val audioCacheCleanTime = "audioCacheCleanTime"
|
||||
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(
|
||||
version = 85,
|
||||
version = 86,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -121,7 +121,8 @@ val appDb by lazy {
|
||||
AutoMigration(from = 81, to = 82),
|
||||
AutoMigration(from = 82, to = 83),
|
||||
AutoMigration(from = 83, to = 84),
|
||||
AutoMigration(from = 84, to = 85)
|
||||
AutoMigration(from = 84, to = 85),
|
||||
AutoMigration(from = 85, to = 86)
|
||||
]
|
||||
)
|
||||
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.map
|
||||
|
||||
data class GroupBookCount(
|
||||
val groupId: Long,
|
||||
val count: Int
|
||||
)
|
||||
|
||||
@Dao
|
||||
interface BookDao {
|
||||
|
||||
@@ -91,7 +96,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
where type & ${BookType.text} > 0
|
||||
and type & ${BookType.local} = 0
|
||||
@@ -125,7 +132,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
ORDER BY durChapterTime DESC
|
||||
"""
|
||||
@@ -156,8 +165,10 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
FROM books
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE type & ${BookType.audio} > 0
|
||||
"""
|
||||
)
|
||||
@@ -187,7 +198,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE type & ${BookType.local} > 0
|
||||
"""
|
||||
@@ -223,7 +236,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
|
||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
||||
@@ -260,7 +275,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
where type & ${BookType.local} > 0
|
||||
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
|
||||
@@ -292,7 +309,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE (`group` & :group) > 0
|
||||
"""
|
||||
@@ -325,7 +344,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE name like '%'||:key||'%' or author like '%'||:key||'%' or originName like '%'||:key||'%'
|
||||
"""
|
||||
@@ -356,7 +377,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
where type & ${BookType.updateError} > 0
|
||||
order by durChapterTime desc
|
||||
@@ -388,7 +411,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE durChapterIndex = 0 AND durChapterPos = 0
|
||||
"""
|
||||
@@ -419,7 +444,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE totalChapterNum > 0 AND durChapterIndex >= totalChapterNum - 1
|
||||
"""
|
||||
@@ -450,7 +477,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE totalChapterNum > 0 AND durChapterIndex > 0 AND durChapterIndex < totalChapterNum - 1
|
||||
"""
|
||||
@@ -481,7 +510,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE type & ${BookType.image} > 0
|
||||
"""
|
||||
@@ -512,7 +543,9 @@ interface BookDao {
|
||||
type,
|
||||
`group`,
|
||||
`order`,
|
||||
canUpdate
|
||||
canUpdate,
|
||||
ifnull(customIntro, intro) as intro,
|
||||
kind
|
||||
FROM books
|
||||
WHERE type & ${BookType.text} > 0
|
||||
"""
|
||||
@@ -627,4 +660,268 @@ interface BookDao {
|
||||
|
||||
@Query("delete from books where type & ${BookType.notShelf} > 0")
|
||||
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,
|
||||
/**注入js**/
|
||||
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")
|
||||
var lastUpdateTime: Long = 0,
|
||||
@ColumnInfo(defaultValue = "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")
|
||||
var redirectPolicy: String = "ASK_CROSS_ORIGIN"
|
||||
@@ -143,6 +163,15 @@ data class RssSource(
|
||||
&& equal(variableComment, source.variableComment)
|
||||
&& equal(style, source.style)
|
||||
&& 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 {
|
||||
|
||||
@@ -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?) {
|
||||
rhinoContext.ensureActive()
|
||||
rhinoContextOrNull?.ensureActive()
|
||||
appCtx.toastForJs("${getSource()?.getTag()}: ${msg.toString()}")
|
||||
}
|
||||
|
||||
@@ -1113,7 +1113,7 @@ interface JsExtensions : JsEncodeUtils {
|
||||
* 弹窗提示 停留时间较长
|
||||
*/
|
||||
fun longToast(msg: Any?) {
|
||||
rhinoContext.ensureActive()
|
||||
rhinoContextOrNull?.ensureActive()
|
||||
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.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefInt
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.hexString
|
||||
import io.legado.app.utils.printOnDebug
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import io.legado.app.utils.putPrefInt
|
||||
import io.legado.app.utils.putPrefString
|
||||
import io.legado.app.utils.resizeAndRecycle
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
@@ -226,6 +228,12 @@ object ReadBookConfig {
|
||||
appCtx.putPrefBoolean(PreferKey.shareLayout, value)
|
||||
}
|
||||
}
|
||||
|
||||
val regexColorRules: ArrayList<RegexColorRule> get() = durConfig.regexColorRules
|
||||
|
||||
fun saveRegexColorRules() {
|
||||
save()
|
||||
}
|
||||
|
||||
/**
|
||||
* 两端对齐
|
||||
@@ -260,6 +268,36 @@ object ReadBookConfig {
|
||||
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
|
||||
get() = config.textBold
|
||||
set(value) {
|
||||
@@ -393,6 +431,12 @@ object ReadBookConfig {
|
||||
config.titleBottomSpacing = value
|
||||
}
|
||||
|
||||
var titleColor: Int
|
||||
get() = config.titleColor
|
||||
set(value) {
|
||||
config.titleColor = value
|
||||
}
|
||||
|
||||
var paragraphIndent: String
|
||||
get() = config.paragraphIndent
|
||||
set(value) {
|
||||
@@ -417,6 +461,12 @@ object ReadBookConfig {
|
||||
config.underlinePadding = value
|
||||
}
|
||||
|
||||
var underlineExtend: Boolean
|
||||
get() = config.underlineExtend
|
||||
set(value) {
|
||||
config.underlineExtend = value
|
||||
}
|
||||
|
||||
var dottedLine: Boolean
|
||||
get() = config.dottedLine
|
||||
set(value) {
|
||||
@@ -544,9 +594,14 @@ object ReadBookConfig {
|
||||
}
|
||||
|
||||
fun getExportConfig(): Config {
|
||||
val exportConfig = durConfig.copy()
|
||||
val exportConfig = durConfig.copy(regexColorRules = ArrayList(durConfig.regexColorRules.map { it.copy() }))
|
||||
if (shareLayout) {
|
||||
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.textSize = shareConfig.textSize
|
||||
exportConfig.letterSpacing = shareConfig.letterSpacing
|
||||
@@ -556,6 +611,7 @@ object ReadBookConfig {
|
||||
exportConfig.titleSize = shareConfig.titleSize
|
||||
exportConfig.titleTopSpacing = shareConfig.titleTopSpacing
|
||||
exportConfig.titleBottomSpacing = shareConfig.titleBottomSpacing
|
||||
exportConfig.titleColor = shareConfig.titleColor
|
||||
exportConfig.paddingBottom = shareConfig.paddingBottom
|
||||
exportConfig.paddingLeft = shareConfig.paddingLeft
|
||||
exportConfig.paddingRight = shareConfig.paddingRight
|
||||
@@ -576,7 +632,8 @@ object ReadBookConfig {
|
||||
exportConfig.tipFooterLeft = shareConfig.tipFooterLeft
|
||||
exportConfig.tipFooterMiddle = shareConfig.tipFooterMiddle
|
||||
exportConfig.tipFooterRight = shareConfig.tipFooterRight
|
||||
exportConfig.tipColor = shareConfig.tipColor
|
||||
exportConfig.tipHeaderColor = shareConfig.tipHeaderColor
|
||||
exportConfig.tipFooterColor = shareConfig.tipFooterColor
|
||||
exportConfig.headerMode = shareConfig.headerMode
|
||||
// MD3专有属性
|
||||
exportConfig.footerMode = shareConfig.footerMode
|
||||
@@ -628,6 +685,20 @@ object ReadBookConfig {
|
||||
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) {
|
||||
val bgName = FileUtils.getName(config.bgStr)
|
||||
config.bgStr = bgName
|
||||
@@ -688,6 +759,11 @@ object ReadBookConfig {
|
||||
private var pageAnim: Int = 0,//翻页动画
|
||||
private var pageAnimEInk: Int = 4,
|
||||
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 textSize: Int = 20,//文字大小
|
||||
var textItalic: Boolean = false,// 是否启用斜体
|
||||
@@ -704,6 +780,7 @@ object ReadBookConfig {
|
||||
var titleSize: Int = 0,
|
||||
var titleTopSpacing: Int = 0,
|
||||
var titleBottomSpacing: Int = 0,
|
||||
var titleColor: Int = 0,
|
||||
var titleBold: Int = 500,//是否粗体字 0:正常, 1:粗体, 2:细体
|
||||
var titleLineSpacingExtra: Int = 12,
|
||||
var titleLineSpacingSub: Int = 12,
|
||||
@@ -715,6 +792,7 @@ object ReadBookConfig {
|
||||
var underline: Boolean = false, //下划线
|
||||
var underlinePadding: Int = 10,
|
||||
var underlineHeight: Int = 1,
|
||||
var underlineExtend: Boolean = false, //下划线延伸
|
||||
var underlineColor: String = "#3E3D3B",
|
||||
var underlineColorNight: String = "#ADADAD",
|
||||
var dottedLine: Boolean = false, //虚线
|
||||
@@ -740,10 +818,12 @@ object ReadBookConfig {
|
||||
var tipFooterLeft: Int = ReadTipConfig.chapterTitle,
|
||||
var tipFooterMiddle: Int = ReadTipConfig.none,
|
||||
var tipFooterRight: Int = ReadTipConfig.pageAndTotal,
|
||||
var tipColor: Int = 0,
|
||||
var tipHeaderColor: Int = 0,
|
||||
var tipFooterColor: Int = 0,
|
||||
var tipDividerColor: Int = -1,
|
||||
var headerMode: Int = 0,
|
||||
var footerMode: Int = 0
|
||||
var footerMode: Int = 0,
|
||||
var regexColorRules: ArrayList<RegexColorRule> = arrayListOf()
|
||||
) {
|
||||
|
||||
@Transient
|
||||
@@ -821,6 +901,11 @@ object ReadBookConfig {
|
||||
"pageAnim" to pageAnim,
|
||||
"pageAnimEInk" to pageAnimEInk,
|
||||
"textFont" to textFont,
|
||||
"titleFont" to titleFont,
|
||||
"headerFont" to headerFont,
|
||||
"footerFont" to footerFont,
|
||||
"headerFontSize" to headerFontSize,
|
||||
"footerFontSize" to footerFontSize,
|
||||
"textBold" to textBold,
|
||||
"textSize" to textSize,
|
||||
"letterSpacing" to letterSpacing,
|
||||
@@ -830,6 +915,7 @@ object ReadBookConfig {
|
||||
"titleSize" to titleSize,
|
||||
"titleTopSpacing" to titleTopSpacing,
|
||||
"titleBottomSpacing" to titleBottomSpacing,
|
||||
"titleColor" to titleColor,
|
||||
"paragraphIndent" to paragraphIndent,
|
||||
"paddingBottom" to paddingBottom,
|
||||
"paddingLeft" to paddingLeft,
|
||||
@@ -851,10 +937,12 @@ object ReadBookConfig {
|
||||
"tipFooterLeft" to tipFooterLeft,
|
||||
"tipFooterMiddle" to tipFooterMiddle,
|
||||
"tipFooterRight" to tipFooterRight,
|
||||
"tipColor" to tipColor,
|
||||
"tipHeaderColor" to tipHeaderColor,
|
||||
"tipFooterColor" to tipFooterColor,
|
||||
"tipDividerColor" to tipDividerColor,
|
||||
"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? {
|
||||
|
||||
@@ -85,10 +85,16 @@ object ReadTipConfig {
|
||||
ReadBookConfig.config.footerMode = value
|
||||
}
|
||||
|
||||
var tipColor: Int
|
||||
get() = ReadBookConfig.config.tipColor
|
||||
var tipHeaderColor: Int
|
||||
get() = ReadBookConfig.config.tipHeaderColor
|
||||
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
|
||||
|
||||
@@ -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.LocalConfig
|
||||
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.coroutine.Coroutine
|
||||
import io.legado.app.model.BookCover
|
||||
@@ -83,6 +84,7 @@ object Backup {
|
||||
ReadBookConfig.configFileName,
|
||||
ReadBookConfig.shareConfigFileName,
|
||||
OldThemeConfig.configFileName,
|
||||
PersonalizationThemeConfig.configFileName,
|
||||
BookCover.configFileName,
|
||||
"config.xml"
|
||||
)
|
||||
@@ -181,6 +183,8 @@ object Backup {
|
||||
FileUtils.createFileIfNotExist(backupPath + File.separator + BookCover.configFileName)
|
||||
.writeText(GSON.toJson(it))
|
||||
}
|
||||
FileUtils.createFileIfNotExist(backupPath + File.separator + PersonalizationThemeConfig.configFileName)
|
||||
.writeText(PersonalizationThemeConfig.toJson())
|
||||
currentCoroutineContext().ensureActive()
|
||||
appCtx.getSharedPreferences(backupPath, "config")?.let { sp ->
|
||||
sp.edit(commit = true) {
|
||||
|
||||
@@ -23,6 +23,7 @@ object BackupConfig {
|
||||
private const val readConfigKey = "readConfig"
|
||||
private const val themeConfigKey = "themeConfig"
|
||||
private const val coverConfigKey = "coverConfig"
|
||||
private const val personalizationThemeConfigKey = "personalizationThemeConfig"
|
||||
private const val localBookKey = "localBook"
|
||||
|
||||
//配置忽略key
|
||||
@@ -30,6 +31,7 @@ object BackupConfig {
|
||||
readConfigKey,
|
||||
PreferKey.themeMode,
|
||||
themeConfigKey,
|
||||
personalizationThemeConfigKey,
|
||||
coverConfigKey,
|
||||
PreferKey.bookshelfLayout,
|
||||
PreferKey.showRss,
|
||||
@@ -42,6 +44,7 @@ object BackupConfig {
|
||||
appCtx.getString(R.string.read_config),
|
||||
appCtx.getString(R.string.theme_mode),
|
||||
appCtx.getString(R.string.theme_config),
|
||||
"深度个性化主题",
|
||||
appCtx.getString(R.string.cover_config),
|
||||
appCtx.getString(R.string.bookshelf_layout),
|
||||
appCtx.getString(R.string.show_rss),
|
||||
@@ -84,15 +87,9 @@ object BackupConfig {
|
||||
|
||||
private val themePrefKeys = arrayOf(
|
||||
PreferKey.cPrimary,
|
||||
PreferKey.cAccent,
|
||||
PreferKey.cBackground,
|
||||
PreferKey.cBBackground,
|
||||
PreferKey.cNPrimary,
|
||||
PreferKey.bgImage,
|
||||
PreferKey.bgImageBlurring,
|
||||
PreferKey.cNPrimary,
|
||||
PreferKey.cNAccent,
|
||||
PreferKey.cNBackground,
|
||||
PreferKey.cNBBackground,
|
||||
PreferKey.bgImageN,
|
||||
PreferKey.bgImageNBlurring
|
||||
)
|
||||
@@ -128,6 +125,8 @@ object BackupConfig {
|
||||
get() = ignoreConfig[themeConfigKey] == true
|
||||
private val ignoreCoverConfig: Boolean
|
||||
get() = ignoreConfig[coverConfigKey] == true
|
||||
val ignorePersonalizationThemeConfig: Boolean
|
||||
get() = ignoreConfig[personalizationThemeConfigKey] == true
|
||||
private val ignoreBookshelfLayout: Boolean
|
||||
get() = ignoreConfig[PreferKey.bookshelfLayout] == true
|
||||
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.config.LocalConfig
|
||||
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.model.BookCover
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
@@ -233,6 +234,16 @@ object Restore {
|
||||
}?.onFailure {
|
||||
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) {
|
||||
//恢复阅读界面配置
|
||||
File(path, ReadBookConfig.configFileName).takeIf {
|
||||
|
||||
@@ -24,9 +24,9 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
||||
|
||||
private suspend fun getLatestRelease(): List<AppReleaseInfo> {
|
||||
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
|
||||
"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) }
|
||||
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
|
||||
@@ -65,7 +65,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
||||
}
|
||||
|
||||
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) }
|
||||
if (!res.isSuccessful) return null
|
||||
|
||||
@@ -86,7 +86,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
||||
val currentVersion = AppConst.appInfo.versionName
|
||||
val releases = getLatestRelease()
|
||||
|
||||
val filtered = if (checkVariant == AppVariant.ALL) {
|
||||
val filtered = if (checkVariant == AppVariant.ALL || checkVariant == AppVariant.UNKNOWN) {
|
||||
releases
|
||||
} else {
|
||||
releases.filter { it.appVariant == checkVariant }
|
||||
@@ -153,8 +153,8 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
|
||||
|
||||
companion object {
|
||||
fun parse(version: String): SemVer {
|
||||
val regex = Regex("""(\d+)\.(\d+)\.(\d+)(?:-([\w.]+))?""")
|
||||
val match = regex.matchEntire(version)
|
||||
val regex = Regex("""(\d+)\.(\d+)\.(\d+)(?:[-_]([\w.]+))?""")
|
||||
val match = regex.find(version)
|
||||
?: throw IllegalArgumentException("Invalid version: $version")
|
||||
val (maj, min, pat, pre) = match.destructured
|
||||
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.Book
|
||||
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.model.AudioPlay
|
||||
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.ui.rss.read.RssJsExtensions
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.escapeForJs
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import io.legado.app.utils.isJsonObject
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.UUID
|
||||
|
||||
@Suppress("unused")
|
||||
class WebJsExtensions(
|
||||
source: BaseSource, activity: AppCompatActivity,
|
||||
private val webView: WebView,
|
||||
private val bookType: Int = 0,
|
||||
source: BaseSource,
|
||||
activity: AppCompatActivity?,
|
||||
webView: WebView,
|
||||
bookType: Int = 0,
|
||||
callback: Callback? = null
|
||||
) : RssJsExtensions(activity, source) {
|
||||
private val callbackRef: WeakReference<Callback> = WeakReference(callback)
|
||||
private val webViewRef: WeakReference<WebView?> = WeakReference(webView)
|
||||
|
||||
interface Callback {
|
||||
fun upConfig(config: String)
|
||||
fun onNavigateToArticles(sortUrl: String? = null)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
@@ -40,6 +45,16 @@ class WebJsExtensions(
|
||||
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 {
|
||||
var book: Book? = null
|
||||
var chapter: BookChapter? = null
|
||||
@@ -63,154 +78,201 @@ class WebJsExtensions(
|
||||
private val book: Book? get() = bookAndChapter.first
|
||||
private val chapter: BookChapter? get() = bookAndChapter.second
|
||||
|
||||
/**
|
||||
* 由软件主动注入的js函数调用
|
||||
*/
|
||||
val analyzeRule by lazy {
|
||||
AnalyzeRule(book, source = getSource()).setChapter(chapter)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun request(funName: String, jsParam: Array<String>, id: String) {
|
||||
fun request(funName: String, jsParam: Array<String?>, id: String) {
|
||||
val activity = activityRef.get() ?: return
|
||||
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) {
|
||||
"run" -> AnalyzeRule(book, getSource()).run {
|
||||
setCoroutineContext(coroutineContext)
|
||||
setChapter(chapter)
|
||||
evalJS(jsParam[0]).toString()
|
||||
"run" -> {
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
.evalJS(
|
||||
p0 ?: throw NoStackTraceException("error null")
|
||||
).toString()
|
||||
}
|
||||
"ajaxAwait" -> {
|
||||
ajax(jsParam[0], jsParam[1].toIntOrNull()).toString()
|
||||
ajax(
|
||||
p0 ?: throw NoStackTraceException("error url null"),
|
||||
p1?.toLongOrNull()
|
||||
).toString()
|
||||
}
|
||||
"connectAwait" -> {
|
||||
connect(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
||||
connect(
|
||||
p0 ?: throw NoStackTraceException("error url null"),
|
||||
p1,
|
||||
p2?.toIntOrNull()
|
||||
)
|
||||
}
|
||||
"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" -> {
|
||||
head(jsParam[0], jsParam[1], jsParam[2].toIntOrNull())
|
||||
head(
|
||||
p0 ?: throw NoStackTraceException("error url null"),
|
||||
p1 ?: throw NoStackTraceException("error header null"),
|
||||
p2?.toIntOrNull()
|
||||
)
|
||||
}
|
||||
"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" -> {
|
||||
webView(jsParam[0], jsParam[1], jsParam[2], jsParam[3].toBoolean()).toString()
|
||||
webView(
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
p3.toBoolean()
|
||||
).toString()
|
||||
}
|
||||
"webViewGetSourceAwait" -> {
|
||||
webViewGetSource(
|
||||
jsParam[0],
|
||||
jsParam[1],
|
||||
jsParam[2],
|
||||
jsParam[3],
|
||||
jsParam[4].toBoolean(),
|
||||
jsParam[5].toLongOrNull() ?: 0
|
||||
p0,
|
||||
p1,
|
||||
p2,
|
||||
p3 ?: throw NoStackTraceException("error sourceRegex null"),
|
||||
p4.toBoolean(),
|
||||
p5?.toLongOrNull() ?: 0
|
||||
).toString()
|
||||
}
|
||||
"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" -> {
|
||||
createSymmetricCrypto(
|
||||
jsParam[0],
|
||||
jsParam[1],
|
||||
jsParam[2]
|
||||
).encryptBase64(jsParam[3])
|
||||
p0 ?: throw NoStackTraceException("error transformation null"),
|
||||
p1 ?: throw NoStackTraceException("error key null"),
|
||||
p2
|
||||
).encryptBase64(p3 ?: throw NoStackTraceException("error data null"))
|
||||
}
|
||||
"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" -> {
|
||||
createSign(jsParam[0]).setPublicKey(jsParam[1]).setPrivateKey(jsParam[2])
|
||||
.signHex(jsParam[3])
|
||||
createSign(p0 ?: throw NoStackTraceException("error algorithm null"))
|
||||
.setPublicKey(p1 ?: throw NoStackTraceException("error publicKey null"))
|
||||
.setPrivateKey(p2 ?: throw NoStackTraceException("error privateKey null"))
|
||||
.signHex(p3 ?: throw NoStackTraceException("error data null"))
|
||||
}
|
||||
"downloadFileAwait" -> {
|
||||
downloadFile(jsParam[0])
|
||||
downloadFile(p0 ?: throw NoStackTraceException("error url null"))
|
||||
}
|
||||
"readTxtFileAwait" -> {
|
||||
readTxtFile(jsParam[0])
|
||||
readTxtFile(p0 ?: throw NoStackTraceException("error path null"))
|
||||
}
|
||||
"importScriptAwait" -> {
|
||||
importScript(jsParam[0])
|
||||
importScript(p0 ?: throw NoStackTraceException("error path null"))
|
||||
}
|
||||
"getStringAwait" -> AnalyzeRule(book, getSource()).run {
|
||||
setCoroutineContext(coroutineContext)
|
||||
setChapter(chapter)
|
||||
getString(jsParam[0], jsParam[1])
|
||||
"getStringAwait" -> {
|
||||
analyzeRule.setCoroutineContext(coroutineContext)
|
||||
.getString(p0, p1)
|
||||
}
|
||||
else -> "error funName"
|
||||
else -> throw NoStackTraceException("error funName")
|
||||
}
|
||||
}.onSuccess { data ->
|
||||
webView.evaluateJavascript(
|
||||
"window.$JSBridgeResult('$id', '${data.escapeForJs()}', null);",
|
||||
null
|
||||
)
|
||||
CacheManager.putMemory(id, data)
|
||||
webViewRef.get()?.evaluateJavascript("window.$JSBridgeResult('$id', true);", null)
|
||||
}.onError {
|
||||
webView.evaluateJavascript(
|
||||
"window.$JSBridgeResult('$id', null, '${it.localizedMessage?.escapeForJs()}');",
|
||||
null
|
||||
)
|
||||
CacheManager.putMemory(id, it.localizedMessage ?: "err")
|
||||
webViewRef.get()?.evaluateJavascript("window.$JSBridgeResult('$id', false);", null)
|
||||
}
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun toast(msg: String?) {
|
||||
super.toast(msg)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun longToast(msg: String?) {
|
||||
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
|
||||
fun log(msg: String?): String {
|
||||
return super.log(msg).toString()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun ajax(url: String): String? {
|
||||
return super.ajax(url, 9000)
|
||||
@JvmOverloads
|
||||
fun ajax(url: String, callTimeout: Int = 9000): String? {
|
||||
return super.ajax(url, callTimeout.toLong())
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun ajax(url: String, callTimeout: Int?): String? {
|
||||
return super.ajax(url, callTimeout?.toLong())
|
||||
}
|
||||
@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 {
|
||||
@JvmOverloads
|
||||
fun connect(urlStr: String, header: String? = null, callTimeout: Int? = 9000): String {
|
||||
return super.connect(urlStr, header, callTimeout?.toLong()).toString()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun get(urlStr: String, headers: String): String {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||
return super.get(urlStr, headerMap, 9000).body()
|
||||
@JvmOverloads
|
||||
fun get(urlStr: String, header: String, timeout: Int? = 9000): String {
|
||||
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||
return super.get(urlStr, headers, timeout).body()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun get(urlStr: String, headers: String, timeout: Int?): String {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||
return super.get(urlStr, headerMap, timeout).body()
|
||||
@JvmOverloads
|
||||
fun post(urlStr: String, body: String, header: String, timeout: Int? = 9000): String {
|
||||
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||
return super.post(urlStr, body, headers, timeout).body()
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun post(urlStr: String, body: String, headers: String): String {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||
return super.post(urlStr, body, headerMap, 9000).body()
|
||||
@JvmOverloads
|
||||
fun head(urlStr: String, header: String, timeout: Int? = 9000): String {
|
||||
val headers = GSON.fromJsonObject<Map<String, String>>(header).getOrNull() ?: emptyMap()
|
||||
return GSON.toJson(super.head(urlStr, headers, timeout).headers())
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun post(urlStr: String, body: String, headers: String, timeout: Int?): String {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||
return super.post(urlStr, body, headerMap, timeout).body()
|
||||
fun getStringList(rule: String?, mContent: String? = null, isUrl: Boolean = false): List<String>? {
|
||||
return super.getStringList(rule, mContent, isUrl)
|
||||
}
|
||||
|
||||
@JavascriptInterface
|
||||
fun head(urlStr: String, headers: String): String {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(headers).getOrNull() ?: emptyMap()
|
||||
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())
|
||||
fun getString(ruleStr: String?, mContent: String? = null, isUrl: Boolean = false): String {
|
||||
return super.getString(ruleStr, mContent, isUrl)
|
||||
}
|
||||
|
||||
companion object {
|
||||
@@ -218,6 +280,7 @@ class WebJsExtensions(
|
||||
val letters = "abcdefghijklmnopqrstuvwxyz"
|
||||
return letters.random()
|
||||
}
|
||||
|
||||
val uuid by lazy {
|
||||
UUID.randomUUID().toString().replace('-', getRandomLetter()).chunked(6)
|
||||
}
|
||||
@@ -234,9 +297,12 @@ class WebJsExtensions(
|
||||
"<script src=\"$nameUrl\"></script>"
|
||||
}
|
||||
|
||||
val getInjectionString = "try{var cache=$nameCache,source=$nameSource,java=$nameJava;}catch(e){}"
|
||||
|
||||
val JS_INJECTION by lazy {
|
||||
"""
|
||||
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 java = window.$nameJava;
|
||||
delete window.$nameJava;
|
||||
@@ -248,122 +314,124 @@ class WebJsExtensions(
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = requestId("run");
|
||||
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) => {
|
||||
const id = requestId("ajaxAwait");
|
||||
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) => {
|
||||
const id = requestId("connectAwait");
|
||||
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) => {
|
||||
const id = requestId("getAwait");
|
||||
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) => {
|
||||
const id = requestId("headAwait");
|
||||
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) => {
|
||||
const id = requestId("postAwait");
|
||||
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) => {
|
||||
const id = requestId("webViewAwait");
|
||||
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) => {
|
||||
const id = requestId("webViewGetSourceAwait");
|
||||
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) => {
|
||||
const id = requestId("decryptStrAwait");
|
||||
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) => {
|
||||
const id = requestId("encryptBase64Await");
|
||||
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) => {
|
||||
const id = requestId("encryptHexAwait");
|
||||
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) => {
|
||||
const id = requestId("createSignHexAwait");
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = requestId("downloadFileAwait");
|
||||
JSBridgeCallbacks[id] = { resolve, reject };
|
||||
java?.request("downloadFileAwait", [String(url)], id);
|
||||
java.request("downloadFileAwait", [String(url)], id);
|
||||
});
|
||||
};
|
||||
function readTxtFileAwait(path) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = requestId("readTxtFileAwait");
|
||||
JSBridgeCallbacks[id] = { resolve, reject };
|
||||
java?.request("readTxtFileAwait", [String(path)], id);
|
||||
java.request("readTxtFileAwait", [String(path)], id);
|
||||
});
|
||||
};
|
||||
function importScriptAwait(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = requestId("importScriptAwait");
|
||||
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) => {
|
||||
const id = requestId("getStringAwait");
|
||||
JSBridgeCallbacks[id] = { resolve, reject };
|
||||
java?.request("getStringAwait", [String(ruleStr), String(mContent)], id);
|
||||
java.request("getStringAwait", params(args), id);
|
||||
});
|
||||
};
|
||||
window.$JSBridgeResult = function(requestId, result, error) {
|
||||
if (JSBridgeCallbacks[requestId]) {
|
||||
if (error) {
|
||||
JSBridgeCallbacks[requestId].reject(error);
|
||||
window.$JSBridgeResult = function(id, success) {
|
||||
const callBack = JSBridgeCallbacks[id];
|
||||
if (callBack) {
|
||||
const result = cache.getFromMemory(id);
|
||||
if (success) {
|
||||
callBack.resolve(result);
|
||||
} else {
|
||||
JSBridgeCallbacks[requestId].resolve(result);
|
||||
callBack.reject(result);
|
||||
}
|
||||
delete JSBridgeCallbacks[requestId];
|
||||
delete JSBridgeCallbacks[id];
|
||||
}
|
||||
};""".trimIndent()
|
||||
}
|
||||
@@ -374,21 +442,25 @@ class WebJsExtensions(
|
||||
const JSBridgeCallbacks = {};
|
||||
const java = window.$nameJava;
|
||||
delete window.$nameJava;
|
||||
const cache = window.$nameCache;
|
||||
delete window.$nameCache;
|
||||
function run(jsCode) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const id = requestId("run");
|
||||
JSBridgeCallbacks[id] = { resolve, reject };
|
||||
java?.request("run", [String(jsCode)], id);
|
||||
java.request("run", [String(jsCode)], id);
|
||||
});
|
||||
};
|
||||
window.$JSBridgeResult = function(requestId, result, error) {
|
||||
if (JSBridgeCallbacks[requestId]) {
|
||||
if (error) {
|
||||
JSBridgeCallbacks[requestId].reject(error);
|
||||
window.$JSBridgeResult = function(id, success) {
|
||||
const callBack = JSBridgeCallbacks[id];
|
||||
if (callBack) {
|
||||
const result = cache.getFromMemory(id);
|
||||
if (success) {
|
||||
callBack.resolve(result);
|
||||
} else {
|
||||
JSBridgeCallbacks[requestId].resolve(result);
|
||||
callBack.reject(result);
|
||||
}
|
||||
delete JSBridgeCallbacks[requestId];
|
||||
delete JSBridgeCallbacks[id];
|
||||
}
|
||||
};""".trimIndent()
|
||||
}
|
||||
@@ -416,4 +488,4 @@ class WebJsExtensions(
|
||||
})();""".trimIndent()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,6 +59,7 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
var durChapterPos = 0
|
||||
var durChapter: BookChapter? = null
|
||||
var durPlayUrl = ""
|
||||
var durLyric: String? = null
|
||||
var durAudioSize = 0
|
||||
var inBookshelf = false
|
||||
var bookSource: BookSource? = null
|
||||
@@ -153,14 +154,23 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
.onSuccess { content ->
|
||||
if (content.isEmpty()) {
|
||||
appCtx.toastOnUi("未获取到资源链接")
|
||||
// 加载失败时清空歌词
|
||||
durLyric = null
|
||||
callback?.upLyric(null)
|
||||
} else {
|
||||
contentLoadFinish(chapter, content)
|
||||
}
|
||||
}.onError {
|
||||
AppLog.put("获取资源链接出错\n$it", it, true)
|
||||
}.onError { error ->
|
||||
AppLog.put("获取资源链接出错\n$error", error, true)
|
||||
upLoading(false)
|
||||
// 加载失败时清空歌词
|
||||
durLyric = null
|
||||
callback?.upLyric(null)
|
||||
}.onCancel {
|
||||
removeLoading(index)
|
||||
// 取消加载时清空歌词
|
||||
durLyric = null
|
||||
callback?.upLyric(null)
|
||||
}.onFinally {
|
||||
removeLoading(index)
|
||||
}
|
||||
@@ -177,7 +187,10 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
private fun contentLoadFinish(chapter: BookChapter, content: String) {
|
||||
if (chapter.index == book?.durChapterIndex) {
|
||||
durPlayUrl = content
|
||||
durLyric = chapter.getVariable("lyric").takeIf { it.isNotBlank() }
|
||||
upPlayUrl()
|
||||
// 重新加载歌词,确保循环播放时歌词进度重置
|
||||
callback?.upLyric(durLyric)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -397,6 +410,8 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
fun register(context: Context) {
|
||||
activityContext = context
|
||||
callback = context as CallBack
|
||||
// 主动更新歌词状态,确保重新进入时歌词控件能正确显示
|
||||
callback?.upLyric(durLyric)
|
||||
}
|
||||
|
||||
fun unregister(context: Context) {
|
||||
@@ -418,6 +433,8 @@ object AudioPlay : CoroutineScope by MainScope() {
|
||||
interface CallBack {
|
||||
|
||||
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(
|
||||
scope: CoroutineScope,
|
||||
rssArticle: RssArticle,
|
||||
|
||||
@@ -24,10 +24,11 @@ object Rss {
|
||||
sortUrl: String,
|
||||
rssSource: RssSource,
|
||||
page: Int,
|
||||
searchKey: String? = null,
|
||||
context: CoroutineContext = Dispatchers.IO
|
||||
): Coroutine<Pair<MutableList<RssArticle>, String?>> {
|
||||
return Coroutine.async(scope, context) {
|
||||
getArticlesAwait(sortName, sortUrl, rssSource, page)
|
||||
getArticlesAwait(sortName, sortUrl, rssSource, page, searchKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,8 +37,13 @@ object Rss {
|
||||
sortUrl: String,
|
||||
rssSource: RssSource,
|
||||
page: Int,
|
||||
searchKey: String? = null
|
||||
): Pair<MutableList<RssArticle>, String?> {
|
||||
val ruleData = RuleData()
|
||||
if (!searchKey.isNullOrBlank()) {
|
||||
ruleData.putVariable("searchKey", searchKey)
|
||||
ruleData.putVariable("key", searchKey)
|
||||
}
|
||||
val analyzeUrl = AnalyzeUrl(
|
||||
sortUrl,
|
||||
page = page,
|
||||
@@ -100,4 +106,4 @@ object Rss {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -211,9 +211,9 @@ object BookContent {
|
||||
if (!book.isAudio && !book.isVideo) { //音频和视频获取的是链接,不需要html格式化 && !book.isVideo
|
||||
val useHtmlMap = mutableMapOf<String, String>()
|
||||
if (AppConfig.adaptSpecialStyle) {
|
||||
content = AppPattern.useHtmlRegex.replace(content) { matchResult ->
|
||||
content = AppPattern.useHtmlRegex.replace(content) {
|
||||
val placeholder = "{usehtml_${useHtmlMap.size}}"
|
||||
useHtmlMap[placeholder] = matchResult.value
|
||||
useHtmlMap[placeholder] = it.value
|
||||
placeholder
|
||||
}
|
||||
}
|
||||
@@ -225,6 +225,13 @@ object BookContent {
|
||||
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) {
|
||||
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.card.TextCard
|
||||
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.topbar.GlassMediumFlexibleTopAppBar
|
||||
import io.legado.app.ui.widget.components.topbar.GlassTopAppBarDefaults
|
||||
@@ -132,56 +133,56 @@ fun AboutScreen(
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
title = ""
|
||||
){
|
||||
SettingItem(
|
||||
title = stringResource(R.string.contributors),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = {
|
||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = {
|
||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.license),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = {
|
||||
onShowMdFile("许可证", "LICENSE.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.disclaimer),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = {
|
||||
onShowMdFile("免责声明", "disclaimer.md")
|
||||
}
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.crash_log),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = onShowCrashLogs
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.save_log),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = onSaveLog
|
||||
)
|
||||
SettingItem(
|
||||
title = stringResource(R.string.create_heap_dump),
|
||||
description = null,
|
||||
option = null,
|
||||
onClick = onCreateHeapDump
|
||||
)
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.contributors),
|
||||
onClick = {
|
||||
onOpenUrl("https://github.com/gedoor/legado/graphs/contributors")
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.privacy_policy),
|
||||
onClick = {
|
||||
onShowMdFile("隐私政策", "privacyPolicy.md")
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.license),
|
||||
onClick = {
|
||||
onShowMdFile("许可证", "LICENSE.md")
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.disclaimer),
|
||||
onClick = {
|
||||
onShowMdFile("免责声明", "disclaimer.md")
|
||||
}
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.crash_log),
|
||||
onClick = onShowCrashLogs
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.save_log),
|
||||
onClick = onSaveLog
|
||||
)
|
||||
}
|
||||
SettingItemWithDivider {
|
||||
SettingItem(
|
||||
title = stringResource(R.string.create_heap_dump),
|
||||
onClick = onCreateHeapDump
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -75,7 +75,7 @@ fun CrashReportScreen(
|
||||
.fillMaxWidth()
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
Text(
|
||||
AppText(
|
||||
text = displayText,
|
||||
style = LegadoTheme.typography.bodySmall,
|
||||
fontFamily = FontFamily.Monospace
|
||||
|
||||
@@ -25,10 +25,13 @@ import androidx.lifecycle.lifecycleScope
|
||||
import androidx.transition.TransitionManager
|
||||
import com.google.android.material.color.DynamicColors
|
||||
import com.google.android.material.color.DynamicColorsOptions
|
||||
import androidx.constraintlayout.widget.ConstraintLayout
|
||||
import com.google.android.material.color.MaterialColors
|
||||
import com.google.android.material.slider.Slider
|
||||
import com.google.android.material.transition.platform.MaterialContainerTransform
|
||||
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.base.VMBaseActivity
|
||||
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.removeType
|
||||
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.BookCover
|
||||
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.ToolbarUtils.setAllIconsColor
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
import io.legado.app.utils.dpToPx
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.observeEventSticky
|
||||
@@ -96,6 +101,9 @@ class AudioPlayActivity :
|
||||
private var secondaryContainerFinalColor: Int = 0
|
||||
private var currentJob: Job? = 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 {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
@@ -227,6 +235,21 @@ class AudioPlayActivity :
|
||||
}
|
||||
|
||||
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.ivFastForward.isEnabled = false
|
||||
|
||||
@@ -279,7 +302,10 @@ class AudioPlayActivity :
|
||||
|
||||
override fun onStopTrackingTouch(slider: Slider) {
|
||||
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.tvDurTime.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)
|
||||
if (!adjustProgress) slider.value = safeValue
|
||||
binding.tvDurTime.text = progressTimeFormat.format(progress.toLong())
|
||||
// 更新歌词进度
|
||||
upLyricP(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>) {
|
||||
viewModel.addToBookshelf(book, toc) {
|
||||
toastOnUi("已添加到书架")
|
||||
|
||||
@@ -72,6 +72,8 @@ import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
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.config.coverConfig.CoverConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
@@ -402,6 +404,9 @@ private fun BookInfoTransparentTopAppBar(
|
||||
|
||||
@Composable
|
||||
private fun rememberBookInfoColorTheme(book: Book?): ThemeOverrideState? {
|
||||
val useDefaultCover = AppConfig.useDefaultCover || book?.customCoverUrl == "use_default_cover"
|
||||
if (useDefaultCover) return null
|
||||
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
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 loadOnlyWifi = CoverConfig.loadCoverOnlyWifi
|
||||
val requestKey = remember(coverPath, sourceOrigin, loadOnlyWifi) {
|
||||
@@ -469,8 +479,15 @@ private fun BookInfoTopBarActions(
|
||||
|
||||
@Composable
|
||||
private fun BookInfoBackdrop(book: Book) {
|
||||
val cover = book.getDisplayCover()
|
||||
val sourceOrigin = book.origin
|
||||
val useDefaultCover = AppConfig.useDefaultCover || book.customCoverUrl == "use_default_cover"
|
||||
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 context = LocalContext.current
|
||||
val imageLoader = koinInject<ImageLoader>()
|
||||
|
||||
@@ -709,19 +709,30 @@ class BookInfoViewModel(
|
||||
|
||||
private fun refreshMeta(book: Book) {
|
||||
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) {
|
||||
val size = FileDoc.fromFile(book.bookUrl).size
|
||||
if (size > 0) {
|
||||
kinds.add(ConvertUtils.formatFileSize(size))
|
||||
sourceKinds.add(ConvertUtils.formatFileSize(size))
|
||||
}
|
||||
}
|
||||
val mergedKinds = (customKinds + sourceKinds).distinct()
|
||||
val userGroupIds = appDb.bookGroupDao.idsSum
|
||||
val groupAnd = userGroupIds and book.group
|
||||
val hasCustomGroup = book.group > 0L && groupAnd != 0L
|
||||
val groupNames = appDb.bookGroupDao.getGroupNames(book.group).joinToString(",")
|
||||
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 {
|
||||
currentKindLabels = it.first
|
||||
currentGroupNames = it.second
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.book.info.edit
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.horizontalScroll
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
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.verticalScroll
|
||||
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.ImageSearch
|
||||
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.ExposedDropdownMenuBox
|
||||
import androidx.compose.material3.ExposedDropdownMenuDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
@@ -35,6 +40,7 @@ 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.input.nestedscroll.nestedScroll
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.topbar.GlassMediumFlexibleTopAppBar
|
||||
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.launch
|
||||
import io.legado.app.utils.showDialogFragment
|
||||
@@ -174,10 +181,17 @@ fun BookInfoEditContent(
|
||||
onCheckedChange = { viewModel.onFixedTypeChange(it) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
val bookInfoInputColor = ThemeConfig.bookInfoInputColor
|
||||
val inputBackgroundColor = if (bookInfoInputColor != 0) {
|
||||
Color(bookInfoInputColor)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
AppTextField(
|
||||
value = uiState.name,
|
||||
onValueChange = { viewModel.onNameChange(it) },
|
||||
label = "书名",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -185,6 +199,7 @@ fun BookInfoEditContent(
|
||||
value = uiState.author,
|
||||
onValueChange = { viewModel.onAuthorChange(it) },
|
||||
label = "作者",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -192,13 +207,21 @@ fun BookInfoEditContent(
|
||||
value = uiState.coverUrl ?: "",
|
||||
onValueChange = { viewModel.onCoverUrlChange(it) },
|
||||
label = "封面链接",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
KindEditor(
|
||||
kindList = uiState.kindList,
|
||||
onKindListChange = { viewModel.onKindListChange(it) },
|
||||
backgroundColor = inputBackgroundColor
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
AppTextField(
|
||||
value = uiState.intro ?: "",
|
||||
onValueChange = { viewModel.onIntroChange(it) },
|
||||
label = "简介",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
@@ -206,6 +229,7 @@ fun BookInfoEditContent(
|
||||
value = uiState.remark ?: "",
|
||||
onValueChange = { viewModel.onRemarkChange(it) },
|
||||
label = "备注",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
)
|
||||
}
|
||||
@@ -228,6 +252,13 @@ fun BookTypeDropdown(
|
||||
textFieldState.setTextAndPlaceCursorAtEnd(selectedType)
|
||||
}
|
||||
|
||||
val bookInfoInputColor = ThemeConfig.bookInfoInputColor
|
||||
val inputBackgroundColor = if (bookInfoInputColor != 0) {
|
||||
Color(bookInfoInputColor)
|
||||
} else {
|
||||
Color.Unspecified
|
||||
}
|
||||
|
||||
ExposedDropdownMenuBox(
|
||||
modifier = Modifier.padding(horizontal = 8.dp),
|
||||
expanded = expanded,
|
||||
@@ -238,6 +269,7 @@ fun BookTypeDropdown(
|
||||
readOnly = true,
|
||||
lineLimits = TextFieldLineLimits.SingleLine,
|
||||
label = "书籍类型",
|
||||
backgroundColor = inputBackgroundColor,
|
||||
trailingIcon = {
|
||||
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.MD5Utils
|
||||
import io.legado.app.utils.inputStream
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
@@ -31,6 +32,7 @@ data class BookInfoEditUiState(
|
||||
val coverUrl: String? = null,
|
||||
val intro: String? = null,
|
||||
val remark: String? = null,
|
||||
val kindList: List<String> = emptyList(),
|
||||
val selectedType: String = "文本",
|
||||
val bookTypes: List<String> = listOf("文本", "音频", "图片"),
|
||||
val fixedType: Boolean = false,
|
||||
@@ -57,6 +59,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
coverUrl = it.getDisplayCover(),
|
||||
intro = it.getDisplayIntro(),
|
||||
remark = it.remark,
|
||||
kindList = it.kind?.split(",", "\n")?.filter { kind -> kind.isNotBlank() }.orEmpty(),
|
||||
selectedType = _uiState.value.bookTypes[selectedTypeIndex],
|
||||
fixedType = it.config.fixedType,
|
||||
book = it
|
||||
@@ -85,6 +88,10 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
_uiState.value = _uiState.value.copy(remark = remark)
|
||||
}
|
||||
|
||||
fun onKindListChange(kindList: List<String>) {
|
||||
_uiState.value = _uiState.value.copy(kindList = kindList)
|
||||
}
|
||||
|
||||
fun onBookTypeChange(bookType: String) {
|
||||
_uiState.value = _uiState.value.copy(selectedType = bookType)
|
||||
}
|
||||
@@ -116,6 +123,7 @@ class BookInfoEditViewModel(application: Application) : BaseViewModel(applicatio
|
||||
book.config.fixedType = currentState.fixedType
|
||||
book.customCoverUrl = if (currentState.coverUrl == book.coverUrl) null else currentState.coverUrl
|
||||
book.customIntro = if (currentState.intro == book.intro) null else currentState.intro
|
||||
book.kind = currentState.kindList.joinToString(",")
|
||||
BookHelp.updateCacheFolder(oldBook, book)
|
||||
|
||||
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.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.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.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.UnderlineConfigDialog.Companion.U_COLOR
|
||||
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.TextPage
|
||||
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.searchContent.SearchContentActivity
|
||||
import io.legado.app.ui.book.searchContent.SearchResult
|
||||
@@ -1698,8 +1703,14 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
}
|
||||
}
|
||||
|
||||
TIP_COLOR -> {
|
||||
ReadTipConfig.tipColor = color
|
||||
TIP_HEADER_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.UP_CONFIG, arrayListOf(2))
|
||||
}
|
||||
@@ -1710,6 +1721,24 @@ class ReadBookActivity : BaseReadBookActivity(),
|
||||
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 -> {
|
||||
setMenuCurBg(color)
|
||||
postEvent(EventBus.UPDATE_READ_ACTION_BAR, true)
|
||||
|
||||
@@ -107,6 +107,10 @@ class FontConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_conf
|
||||
callBack2?.showShadowSet()
|
||||
}
|
||||
|
||||
binding.btnRegexColor.setOnClickListener {
|
||||
RegexColorConfigDialog().show(childFragmentManager, "regexColorConfig")
|
||||
}
|
||||
|
||||
binding.btnSelectFonts.setOnClickListener {
|
||||
callBack2?.showFontSelect()
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.widget.addTextChangedListener
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
@@ -50,6 +51,7 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
||||
val curFontPath = callBack?.curFontPath ?: ""
|
||||
FontAdapter(requireContext(), curFontPath)
|
||||
}
|
||||
private var allFontItems: List<FileDoc> = emptyList()
|
||||
|
||||
private val selectFontDir = registerForActivityResult(HandleFileContract()) {
|
||||
it.uri?.let { uri ->
|
||||
@@ -76,6 +78,22 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
||||
binding.recyclerView.layoutManager = GridLayoutManager(context, 2)
|
||||
binding.recyclerView.adapter = adapter
|
||||
initView()
|
||||
initSearch()
|
||||
}
|
||||
|
||||
private fun initSearch() {
|
||||
binding.etSearch.addTextChangedListener {
|
||||
filterFonts(it.toString())
|
||||
}
|
||||
}
|
||||
|
||||
private fun filterFonts(keyword: String) {
|
||||
val filtered = if (keyword.isBlank()) {
|
||||
allFontItems
|
||||
} else {
|
||||
allFontItems.filter { it.name.contains(keyword, ignoreCase = true) }
|
||||
}
|
||||
adapter.setItems(filtered)
|
||||
}
|
||||
|
||||
private fun initView() {
|
||||
@@ -135,6 +153,7 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
||||
} ?: ArrayList()
|
||||
mergeFontItems(fontItems, getLocalFonts())
|
||||
}.onSuccess {
|
||||
allFontItems = it
|
||||
adapter.setItems(it)
|
||||
}.onError {
|
||||
AppLog.put("加载字体文件失败\n${it.localizedMessage}", it)
|
||||
@@ -176,8 +195,10 @@ class FontSelectDialog : BaseBottomSheetDialogFragment(R.layout.dialog_font_sele
|
||||
callBack?.selectFont("")
|
||||
}
|
||||
|
||||
var explicitCallback: CallBack? = null
|
||||
|
||||
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) :
|
||||
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.lib.dialogs.selector
|
||||
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.utils.getCompatColor
|
||||
import io.legado.app.utils.observeEvent
|
||||
import io.legado.app.utils.postEvent
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
|
||||
class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info) {
|
||||
class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info), FontSelectDialog.CallBack {
|
||||
|
||||
private val binding by viewBinding(DialogReadInfoBinding::bind)
|
||||
private val callBack get() = activity as? ReadBookActivity
|
||||
|
||||
override val curFontPath: String
|
||||
get() = ReadBookConfig.headerFont
|
||||
|
||||
override fun selectFont(path: String) {
|
||||
ReadBookConfig.headerFont = path
|
||||
ReadBookConfig.save()
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||
}
|
||||
|
||||
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||
initView()
|
||||
initEvent()
|
||||
observeEvent<String>(EventBus.TIP_COLOR) {
|
||||
upTvTipColor()
|
||||
upTvHeaderColor()
|
||||
upTvFooterColor()
|
||||
upTvTipDividerColor()
|
||||
}
|
||||
observeEvent<ArrayList<Int>>(EventBus.UP_CONFIG) { list ->
|
||||
@@ -59,17 +68,28 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
|
||||
callBack?.showPaddingConfig()
|
||||
dismissAllowingStateLoss()
|
||||
}
|
||||
upTvTipColor()
|
||||
upTvHeaderColor()
|
||||
upTvFooterColor()
|
||||
upTvTipDividerColor()
|
||||
binding.scvHeaderFontSize.progress = ReadBookConfig.headerFontSize
|
||||
}
|
||||
|
||||
private fun upTvTipColor() {
|
||||
val tipColor = if (ReadTipConfig.tipColor == 0) {
|
||||
private fun upTvHeaderColor() {
|
||||
val tipColor = if (ReadTipConfig.tipHeaderColor == 0) {
|
||||
ReadBookConfig.textColor
|
||||
} 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() {
|
||||
@@ -171,19 +191,36 @@ class InfoConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_read_info
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2, 6))
|
||||
}
|
||||
}
|
||||
btnTipColor.setOnClickListener {
|
||||
btnHeaderColor.setOnClickListener {
|
||||
context?.selector(items = ReadTipConfig.tipColorNames) { _, i ->
|
||||
when (i) {
|
||||
0 -> {
|
||||
ReadTipConfig.tipColor = 0
|
||||
upTvTipColor()
|
||||
ReadTipConfig.tipHeaderColor = 0
|
||||
upTvHeaderColor()
|
||||
postEvent(EventBus.UP_CONFIG, arrayListOf(2))
|
||||
}
|
||||
|
||||
1 -> ColorPickerDialog.newBuilder()
|
||||
.setShowAlphaSlider(false)
|
||||
.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())
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config) {
|
||||
class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config), FontSelectDialog.CallBack {
|
||||
|
||||
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 TITLE_COLOR = 7896
|
||||
const val B_COLOR = 114
|
||||
const val A_COLOR = 514
|
||||
}
|
||||
|
||||
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() {
|
||||
super.onStart()
|
||||
}
|
||||
@@ -41,6 +51,10 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
||||
binding.abtnBackgroundColor.color = ReadBookConfig.durConfig.curMenuBg()
|
||||
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() {
|
||||
@@ -53,16 +67,40 @@ class TipConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_tip_config
|
||||
}
|
||||
val weightOptions = context?.resources?.getStringArray(R.array.text_font_weight)
|
||||
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 {
|
||||
context?.alert(titleResource = R.string.text_font_weight_converter) {
|
||||
weightOptions?.let { options ->
|
||||
items(options.toList()) { _, 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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.abtnAccentColor.color = ReadBookConfig.durConfig.curMenuAc()
|
||||
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.dottedLine
|
||||
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.postEvent
|
||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||
@@ -39,6 +40,7 @@ class UnderlineConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_unde
|
||||
binding.swUnderline.isChecked = underline
|
||||
binding.swDottedline.isChecked = dottedLine
|
||||
binding.swDottedline.isEnabled = underline
|
||||
binding.swUnderlineExtend.isChecked = underlineExtend
|
||||
|
||||
binding.swUnderline.addOnCheckedChangeListener { _, isChecked ->
|
||||
underline = isChecked
|
||||
@@ -55,6 +57,11 @@ class UnderlineConfigDialog : BaseBottomSheetDialogFragment(R.layout.dialog_unde
|
||||
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 {
|
||||
progress = ReadBookConfig.durConfig.dottedBase.toInt()
|
||||
onChanged = {
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.LayoutInflater
|
||||
import android.widget.FrameLayout
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.graphics.drawable.toDrawable
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.view.ViewCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
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.dpToPx
|
||||
import io.legado.app.utils.gone
|
||||
import io.legado.app.utils.isContentScheme
|
||||
import io.legado.app.utils.setOnApplyWindowInsetsListenerCompat
|
||||
import io.legado.app.utils.statusBarHeight
|
||||
import splitties.views.backgroundColor
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
@@ -91,8 +94,11 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
upTipStyle()
|
||||
ReadBookConfig.let {
|
||||
val textColor = it.textColor
|
||||
val tipColor = with(ReadTipConfig) {
|
||||
if (tipColor == 0) textColor else tipColor
|
||||
val headerColor = with(ReadTipConfig) {
|
||||
if (tipHeaderColor == 0) textColor else tipHeaderColor
|
||||
}
|
||||
val footerColor = with(ReadTipConfig) {
|
||||
if (tipFooterColor == 0) textColor else tipFooterColor
|
||||
}
|
||||
val tipDividerColor = with(ReadTipConfig) {
|
||||
when (tipDividerColor) {
|
||||
@@ -101,12 +107,12 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
else -> tipDividerColor
|
||||
}
|
||||
}
|
||||
tvHeaderLeft.setColor(tipColor)
|
||||
tvHeaderMiddle.setColor(tipColor)
|
||||
tvHeaderRight.setColor(tipColor)
|
||||
tvFooterLeft.setColor(tipColor)
|
||||
tvFooterMiddle.setColor(tipColor)
|
||||
tvFooterRight.setColor(tipColor)
|
||||
tvHeaderLeft.setColor(headerColor)
|
||||
tvHeaderMiddle.setColor(headerColor)
|
||||
tvHeaderRight.setColor(headerColor)
|
||||
tvFooterLeft.setColor(footerColor)
|
||||
tvFooterMiddle.setColor(footerColor)
|
||||
tvFooterRight.setColor(footerColor)
|
||||
vwTopDivider.backgroundColor = tipDividerColor
|
||||
vwBottomDivider.backgroundColor = tipDividerColor
|
||||
upStatusBar()
|
||||
@@ -131,6 +137,24 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
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
|
||||
*/
|
||||
@@ -208,106 +232,108 @@ class PageView(context: Context) : FrameLayout(context) {
|
||||
tvFooterRight.batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
}
|
||||
}
|
||||
val tipTypeface = loadTypeface(ReadBookConfig.headerFont) ?: ChapterProvider.typeface
|
||||
val tipTextSize = ReadBookConfig.headerFontSize.toFloat()
|
||||
tvTitle = getTipView(ReadTipConfig.chapterTitle)?.apply {
|
||||
tag = ReadTipConfig.chapterTitle
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
}
|
||||
tvTitleArrow = getTipView(ReadTipConfig.chapterTitleArrow)?.apply {
|
||||
tag = ReadTipConfig.chapterTitleArrow
|
||||
typeface = Typeface.DEFAULT
|
||||
textSize = 12f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.ARROW
|
||||
}
|
||||
tvTitleArrowClassic = getTipView(ReadTipConfig.chapterTitleArrowClassic)?.apply {
|
||||
tag = ReadTipConfig.chapterTitleArrowClassic
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.ARROW
|
||||
}
|
||||
tvTime = getTipView(ReadTipConfig.time)?.apply {
|
||||
tag = ReadTipConfig.time
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
}
|
||||
tvBattery = getTipView(ReadTipConfig.battery)?.apply {
|
||||
tag = ReadTipConfig.battery
|
||||
typeface = Typeface.DEFAULT
|
||||
textSize = 11f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.OUTER
|
||||
}
|
||||
tvBatteryClassic = getTipView(ReadTipConfig.batteryClassic)?.apply {
|
||||
tag = ReadTipConfig.batteryClassic
|
||||
textSize = 11f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.CLASSIC
|
||||
}
|
||||
tvBatteryInside = getTipView(ReadTipConfig.batteryInside)?.apply {
|
||||
tag = ReadTipConfig.batteryInside
|
||||
typeface = Typeface.DEFAULT
|
||||
textSize = 11f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.INNER
|
||||
}
|
||||
tvBatteryIcon = getTipView(ReadTipConfig.batteryIcon)?.apply {
|
||||
tag = ReadTipConfig.batteryIcon
|
||||
typeface = Typeface.DEFAULT
|
||||
textSize = 11f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.ICON
|
||||
}
|
||||
tvPage = getTipView(ReadTipConfig.page)?.apply {
|
||||
tag = ReadTipConfig.page
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
}
|
||||
tvTotalProgress = getTipView(ReadTipConfig.totalProgress)?.apply {
|
||||
tag = ReadTipConfig.totalProgress
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
tvTotalProgress1 = getTipView(ReadTipConfig.totalProgress1)?.apply {
|
||||
tag = ReadTipConfig.totalProgress1
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
tvPageAndTotal = getTipView(ReadTipConfig.pageAndTotal)?.apply {
|
||||
tag = ReadTipConfig.pageAndTotal
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
tvBookName = getTipView(ReadTipConfig.bookName)?.apply {
|
||||
tag = ReadTipConfig.bookName
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
tvTimeBattery = getTipView(ReadTipConfig.timeBattery)?.apply {
|
||||
tag = ReadTipConfig.timeBattery
|
||||
typeface = Typeface.DEFAULT
|
||||
textSize = 11f
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.TIME
|
||||
}
|
||||
tvTimeBatteryClassic = getTipView(ReadTipConfig.timeBatteryClassic)?.apply {
|
||||
tag = ReadTipConfig.timeBatteryClassic
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 11f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
batteryMode = BatteryView.BatteryMode.CLASSIC
|
||||
}
|
||||
tvBatteryP = getTipView(ReadTipConfig.batteryPercentage)?.apply {
|
||||
tag = ReadTipConfig.batteryPercentage
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
tvTimeBatteryP = getTipView(ReadTipConfig.timeBatteryPercentage)?.apply {
|
||||
tag = ReadTipConfig.timeBatteryPercentage
|
||||
batteryMode = BatteryView.BatteryMode.NO_BATTERY
|
||||
typeface = ChapterProvider.typeface
|
||||
textSize = 12f
|
||||
typeface = tipTypeface
|
||||
textSize = tipTextSize
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -73,6 +73,8 @@ data class TextLine(
|
||||
fun addColumn(column: BaseColumn) {
|
||||
if (column !is TextColumn) {
|
||||
onlyTextColumn = false
|
||||
} else if (column.color != null) {
|
||||
onlyTextColumn = false
|
||||
}
|
||||
column.textLine = this
|
||||
textColumns.add(column)
|
||||
@@ -189,6 +191,8 @@ data class TextLine(
|
||||
}
|
||||
val textColor = if (isReadAloud) {
|
||||
ReadBookConfig.textAccentColor
|
||||
} else if (isTitle && ReadBookConfig.titleColor != 0) {
|
||||
ReadBookConfig.titleColor
|
||||
} else {
|
||||
ReadBookConfig.textColor
|
||||
}
|
||||
@@ -229,7 +233,17 @@ data class TextLine(
|
||||
null
|
||||
|
||||
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() {
|
||||
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) {
|
||||
val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt()
|
||||
val leftHeight = ceil(lines[leftLineSize - 1].lineBottom).toInt() + underlineExtraHeight
|
||||
renderHeight = max(renderHeight, leftHeight)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package io.legado.app.ui.book.read.page.entities.column
|
||||
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.Typeface
|
||||
import android.os.Build
|
||||
import androidx.annotation.Keep
|
||||
import androidx.core.net.toUri
|
||||
import io.legado.app.help.config.ReadBookConfig
|
||||
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.Companion.emptyTextLine
|
||||
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
|
||||
data class TextColumn(
|
||||
override var start: Float,
|
||||
override var end: Float,
|
||||
override val charData: String,
|
||||
var color: Int? = null,
|
||||
var fontPath: String? = null,
|
||||
) : TextBaseColumn {
|
||||
|
||||
override var textLine: TextLine = emptyTextLine
|
||||
@@ -47,19 +51,44 @@ data class TextColumn(
|
||||
} else {
|
||||
ChapterProvider.contentPaint
|
||||
}
|
||||
val textColor = if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
||||
val textColor = color ?: if (!textLine.useUnderline && (textLine.isReadAloud || isSearchResult)) {
|
||||
ReadBookConfig.textAccentColor
|
||||
} else if (textLine.isTitle && ReadBookConfig.titleColor != 0) {
|
||||
ReadBookConfig.titleColor
|
||||
} else {
|
||||
ReadBookConfig.textColor
|
||||
}
|
||||
val originalSize = textPaint.textSize
|
||||
textLine.titleTextSize?.let {
|
||||
textPaint.textSize = it
|
||||
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
|
||||
textPaint.textSize = textLine.titleTextSize!!
|
||||
if (needRestoreColor) textPaint.color = textColor
|
||||
if (needRestoreTypeface) textPaint.typeface = customTypeface
|
||||
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 (textPaint.color != textColor) {
|
||||
textPaint.color = textColor
|
||||
if (selected) {
|
||||
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
||||
}
|
||||
val y = textLine.lineBase - textLine.lineTop
|
||||
}
|
||||
|
||||
private fun drawText(canvas: Canvas, y: Float, textPaint: android.text.TextPaint) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM) {
|
||||
val letterSpacing = textPaint.letterSpacing * textPaint.textSize
|
||||
val letterSpacingHalf = letterSpacing * 0.5f
|
||||
@@ -67,10 +96,32 @@ data class TextColumn(
|
||||
} else {
|
||||
canvas.drawText(charData, start, y, textPaint)
|
||||
}
|
||||
if (selected) {
|
||||
canvas.drawRect(start, 0f, end, textLine.height, view.selectedPaint)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
textPaint.textSize = originalSize
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -934,30 +934,49 @@ object ChapterProvider {
|
||||
|
||||
@SuppressLint("UseKtx")
|
||||
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 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)
|
||||
Typeface.create(typeface, 900, false)
|
||||
Typeface.create(titleTypeface, 900, false)
|
||||
else
|
||||
bold
|
||||
titleBoldTypeface
|
||||
|
||||
2 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
Typeface.create(typeface, 300, false)
|
||||
Typeface.create(titleTypeface, 300, false)
|
||||
else
|
||||
normal
|
||||
titleNormalTypeface
|
||||
|
||||
0 -> if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
|
||||
Typeface.create(typeface, 400, false)
|
||||
Typeface.create(titleTypeface, 400, false)
|
||||
else
|
||||
normal
|
||||
titleNormalTypeface
|
||||
|
||||
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
|
||||
normal
|
||||
titleNormalTypeface
|
||||
|
||||
else -> normal
|
||||
else -> titleNormalTypeface
|
||||
}
|
||||
|
||||
val textFont = when (ReadBookConfig.textBold) {
|
||||
@@ -989,7 +1008,7 @@ object ChapterProvider {
|
||||
val tPaint = TextPaint()
|
||||
tPaint.color = ReadBookConfig.textColor
|
||||
tPaint.letterSpacing = ReadBookConfig.letterSpacing
|
||||
tPaint.typeface = titleFont
|
||||
tPaint.typeface = titleFontTypeface
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && ReadBookConfig.titleBold in 100..900)
|
||||
tPaint.setFontVariationSettings("'wght' ${ReadBookConfig.titleBold}")
|
||||
tPaint.textSize = with(ReadBookConfig) { textSize + titleSize }.toFloat().spToPx()
|
||||
|
||||
@@ -69,6 +69,14 @@ class TextChapterLayout(
|
||||
private val bookContent: BookContent,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val regexCache = mutableMapOf<String, Regex>()
|
||||
|
||||
fun invalidateRegexCache() {
|
||||
regexCache.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var listener: LayoutProgressListener? = textChapter
|
||||
|
||||
@@ -864,6 +872,7 @@ class TextChapterLayout(
|
||||
) {
|
||||
val widthsArray = allocateFloatArray(text.length)
|
||||
textPaint.getTextWidthsCompat(text, widthsArray)
|
||||
val colorMap = applyRegexColorRules(text)
|
||||
val layout = if (useZhLayout) {
|
||||
val (words, widths) = measureTextSplit(text, widthsArray)
|
||||
val indentSize = if (isFirstLine) paragraphIndent.length else 0
|
||||
@@ -915,12 +924,15 @@ class TextChapterLayout(
|
||||
val (words, widths) = measureTextSplit(lineText, widthsArray, lineStart)
|
||||
val desiredWidth = widths.fastSum()
|
||||
textLine.text = lineText
|
||||
val lineWordStyles = if (colorMap != null) {
|
||||
buildWordStyles(words, lineText, colorMap.colorArray, colorMap.fontPathArray, lineStart)
|
||||
} else null
|
||||
when (lineIndex) {
|
||||
0 if layout.lineCount > 1 && !isTitle && isFirstLine -> {
|
||||
//多行的第一行 非标题
|
||||
addCharsToLineFirst(
|
||||
book, absStartX, textLine, words, textPaint,
|
||||
desiredWidth, widths, srcList, clickList
|
||||
desiredWidth, widths, srcList, clickList, lineWordStyles
|
||||
)
|
||||
}
|
||||
|
||||
@@ -938,7 +950,7 @@ class TextChapterLayout(
|
||||
}
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, !isTitle && lineIndex == 0, widths, srcList, clickList
|
||||
startX, !isTitle && lineIndex == 0, widths, srcList, clickList, lineWordStyles
|
||||
)
|
||||
}
|
||||
else -> {
|
||||
@@ -951,13 +963,13 @@ class TextChapterLayout(
|
||||
val startX = (visibleWidth - desiredWidth) / 2
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, false, widths, srcList, clickList
|
||||
startX, false, widths, srcList, clickList, lineWordStyles
|
||||
)
|
||||
} else {
|
||||
//中间行
|
||||
addCharsToLineMiddle(
|
||||
book, absStartX, textLine, words, textPaint,
|
||||
desiredWidth, 0f, widths, srcList, clickList
|
||||
desiredWidth, 0f, widths, srcList, clickList, lineWordStyles
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1007,17 +1019,17 @@ class TextChapterLayout(
|
||||
textLine: TextLine,
|
||||
words: List<String>,
|
||||
textPaint: TextPaint,
|
||||
/**自然排版长度**/
|
||||
desiredWidth: Float,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?,
|
||||
clickList: LinkedList<String?>?
|
||||
clickList: LinkedList<String?>?,
|
||||
wordStyles: List<WordStyle>? = null
|
||||
) {
|
||||
var x = 0f
|
||||
if (!textFullJustify) {
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
x, true, textWidths, srcList, clickList
|
||||
x, true, textWidths, srcList, clickList, wordStyles
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -1038,9 +1050,10 @@ class TextChapterLayout(
|
||||
if (words.size > bodyIndent.length) {
|
||||
val text1 = words.subList(bodyIndent.length, words.size)
|
||||
val textWidths1 = textWidths.subList(bodyIndent.length, textWidths.size)
|
||||
val wordStyles1 = wordStyles?.subList(bodyIndent.length, wordStyles.size)
|
||||
addCharsToLineMiddle(
|
||||
book, absStartX, textLine, text1, textPaint,
|
||||
desiredWidth, x, textWidths1, srcList, clickList
|
||||
desiredWidth, x, textWidths1, srcList, clickList, wordStyles1
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1054,18 +1067,17 @@ class TextChapterLayout(
|
||||
textLine: TextLine,
|
||||
words: List<String>,
|
||||
textPaint: TextPaint,
|
||||
/**自然排版长度**/
|
||||
desiredWidth: Float,
|
||||
/**起始x坐标**/
|
||||
startX: Float,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?,
|
||||
clickList: LinkedList<String?>?
|
||||
clickList: LinkedList<String?>?,
|
||||
wordStyles: List<WordStyle>? = null
|
||||
) {
|
||||
if (!textFullJustify) {
|
||||
addCharsToLineNatural(
|
||||
book, absStartX, textLine, words,
|
||||
startX, false, textWidths, srcList, clickList
|
||||
startX, false, textWidths, srcList, clickList, wordStyles
|
||||
)
|
||||
return
|
||||
}
|
||||
@@ -1086,7 +1098,9 @@ class TextChapterLayout(
|
||||
}
|
||||
addCharToLine(
|
||||
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
|
||||
}
|
||||
@@ -1102,7 +1116,9 @@ class TextChapterLayout(
|
||||
val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
|
||||
addCharToLine(
|
||||
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
|
||||
}
|
||||
@@ -1122,7 +1138,8 @@ class TextChapterLayout(
|
||||
hasIndent: Boolean,
|
||||
textWidths: List<Float>,
|
||||
srcList: LinkedList<String>?,
|
||||
clickList: LinkedList<String?>?
|
||||
clickList: LinkedList<String?>?,
|
||||
wordStyles: List<WordStyle>? = null
|
||||
) {
|
||||
val indentLength = paragraphIndent.length
|
||||
var x = startX
|
||||
@@ -1140,7 +1157,9 @@ class TextChapterLayout(
|
||||
x1,
|
||||
index + 1 == words.size,
|
||||
srcList,
|
||||
clickList
|
||||
clickList,
|
||||
wordStyles?.getOrNull(index)?.color,
|
||||
wordStyles?.getOrNull(index)?.fontPath
|
||||
)
|
||||
x = x1
|
||||
if (hasIndent && index == indentLength - 1) {
|
||||
@@ -1162,7 +1181,9 @@ class TextChapterLayout(
|
||||
xEnd: Float,
|
||||
isLineEnd: Boolean,
|
||||
srcList: LinkedList<String>?,
|
||||
clickList: LinkedList<String?>?
|
||||
clickList: LinkedList<String?>?,
|
||||
color: Int? = null,
|
||||
fontPath: String? = null
|
||||
) {
|
||||
val column = when {
|
||||
!srcList.isNullOrEmpty() && (char == srcReplaceChar || char == reviewChar) -> {
|
||||
@@ -1176,19 +1197,14 @@ class TextChapterLayout(
|
||||
click = click
|
||||
)
|
||||
}
|
||||
// isLineEnd && char == ChapterProvider.reviewChar -> {
|
||||
// ReviewColumn(
|
||||
// start = absStartX + xStart,
|
||||
// end = absStartX + xEnd,
|
||||
// count = 10
|
||||
// )
|
||||
// }
|
||||
|
||||
else -> {
|
||||
TextColumn(
|
||||
start = absStartX + xStart,
|
||||
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
|
||||
}
|
||||
|
||||
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.apply {
|
||||
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("nextContentUrl", cr.nextContentUrl, R.string.rule_next_content))
|
||||
add(EditEntity("webJs", cr.webJs, R.string.rule_web_js))
|
||||
@@ -530,6 +531,7 @@ class BookSourceEditActivity :
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
"content" -> contentRule.content = viewModel.ruleComplete(it.value)
|
||||
"subContent" -> contentRule.subContent = viewModel.ruleComplete(it.value)
|
||||
"title" -> contentRule.title = viewModel.ruleComplete(it.value)
|
||||
"nextContentUrl" -> contentRule.nextContentUrl =
|
||||
viewModel.ruleComplete(it.value, type = 2)
|
||||
|
||||
@@ -7,10 +7,12 @@ import androidx.lifecycle.DefaultLifecycleObserver
|
||||
import androidx.lifecycle.LifecycleOwner
|
||||
import io.legado.app.utils.defaultSharedPreferences
|
||||
import io.legado.app.utils.getPrefBoolean
|
||||
import io.legado.app.utils.getPrefFloat
|
||||
import io.legado.app.utils.getPrefInt
|
||||
import io.legado.app.utils.getPrefLong
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.putPrefBoolean
|
||||
import io.legado.app.utils.putPrefFloat
|
||||
import io.legado.app.utils.putPrefInt
|
||||
import io.legado.app.utils.putPrefLong
|
||||
import io.legado.app.utils.putPrefString
|
||||
@@ -63,6 +65,7 @@ fun <T> prefDelegate(
|
||||
defaultValue is Int -> appCtx.getPrefInt(key, defaultValue) as T
|
||||
defaultValue is Boolean -> appCtx.getPrefBoolean(key, defaultValue) as T
|
||||
defaultValue is Long -> appCtx.getPrefLong(key, defaultValue) as T
|
||||
defaultValue is Float -> appCtx.getPrefFloat(key, defaultValue) as T
|
||||
else -> defaultValue
|
||||
}
|
||||
}
|
||||
@@ -78,6 +81,7 @@ fun <T> prefDelegate(
|
||||
is Int -> appCtx.putPrefInt(key, value)
|
||||
is Boolean -> appCtx.putPrefBoolean(key, value)
|
||||
is Long -> appCtx.putPrefLong(key, value)
|
||||
is Float -> appCtx.putPrefFloat(key, value)
|
||||
}
|
||||
_value.value = value
|
||||
onValueChange?.invoke(value)
|
||||
|
||||
@@ -53,6 +53,16 @@ object BookshelfConfig {
|
||||
*/
|
||||
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.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
@@ -86,7 +87,8 @@ fun CoverManageSheet(
|
||||
items(coverList) { path ->
|
||||
Box {
|
||||
NormalCard(
|
||||
cornerRadius = 12.dp
|
||||
cornerRadius = 12.dp,
|
||||
containerColor = Color.Transparent
|
||||
) {
|
||||
AsyncImage(
|
||||
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 autoCheckNewBackup by prefDelegate(PreferKey.autoCheckNewBackup, 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.Column
|
||||
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
|
||||
@@ -90,7 +91,10 @@ fun BackgroundImageManageSheet(
|
||||
.aspectRatio(16f / 9f)
|
||||
.padding(16.dp)
|
||||
) {
|
||||
Box(contentAlignment = Alignment.Center) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
AppIcon(
|
||||
imageVector = Icons.Default.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.PreferKey
|
||||
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.putPrefString
|
||||
import splitties.init.appCtx
|
||||
|
||||
data class TagColorPair(
|
||||
val textColor: Int = 0,
|
||||
val bgColor: Int = 0
|
||||
)
|
||||
|
||||
object ThemeConfig {
|
||||
|
||||
@@ -17,6 +26,16 @@ object ThemeConfig {
|
||||
|
||||
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 paletteStyle by prefDelegate(PreferKey.paletteStyle, "tonalSpot")
|
||||
@@ -56,9 +75,47 @@ object ThemeConfig {
|
||||
postEvent(EventBus.RECREATE, "")
|
||||
}
|
||||
|
||||
var cPrimary by prefDelegate(PreferKey.cPrimary, 0) {
|
||||
postEvent(EventBus.RECREATE, "")
|
||||
}
|
||||
var appFontPath: String?
|
||||
get() = appCtx.getPrefString(PreferKey.appFontPath)
|
||||
set(value) {
|
||||
appCtx.putPrefString(PreferKey.appFontPath, value)
|
||||
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) {
|
||||
postEvent(EventBus.RECREATE, "")
|
||||
@@ -70,6 +127,28 @@ object ThemeConfig {
|
||||
|
||||
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 showRss by prefDelegate(PreferKey.showRss, true)
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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.mainConfig.MainConfig
|
||||
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.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.RssSortRouteScreen
|
||||
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_THEME = "settings/theme"
|
||||
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_REMOTE = "import/remote"
|
||||
private const val ROUTE_CACHE = "cache"
|
||||
@@ -278,6 +282,12 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
@Serializable
|
||||
private data object MainRouteSettingsBackup : MainRoute
|
||||
|
||||
@Serializable
|
||||
private data object MainRouteSettingsCustomTheme : MainRoute
|
||||
|
||||
@Serializable
|
||||
private data object MainRouteSettingsThemePack : MainRoute
|
||||
|
||||
@Serializable
|
||||
private data object MainRouteImportLocal : MainRoute
|
||||
|
||||
@@ -543,13 +553,27 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsTheme> {
|
||||
ThemeConfigScreen(onBackClick = { navigateBack(backStack) })
|
||||
ThemeConfigScreen(
|
||||
onBackClick = { navigateBack(backStack) },
|
||||
onNavigateToCustomTheme = { backStack.add(MainRouteSettingsCustomTheme) }
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsBackup> {
|
||||
BackupConfigScreen(onBackClick = { navigateBack(backStack) })
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsCustomTheme> {
|
||||
CustomThemeScreen(
|
||||
onBackClick = { navigateBack(backStack) },
|
||||
onNavigateToThemePack = { backStack.add(MainRouteSettingsThemePack) }
|
||||
)
|
||||
}
|
||||
|
||||
entry<MainRouteSettingsThemePack> {
|
||||
ThemePackScreen(onBackClick = { navigateBack(backStack) })
|
||||
}
|
||||
|
||||
entry<MainRouteImportLocal> {
|
||||
ImportBookScreen(
|
||||
onBackClick = { navigateBack(backStack) }
|
||||
@@ -599,7 +623,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
}
|
||||
|
||||
DisposableEffect(lifecycleOwner, searchViewModel) {
|
||||
searchViewModel.onIntent(SearchIntent.ResumeEngine)
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
when (event) {
|
||||
Lifecycle.Event.ON_RESUME -> {
|
||||
@@ -616,7 +639,6 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
searchViewModel.onIntent(SearchIntent.PauseEngine)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,15 +667,25 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
initialSortUrl = route.sortUrl,
|
||||
onBackClick = { navigateBack(backStack) },
|
||||
onOpenRead = { title, origin, link, openUrl ->
|
||||
navigateToRoute(
|
||||
backStack,
|
||||
MainRouteRssRead(
|
||||
title = title,
|
||||
origin = origin,
|
||||
link = link,
|
||||
openUrl = openUrl
|
||||
if (link?.contains("@js:") == true) {
|
||||
navigateToRoute(
|
||||
backStack,
|
||||
MainRouteRssSort(
|
||||
sourceUrl = origin,
|
||||
sortUrl = link
|
||||
)
|
||||
)
|
||||
)
|
||||
} else {
|
||||
navigateToRoute(
|
||||
backStack,
|
||||
MainRouteRssRead(
|
||||
title = title,
|
||||
origin = origin,
|
||||
link = link,
|
||||
openUrl = openUrl
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@@ -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>()
|
||||
BookInfoRouteScreen(
|
||||
bookUrl = route.bookUrl,
|
||||
@@ -734,7 +777,9 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
MainRouteSettingsRead,
|
||||
MainRouteSettingsCover,
|
||||
MainRouteSettingsTheme,
|
||||
MainRouteSettingsBackup -> {
|
||||
MainRouteSettingsBackup,
|
||||
MainRouteSettingsCustomTheme,
|
||||
MainRouteSettingsThemePack -> {
|
||||
backStack.clear()
|
||||
backStack.add(MainRouteHome)
|
||||
backStack.add(MainRouteSettings)
|
||||
@@ -969,6 +1014,16 @@ open class MainActivity : BaseComposeActivity(), VariableDialog.Callback {
|
||||
|
||||
private fun resolveStartRoute(route: String?): MainRoute {
|
||||
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_SETTINGS -> MainRouteSettings
|
||||
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.navigationBars
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.pager.HorizontalPager
|
||||
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.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.kyant.backdrop.backdrops.layerBackdrop
|
||||
import com.kyant.backdrop.backdrops.rememberLayerBackdrop
|
||||
import dev.chrisbanes.haze.HazeState
|
||||
import dev.chrisbanes.haze.hazeSource
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.painter.BitmapPainter
|
||||
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.BookshelfViewModel
|
||||
import io.legado.app.ui.main.explore.ExploreScreen
|
||||
@@ -149,7 +155,11 @@ fun MainScreen(
|
||||
}
|
||||
|
||||
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 {
|
||||
drawRect(floatingBarSurfaceColor)
|
||||
drawContent()
|
||||
@@ -270,7 +280,13 @@ fun MainScreen(
|
||||
}
|
||||
},
|
||||
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
|
||||
)
|
||||
}
|
||||
@@ -280,88 +296,37 @@ fun MainScreen(
|
||||
AppScaffold(
|
||||
modifier = Modifier.weight(1f),
|
||||
bottomBar = {
|
||||
if (!useRail && mainUiState.showBottomView) {
|
||||
if (useFloatingBottomBar) {
|
||||
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)
|
||||
}
|
||||
if (!useRail && mainUiState.showBottomView && !useFloatingBottomBar) {
|
||||
AppNavigationBar() {
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
val customIconPath = when (destination) {
|
||||
MainDestination.Bookshelf -> MainConfig.navIconBookshelf
|
||||
MainDestination.Explore -> MainConfig.navIconExplore
|
||||
MainDestination.Rss -> MainConfig.navIconRss
|
||||
MainDestination.My -> MainConfig.navIconMy
|
||||
}
|
||||
AppNavigationBarItem(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
backdrop = floatingBarBackdrop,
|
||||
tabsCount = destinations.size,
|
||||
isBlurEnabled = useLiquidGlass
|
||||
) {
|
||||
destinations.forEachIndexed { index, destination ->
|
||||
val selected = pagerState.targetPage == index
|
||||
FloatingBottomBarItem(
|
||||
onClick = {
|
||||
coroutineScope.launch {
|
||||
pagerState.animateScrollToPage(index)
|
||||
}
|
||||
},
|
||||
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(
|
||||
selected = selected,
|
||||
onClick = {
|
||||
coroutineScope.launch { pagerState.animateScrollToPage(index) }
|
||||
},
|
||||
labelString = stringResource(destination.labelId),
|
||||
iconVector = AppIcons.mainDestination(destination, selected),
|
||||
m3Icon = {
|
||||
NavigationIcon(
|
||||
destination = destination,
|
||||
selected = selected
|
||||
)
|
||||
},
|
||||
m3IndicatorColor = GlassDefaults.glassColor(
|
||||
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||
),
|
||||
m3ShowLabel = showLabel,
|
||||
m3AlwaysShowLabel = alwaysShowLabel
|
||||
)
|
||||
}
|
||||
labelString = stringResource(destination.labelId),
|
||||
iconVector = AppIcons.mainDestination(destination, selected),
|
||||
m3Icon = {
|
||||
NavigationIcon(
|
||||
destination = destination,
|
||||
selected = selected
|
||||
)
|
||||
},
|
||||
m3IndicatorColor = GlassDefaults.glassColor(
|
||||
noBlurColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
blurAlpha = GlassDefaults.ThickBlurAlpha
|
||||
),
|
||||
m3ShowLabel = showLabel && !customIconPath.isNotEmpty(),
|
||||
m3AlwaysShowLabel = alwaysShowLabel && !customIconPath.isNotEmpty(),
|
||||
useCustomIcon = customIconPath.isNotEmpty()
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -369,60 +334,136 @@ fun MainScreen(
|
||||
contentWindowInsets = WindowInsets(0)
|
||||
) { _ ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.hazeSource(hazeState)
|
||||
.then(
|
||||
if (useFloatingBottomBar) {
|
||||
Modifier.layerBackdrop(floatingBarBackdrop)
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
Box(
|
||||
modifier = Modifier.then(
|
||||
if (useLiquidGlass) {
|
||||
Modifier
|
||||
.hazeSource(hazeState)
|
||||
.layerBackdrop(floatingBarBackdrop)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
userScrollEnabled = true,
|
||||
beyondViewportPageCount = 1
|
||||
) { page ->
|
||||
val destination = destinations.getOrNull(page) ?: return@HorizontalPager
|
||||
when (destination) {
|
||||
MainDestination.Bookshelf -> BookshelfScreen(
|
||||
onBookClick = { book ->
|
||||
context.startActivityForBook(book)
|
||||
},
|
||||
onBookLongClick = { book ->
|
||||
onNavigateToBookInfo(book.name, book.author, book.bookUrl)
|
||||
},
|
||||
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||
onNavigateToLocalImport = onNavigateToLocalImport,
|
||||
onNavigateToCache = onNavigateToCache,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
) {
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
userScrollEnabled = true,
|
||||
beyondViewportPageCount = 1
|
||||
) { page ->
|
||||
val destination = destinations.getOrNull(page) ?: return@HorizontalPager
|
||||
when (destination) {
|
||||
MainDestination.Bookshelf -> BookshelfScreen(
|
||||
onBookClick = { book ->
|
||||
context.startActivityForBook(book)
|
||||
},
|
||||
onBookLongClick = { book ->
|
||||
onNavigateToBookInfo(book.name, book.author, book.bookUrl)
|
||||
},
|
||||
onNavigateToSearch = { query -> onNavigateToSearch(query) },
|
||||
onNavigateToRemoteImport = onNavigateToRemoteImport,
|
||||
onNavigateToLocalImport = onNavigateToLocalImport,
|
||||
onNavigateToCache = onNavigateToCache,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
|
||||
MainDestination.Explore -> ExploreScreen(
|
||||
onOpenExploreShow = onNavigateToExploreShow
|
||||
)
|
||||
MainDestination.Rss -> RssScreen(
|
||||
onOpenSort = { sourceUrl, sortUrl, key ->
|
||||
onNavigateToRssSort(sourceUrl, sortUrl, key)
|
||||
MainDestination.Explore -> ExploreScreen(
|
||||
onOpenExploreShow = onNavigateToExploreShow
|
||||
)
|
||||
MainDestination.Rss -> RssScreen(
|
||||
onOpenSort = { sourceUrl, sortUrl, key ->
|
||||
onNavigateToRssSort(sourceUrl, sortUrl, key)
|
||||
},
|
||||
onOpenRead = { title, origin, link, openUrl ->
|
||||
onNavigateToRssRead(title, origin, link, openUrl)
|
||||
}
|
||||
)
|
||||
MainDestination.My -> MyScreen(
|
||||
onOpenSettings = onOpenSettings,
|
||||
onNavigate = { event ->
|
||||
if (event == PrefClickEvent.OpenBookCacheManage) {
|
||||
onNavigateToBookCacheManage()
|
||||
} else {
|
||||
viewModel.onPrefClickEvent(event)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
},
|
||||
onOpenRead = { title, origin, link, openUrl ->
|
||||
onNavigateToRssRead(title, origin, link, openUrl)
|
||||
}
|
||||
)
|
||||
MainDestination.My -> MyScreen(
|
||||
onOpenSettings = onOpenSettings,
|
||||
onNavigate = { event ->
|
||||
if (event == PrefClickEvent.OpenBookCacheManage) {
|
||||
onNavigateToBookCacheManage()
|
||||
} else {
|
||||
viewModel.onPrefClickEvent(event)
|
||||
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,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
val icon = AppIcons.mainDestination(destination, selected)
|
||||
AppIcon(icon, contentDescription = null, 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)
|
||||
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.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
import androidx.compose.foundation.layout.aspectRatio
|
||||
@@ -24,19 +26,29 @@ 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.draw.drawWithContent
|
||||
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.Color
|
||||
import androidx.compose.ui.graphics.PathEffect
|
||||
import androidx.compose.ui.graphics.Shadow
|
||||
import androidx.compose.ui.graphics.drawscope.Stroke
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
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 io.legado.app.R
|
||||
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.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.widget.components.card.GlassCard
|
||||
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.BookshelfCover
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
import io.legado.app.utils.toTimeAgo
|
||||
|
||||
/**
|
||||
@@ -63,24 +76,62 @@ fun BookshelfItem(
|
||||
titleEnd: @Composable (() -> Unit)? = null,
|
||||
subTitle: String? = null,
|
||||
desc: String? = null,
|
||||
descMaxLines: Int = 1,
|
||||
extra: @Composable (RowScope.() -> Unit)? = null,
|
||||
bottomContent: @Composable (() -> Unit)? = null,
|
||||
titleSmallFont: Boolean = false,
|
||||
titleCenter: Boolean = true,
|
||||
titleMaxLines: Int = 2,
|
||||
coverShadow: Boolean = false,
|
||||
titleColor: Color? = null,
|
||||
descAnnotated: AnnotatedString? = null,
|
||||
onClick: () -> Unit,
|
||||
onLongClick: (() -> Unit)?
|
||||
) {
|
||||
val selectedColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.secondaryContainer
|
||||
val containerColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||
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 {
|
||||
LegadoTheme.colorScheme.surface.copy(alpha = 0f)
|
||||
Modifier
|
||||
}
|
||||
|
||||
if (isGrid) {
|
||||
Box(
|
||||
modifier = modifier
|
||||
.clip(MaterialTheme.shapes.small)
|
||||
.background(selectedColor)
|
||||
.combinedClickable(
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
@@ -99,7 +150,7 @@ fun BookshelfItem(
|
||||
MaterialTheme.shapes.extraSmall
|
||||
) else Modifier
|
||||
)
|
||||
.clip(MaterialTheme.shapes.extraSmall) // 先阴影后裁剪
|
||||
.clip(MaterialTheme.shapes.extraSmall)
|
||||
) {
|
||||
cover(Modifier.fillMaxSize())
|
||||
if (gridStyle == 1) {
|
||||
@@ -146,14 +197,18 @@ fun BookshelfItem(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 列表布局
|
||||
Column {
|
||||
NormalCard(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(all = 4.dp),
|
||||
.padding(all = 4.dp)
|
||||
.then(borderModifier),
|
||||
cornerRadius = 8.dp,
|
||||
containerColor = selectedColor,
|
||||
containerColor = if (isSelected) {
|
||||
LegadoTheme.colorScheme.secondaryContainer
|
||||
} else {
|
||||
containerColor
|
||||
},
|
||||
onClick = onClick,
|
||||
onLongClick = onLongClick
|
||||
) {
|
||||
@@ -177,7 +232,11 @@ fun BookshelfItem(
|
||||
) {
|
||||
AppText(
|
||||
text = title,
|
||||
style = LegadoTheme.typography.titleMediumEmphasized,
|
||||
style = if (titleColor != null) {
|
||||
LegadoTheme.typography.titleMediumEmphasized.copy(color = titleColor)
|
||||
} else {
|
||||
LegadoTheme.typography.titleMediumEmphasized
|
||||
},
|
||||
maxLines = 1,
|
||||
overflow = TextOverflow.Ellipsis,
|
||||
modifier = Modifier.weight(1f)
|
||||
@@ -193,13 +252,22 @@ fun BookshelfItem(
|
||||
)
|
||||
}
|
||||
if (!isCompact) {
|
||||
desc?.let {
|
||||
if (descAnnotated != null) {
|
||||
AppText(
|
||||
text = it,
|
||||
text = descAnnotated,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
maxLines = 1,
|
||||
maxLines = descMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
} else {
|
||||
desc?.let {
|
||||
AppText(
|
||||
text = it,
|
||||
style = LegadoTheme.typography.labelSmallEmphasized,
|
||||
maxLines = descMaxLines,
|
||||
overflow = TextOverflow.Ellipsis
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
extra?.let {
|
||||
@@ -210,6 +278,7 @@ fun BookshelfItem(
|
||||
}
|
||||
}
|
||||
}
|
||||
bottomContent?.invoke()
|
||||
}
|
||||
if (BookshelfConfig.bookshelfShowDivider)
|
||||
HorizontalDivider(
|
||||
@@ -232,7 +301,6 @@ fun BookGroupCover(
|
||||
modifier = modifier
|
||||
.aspectRatio(5f / 7f)
|
||||
.clip(RoundedCornerShape(4.dp))
|
||||
.background(LegadoTheme.colorScheme.surfaceContainer)
|
||||
) {
|
||||
if (!coverPath.isNullOrBlank()) {
|
||||
CoilBookCover(
|
||||
@@ -326,7 +394,7 @@ fun BookGroupCover(
|
||||
|
||||
@Composable
|
||||
fun BookGroupItemGrid(
|
||||
group: BookGroup,
|
||||
group: BookGroupUi,
|
||||
previewBooks: List<BookShelfItem>,
|
||||
countText: String? = null,
|
||||
gridStyle: Int = 0,
|
||||
@@ -363,7 +431,7 @@ fun BookGroupItemGrid(
|
||||
|
||||
@Composable
|
||||
fun BookGroupItemList(
|
||||
group: BookGroup,
|
||||
group: BookGroupUi,
|
||||
previewBooks: List<BookShelfItem>,
|
||||
countText: String? = null,
|
||||
isCompact: Boolean = false,
|
||||
@@ -375,13 +443,27 @@ fun BookGroupItemList(
|
||||
onClick: () -> 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(
|
||||
isGrid = false,
|
||||
gridStyle = 0,
|
||||
isCompact = isCompact,
|
||||
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
|
||||
title = group.groupName,
|
||||
titleColor = primaryColor,
|
||||
subTitle = countText,
|
||||
descAnnotated = descAnnotated,
|
||||
titleSmallFont = titleSmallFont,
|
||||
titleCenter = titleCenter,
|
||||
titleMaxLines = titleMaxLines,
|
||||
@@ -485,6 +567,52 @@ fun BookItem(
|
||||
book.author
|
||||
},
|
||||
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 = {
|
||||
if (BookshelfConfig.showLastUpdateTime && !book.isLocal) {
|
||||
AppText(
|
||||
|
||||
@@ -25,7 +25,9 @@ data class BookShelfItem(
|
||||
val type: Int,
|
||||
val group: Long,
|
||||
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
|
||||
|
||||
|
||||
@@ -12,6 +12,10 @@ import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.ExperimentalMaterial3Api
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.platform.LocalConfiguration
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
@@ -19,8 +23,11 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import io.legado.app.R
|
||||
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.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.CompactSliderSettingItem
|
||||
import io.legado.app.ui.widget.components.settingItem.CompactSwitchSettingItem
|
||||
@@ -33,6 +40,7 @@ fun BookshelfConfigSheet(
|
||||
) {
|
||||
val configuration = LocalConfiguration.current
|
||||
val isLandscape = configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
|
||||
var showLabelColorManage by remember { mutableStateOf(false) }
|
||||
|
||||
AppModalBottomSheet(
|
||||
title = stringResource(R.string.bookshelf_layout),
|
||||
@@ -234,6 +242,38 @@ fun BookshelfConfigSheet(
|
||||
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(
|
||||
title = stringResource(R.string.show_wait_up_count),
|
||||
checked = BookshelfConfig.showWaitUpCount,
|
||||
@@ -265,5 +305,10 @@ fun BookshelfConfigSheet(
|
||||
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 io.legado.app.R
|
||||
import io.legado.app.base.BaseRuleEvent
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
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.config.bookshelfConfig.BookshelfConfig
|
||||
import io.legado.app.ui.main.bookCoverSharedElementKey
|
||||
@@ -212,17 +212,6 @@ fun BookshelfScreen(
|
||||
val latestGroups by rememberUpdatedState(uiState.groups)
|
||||
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) {
|
||||
snapshotFlow { pagerState.settledPage }
|
||||
.distinctUntilChanged()
|
||||
@@ -709,10 +698,6 @@ fun BookshelfScreen(
|
||||
paddingValues = paddingValues,
|
||||
books = uiState.items,
|
||||
uiState = uiState,
|
||||
bookshelfLayoutMode = bookshelfLayoutMode,
|
||||
bookshelfLayoutGrid = bookshelfLayoutGrid,
|
||||
bookshelfLayoutList = bookshelfLayoutList,
|
||||
isEditMode = isEditMode,
|
||||
selectedBookUrls = selectedBookUrls,
|
||||
canReorderBooks = false,
|
||||
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
||||
@@ -725,6 +710,7 @@ fun BookshelfScreen(
|
||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||
onBookClick = onBookClick,
|
||||
onBookLongClick = onBookLongClick,
|
||||
isCurrentPage = true,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
@@ -732,17 +718,15 @@ fun BookshelfScreen(
|
||||
HorizontalPager(
|
||||
state = pagerState,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
beyondViewportPageCount = 1,
|
||||
beyondViewportPageCount = 2,
|
||||
key = { if (it < uiState.groups.size) uiState.groups[it].groupId else it }
|
||||
) { pageIndex ->
|
||||
val group = uiState.groups.getOrNull(pageIndex)
|
||||
if (group != null) {
|
||||
val isSelectedGroup = group.groupId == uiState.selectedGroupId
|
||||
val books = if (isSelectedGroup) {
|
||||
uiState.items
|
||||
} else {
|
||||
emptyList()
|
||||
}
|
||||
val books = uiState.allGroupBooks[group.groupId]
|
||||
?: if (isSelectedGroup) uiState.items
|
||||
else emptyList()
|
||||
val canReorderBooks = isEditMode &&
|
||||
!uiState.isSearch &&
|
||||
(group.bookSort.takeIf { it >= 0 }
|
||||
@@ -752,10 +736,6 @@ fun BookshelfScreen(
|
||||
paddingValues = paddingValues,
|
||||
books = books,
|
||||
uiState = uiState,
|
||||
bookshelfLayoutMode = bookshelfLayoutMode,
|
||||
bookshelfLayoutGrid = bookshelfLayoutGrid,
|
||||
bookshelfLayoutList = bookshelfLayoutList,
|
||||
isEditMode = isEditMode,
|
||||
selectedBookUrls = selectedBookUrls,
|
||||
canReorderBooks = canReorderBooks,
|
||||
onToggleBookSelection = { toggleBookSelection(it.bookUrl) },
|
||||
@@ -788,6 +768,7 @@ fun BookshelfScreen(
|
||||
onGlobalSearch = { onNavigateToSearch(uiState.searchKey.trim()) },
|
||||
onBookClick = onBookClick,
|
||||
onBookLongClick = onBookLongClick,
|
||||
isCurrentPage = isSelectedGroup,
|
||||
sharedTransitionScope = sharedTransitionScope,
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
)
|
||||
@@ -1022,10 +1003,6 @@ fun BookshelfPage(
|
||||
paddingValues: PaddingValues,
|
||||
books: List<BookShelfItem>,
|
||||
uiState: BookshelfUiState,
|
||||
bookshelfLayoutMode: Int,
|
||||
bookshelfLayoutGrid: Int,
|
||||
bookshelfLayoutList: Int,
|
||||
isEditMode: Boolean,
|
||||
selectedBookUrls: Set<String>,
|
||||
canReorderBooks: Boolean,
|
||||
onToggleBookSelection: (BookShelfItem) -> Unit,
|
||||
@@ -1038,10 +1015,12 @@ fun BookshelfPage(
|
||||
onGlobalSearch: () -> Unit,
|
||||
onBookClick: (BookShelfItem) -> Unit,
|
||||
onBookLongClick: (BookShelfItem) -> Unit,
|
||||
isCurrentPage: Boolean = true,
|
||||
sharedTransitionScope: SharedTransitionScope? = null,
|
||||
animatedVisibilityScope: AnimatedVisibilityScope? = null,
|
||||
) {
|
||||
if (books.isEmpty()) {
|
||||
if (!isCurrentPage) return
|
||||
if (uiState.isSearch) {
|
||||
EmptyMessage(
|
||||
modifier = Modifier
|
||||
@@ -1068,6 +1047,14 @@ fun BookshelfPage(
|
||||
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 isGridMode = bookshelfLayoutMode != 0
|
||||
val totalHorizontalPadding =
|
||||
@@ -1148,7 +1135,7 @@ fun BookshelfPage(
|
||||
animatedVisibilityScope = animatedVisibilityScope,
|
||||
sharedCoverKey = bookCoverSharedElementKey(book.bookUrl),
|
||||
onClick = {
|
||||
if (isEditMode) {
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
} else {
|
||||
onBookClick(book)
|
||||
@@ -1158,7 +1145,7 @@ fun BookshelfPage(
|
||||
null
|
||||
} else {
|
||||
{
|
||||
if (isEditMode) {
|
||||
if (uiState.isEditMode) {
|
||||
onToggleBookSelection(book)
|
||||
} else {
|
||||
onBookLongClick(book)
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.ui.main.bookshelf
|
||||
|
||||
import androidx.compose.runtime.Stable
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import io.legado.app.ui.widget.components.list.ListUiState
|
||||
import kotlinx.collections.immutable.ImmutableList
|
||||
@@ -9,8 +10,9 @@ import kotlinx.collections.immutable.persistentListOf
|
||||
import kotlinx.collections.immutable.persistentMapOf
|
||||
import kotlinx.collections.immutable.persistentSetOf
|
||||
|
||||
@Stable
|
||||
data class BookshelfGroupSelectorState(
|
||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val groups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||
val selectedGroupIndex: Int = 0,
|
||||
val selectedGroupId: Long = BookGroup.IdAll
|
||||
)
|
||||
@@ -27,14 +29,15 @@ sealed interface BookshelfOverlay {
|
||||
data object BatchDownloadConfirmDialog : BookshelfOverlay
|
||||
}
|
||||
|
||||
@Stable
|
||||
data class BookshelfUiState(
|
||||
override val items: ImmutableList<BookShelfItem> = persistentListOf(),
|
||||
override val selectedIds: ImmutableSet<Any> = persistentSetOf(),
|
||||
override val searchKey: String = "",
|
||||
override val isSearch: Boolean = false,
|
||||
override val isLoading: Boolean = false,
|
||||
val groups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val allGroups: ImmutableList<BookGroup> = persistentListOf(),
|
||||
val groups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||
val allGroups: ImmutableList<BookGroupUi> = persistentListOf(),
|
||||
val groupPreviews: ImmutableMap<Long, ImmutableList<BookShelfItem>> = persistentMapOf(),
|
||||
val groupBookCounts: ImmutableMap<Long, Int> = persistentMapOf(),
|
||||
val currentGroupBookCount: Int = 0,
|
||||
@@ -56,5 +59,6 @@ data class BookshelfUiState(
|
||||
val subtitle: String? = null,
|
||||
val currentGroupName: String? = 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>
|
||||
|
||||
@@ -73,10 +73,13 @@ import kotlinx.coroutines.flow.SharingStarted
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.catch
|
||||
import kotlinx.coroutines.flow.collect
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.combine
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
import kotlinx.coroutines.flow.flatMapLatest
|
||||
import kotlinx.coroutines.flow.flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.receiveAsFlow
|
||||
import kotlinx.coroutines.flow.stateIn
|
||||
@@ -158,10 +161,6 @@ class BookshelfViewModel(
|
||||
val allGroupsFlow: StateFlow<List<BookGroup>> = bookGroupRepository.flowAll()
|
||||
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
|
||||
|
||||
private val allBooksFlow = appDb.bookDao.flowBookShelf()
|
||||
.distinctUntilChanged()
|
||||
.flowOn(Dispatchers.Default)
|
||||
|
||||
private data class GroupPreviewState(
|
||||
val previews: ImmutableMap<Long, ImmutableList<BookShelfItem>>,
|
||||
val counts: ImmutableMap<Long, Int>,
|
||||
@@ -173,7 +172,7 @@ class BookshelfViewModel(
|
||||
groupIdFlow
|
||||
) { groups, selectedGroupId ->
|
||||
BookshelfGroupSelectorState(
|
||||
groups = groups.toImmutableList(),
|
||||
groups = groups.map { it.toBookGroupUi() }.toImmutableList(),
|
||||
selectedGroupIndex = groups.indexOfFirst { it.groupId == selectedGroupId }
|
||||
.coerceAtLeast(0),
|
||||
selectedGroupId = selectedGroupId
|
||||
@@ -197,6 +196,25 @@ class BookshelfViewModel(
|
||||
}
|
||||
}.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(
|
||||
booksFlow,
|
||||
searchKeyFlow,
|
||||
@@ -213,15 +231,45 @@ class BookshelfViewModel(
|
||||
selectedBookUrls.intersect(visibleBookUrls)
|
||||
}.distinctUntilChanged()
|
||||
|
||||
private val groupPreviewsFlow =
|
||||
combine(
|
||||
groupsFlow,
|
||||
allBooksFlow,
|
||||
bookGroupStyleFlow,
|
||||
sortConfigFlow
|
||||
) { groups, allBooks, bookGroupStyle, sortConfig ->
|
||||
buildGroupPreviewState(groups, allBooks, bookGroupStyle, sortConfig)
|
||||
}.distinctUntilChanged().flowOn(Dispatchers.Default)
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
private val groupPreviewsFlow = combine(
|
||||
groupsFlow,
|
||||
bookGroupStyleFlow,
|
||||
appDb.bookDao.flowSystemGroupCounts()
|
||||
) { groups, bookGroupStyle, systemCounts ->
|
||||
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)
|
||||
|
||||
private val coreInternalStateFlow = combine(
|
||||
groupIdFlow,
|
||||
@@ -309,16 +357,28 @@ class BookshelfViewModel(
|
||||
}
|
||||
|
||||
private val dataStateFlow = combine(
|
||||
booksFlow,
|
||||
groupsFlow,
|
||||
allGroupsFlow,
|
||||
groupPreviewsFlow,
|
||||
internalStateFlow
|
||||
) { books, groups, allGroups, previews, internal ->
|
||||
BookshelfDataState(books, groups, allGroups, previews, internal)
|
||||
combine(
|
||||
booksFlow,
|
||||
groupsFlow,
|
||||
allGroupsFlow,
|
||||
groupPreviewsFlow,
|
||||
internalStateFlow
|
||||
) { 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 groups: List<BookGroup>,
|
||||
val allGroups: List<BookGroup>,
|
||||
@@ -326,6 +386,15 @@ class BookshelfViewModel(
|
||||
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(
|
||||
dataStateFlow,
|
||||
interactionStateFlow
|
||||
@@ -383,7 +452,8 @@ class BookshelfViewModel(
|
||||
},
|
||||
currentGroupName = currentGroupName,
|
||||
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())
|
||||
|
||||
@@ -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) {
|
||||
if (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.animateFloatAsState
|
||||
import androidx.compose.animation.core.tween
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.ExperimentalFoundationApi
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
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.source.edit.BookSourceEditActivity
|
||||
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.composeEngine
|
||||
import io.legado.app.ui.theme.ThemeResolver
|
||||
@@ -286,12 +288,22 @@ fun ExploreScreen(
|
||||
.align(Alignment.TopStart)
|
||||
.padding(top = paddingValues.calculateTopPadding() + 4.dp, start = 8.dp)
|
||||
) { 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(
|
||||
text = item.bookSourceName,
|
||||
textStyle = LegadoTheme.typography.labelMediumEmphasized,
|
||||
cornerRadius = 12.dp,
|
||||
horizontalPadding = 12.dp,
|
||||
verticalPadding = 8.dp,
|
||||
border = if (enableBorder) {
|
||||
BorderStroke(borderWidth, borderColor)
|
||||
} else null,
|
||||
onClick = {
|
||||
scope.launch {
|
||||
val index =
|
||||
@@ -362,12 +374,23 @@ fun ExploreSourceHeader(
|
||||
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(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.padding(vertical = 4.dp),
|
||||
cornerRadius = 12.dp,
|
||||
containerColor = containerColor
|
||||
containerColor = containerColor,
|
||||
border = if (enableBorder) {
|
||||
BorderStroke(borderWidth, borderColor)
|
||||
} else null
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.ui.rss.article
|
||||
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
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.cover.buildCoverImageRequest
|
||||
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.flow.catch
|
||||
import kotlinx.coroutines.flow.flowOn
|
||||
@@ -92,6 +96,7 @@ fun RssArticlesPage(
|
||||
rssUrl: String?,
|
||||
rssSource: RssSource?,
|
||||
viewModel: RssArticlesViewModel,
|
||||
searchKey: String? = null,
|
||||
onRead: (RssArticle) -> Unit,
|
||||
paddingValues: PaddingValues = PaddingValues(0.dp),
|
||||
modifier: Modifier = Modifier
|
||||
@@ -99,9 +104,10 @@ fun RssArticlesPage(
|
||||
val context = LocalContext.current
|
||||
val layout = remember(articleStyle) { articleStyle.toRssArticleLayout() }
|
||||
val loadState by viewModel.loadState.collectAsStateWithLifecycle()
|
||||
val isPreload = remember(rssSource) { rssSource?.preload == true }
|
||||
|
||||
LaunchedEffect(sortName, sortUrl) {
|
||||
viewModel.init(sortName, sortUrl)
|
||||
LaunchedEffect(sortName, sortUrl, searchKey) {
|
||||
viewModel.init(sortName, sortUrl, searchKey)
|
||||
}
|
||||
|
||||
val articleFlow = remember(rssUrl, sortName) {
|
||||
@@ -117,7 +123,11 @@ fun RssArticlesPage(
|
||||
val articles by articleFlow.collectAsStateWithLifecycle(initialValue = emptyList())
|
||||
|
||||
LaunchedEffect(rssSource?.sourceUrl, sortName, sortUrl) {
|
||||
rssSource?.let(viewModel::loadArticles)
|
||||
rssSource?.let { source ->
|
||||
if (isPreload) {
|
||||
viewModel.loadArticles(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(loadState.errorMessage) {
|
||||
@@ -142,6 +152,7 @@ fun RssArticlesPage(
|
||||
LoadMoreDetector(
|
||||
state = listState,
|
||||
enabled = loadState.canLoadMore,
|
||||
preloadThreshold = if (isPreload) 5 else 3,
|
||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
LazyColumn(
|
||||
@@ -174,6 +185,7 @@ fun RssArticlesPage(
|
||||
GridLoadMoreDetector(
|
||||
state = gridState,
|
||||
enabled = loadState.canLoadMore,
|
||||
preloadThreshold = if (isPreload) 5 else 3,
|
||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
LazyVerticalGrid(
|
||||
@@ -208,6 +220,7 @@ fun RssArticlesPage(
|
||||
StaggeredLoadMoreDetector(
|
||||
state = staggeredState,
|
||||
enabled = loadState.canLoadMore,
|
||||
preloadThreshold = if (isPreload) 5 else 3,
|
||||
onLoadMore = { rssSource?.let(viewModel::loadMore) }
|
||||
)
|
||||
LazyVerticalStaggeredGrid(
|
||||
@@ -244,13 +257,14 @@ fun RssArticlesPage(
|
||||
private fun LoadMoreDetector(
|
||||
state: LazyListState,
|
||||
enabled: Boolean,
|
||||
preloadThreshold: Int = 3,
|
||||
onLoadMore: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(state, enabled) {
|
||||
snapshotFlow {
|
||||
val info = state.layoutInfo
|
||||
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 ->
|
||||
if (shouldLoad) onLoadMore()
|
||||
}
|
||||
@@ -261,13 +275,14 @@ private fun LoadMoreDetector(
|
||||
private fun GridLoadMoreDetector(
|
||||
state: LazyGridState,
|
||||
enabled: Boolean,
|
||||
preloadThreshold: Int = 3,
|
||||
onLoadMore: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(state, enabled) {
|
||||
snapshotFlow {
|
||||
val info = state.layoutInfo
|
||||
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 ->
|
||||
if (shouldLoad) onLoadMore()
|
||||
}
|
||||
@@ -278,13 +293,14 @@ private fun GridLoadMoreDetector(
|
||||
private fun StaggeredLoadMoreDetector(
|
||||
state: LazyStaggeredGridState,
|
||||
enabled: Boolean,
|
||||
preloadThreshold: Int = 3,
|
||||
onLoadMore: () -> Unit
|
||||
) {
|
||||
LaunchedEffect(state, enabled) {
|
||||
snapshotFlow {
|
||||
val info = state.layoutInfo
|
||||
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 ->
|
||||
if (shouldLoad) onLoadMore()
|
||||
}
|
||||
@@ -340,11 +356,22 @@ private fun RssArticleItem(
|
||||
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(
|
||||
onClick = { onClick(article) },
|
||||
cornerRadius = 12.dp,
|
||||
containerColor = containerColor,
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
modifier = Modifier.fillMaxWidth().then(borderModifier)
|
||||
) {
|
||||
when (layout) {
|
||||
RssArticleLayout.List -> {
|
||||
|
||||
@@ -24,21 +24,24 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
||||
private var nextPageUrl: String? = null
|
||||
var sortName: String = ""
|
||||
var sortUrl: String = ""
|
||||
var searchKey: String? = null
|
||||
var page = 1
|
||||
|
||||
fun init(bundle: Bundle?) {
|
||||
bundle?.let {
|
||||
init(
|
||||
sortName = it.getString("sortName") ?: "",
|
||||
sortUrl = it.getString("sortUrl") ?: ""
|
||||
sortUrl = it.getString("sortUrl") ?: "",
|
||||
searchKey = it.getString("searchKey")
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun init(sortName: String, sortUrl: String) {
|
||||
if (this.sortName == sortName && this.sortUrl == sortUrl) return
|
||||
fun init(sortName: String, sortUrl: String, searchKey: String? = null) {
|
||||
if (this.sortName == sortName && this.sortUrl == sortUrl && this.searchKey == searchKey) return
|
||||
this.sortName = sortName
|
||||
this.sortUrl = sortUrl
|
||||
this.searchKey = searchKey
|
||||
page = 1
|
||||
nextPageUrl = null
|
||||
_loadState.value = RssArticlesLoadState()
|
||||
@@ -53,7 +56,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
||||
)
|
||||
page = 1
|
||||
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
|
||||
val articles = it.first
|
||||
articles.forEach { rssArticle ->
|
||||
@@ -97,7 +100,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application
|
||||
)
|
||||
|
||||
page++
|
||||
Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page).onSuccess(IO) {
|
||||
Rss.getArticles(viewModelScope, sortName, pageUrl, rssSource, page, searchKey).onSuccess(IO) {
|
||||
nextPageUrl = it.second
|
||||
loadMoreSuccess(it.first)
|
||||
}.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.RssSource
|
||||
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.book.search.SearchActivity
|
||||
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()))
|
||||
}
|
||||
|
||||
@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
|
||||
@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
|
||||
activity.lifecycleScope.launch(IO) {
|
||||
val source = getSource() ?: return@launch
|
||||
@@ -151,7 +161,7 @@ open class RssJsExtensions(activity: AppCompatActivity?, source: BaseSource?) :
|
||||
origin = sourceUrl,
|
||||
readTime = System.currentTimeMillis()
|
||||
)
|
||||
appDb.rssReadRecordDao.insertRecord(rssReadRecord) //留下历史记录
|
||||
appDb.rssReadRecordDao.insertRecord(rssReadRecord)
|
||||
withContext(Main) {
|
||||
activity.startActivity(
|
||||
MainActivity.createRssReadIntent(
|
||||
|
||||
@@ -1,16 +1,17 @@
|
||||
package io.legado.app.ui.rss.source.debug
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.widget.SearchView
|
||||
import android.view.View
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.activity.viewModels
|
||||
import androidx.lifecycle.lifecycleScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.databinding.ActivitySourceDebugBinding
|
||||
//import io.legado.app.lib.theme.accentColor
|
||||
//import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.help.source.sortUrls
|
||||
import io.legado.app.ui.widget.dialog.TextDialog
|
||||
import io.legado.app.utils.applyNavigationBarPadding
|
||||
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.visible
|
||||
import kotlinx.coroutines.launch
|
||||
import splitties.views.onClick
|
||||
import splitties.views.onLongClick
|
||||
|
||||
|
||||
class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSourceDebugModel>() {
|
||||
@@ -28,10 +31,12 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
||||
|
||||
private val adapter by lazy { RssSourceDebugAdapter(this) }
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
initRecyclerView()
|
||||
initSearchView()
|
||||
initHelpView()
|
||||
viewModel.observe { state, msg ->
|
||||
lifecycleScope.launch {
|
||||
adapter.addItem(msg)
|
||||
@@ -41,7 +46,7 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
||||
}
|
||||
}
|
||||
viewModel.initData(intent.getStringExtra("key")) {
|
||||
startDebug()
|
||||
// initData 完成后的回调
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,21 +64,72 @@ class RssSourceDebugActivity : VMBaseActivity<ActivitySourceDebugBinding, RssSou
|
||||
}
|
||||
|
||||
private fun initRecyclerView() {
|
||||
// binding.recyclerView.setEdgeEffectColor(primaryColor)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.recyclerView.applyNavigationBarPadding()
|
||||
// binding.rotateLoading.loadingColor = accentColor
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String?): Boolean {
|
||||
return false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
private fun startDebug() {
|
||||
@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()
|
||||
binding.help.gone() // 关闭调试选项面板
|
||||
viewModel.rssSource?.let {
|
||||
binding.rotateLoading.visible()
|
||||
viewModel.startDebug(it)
|
||||
viewModel.startDebug(it, key)
|
||||
} ?: 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) {
|
||||
when (state) {
|
||||
10 -> listSrc = msg
|
||||
|
||||
@@ -57,6 +57,7 @@ class RssSourceEditActivity :
|
||||
}
|
||||
private val adapter by lazy { RssSourceEditAdapter() }
|
||||
private val sourceEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val startEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val listEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val webViewEntities: ArrayList<EditEntity> = ArrayList()
|
||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||
@@ -156,6 +157,8 @@ class RssSourceEditActivity :
|
||||
ErrorCorrectionLevel.L
|
||||
)
|
||||
|
||||
R.id.menu_log -> showDialogFragment<io.legado.app.ui.about.AppLogDialog>()
|
||||
|
||||
R.id.menu_help -> showHelp("ruleHelp")
|
||||
}
|
||||
return super.onCompatOptionsItemSelected(item)
|
||||
@@ -165,6 +168,9 @@ class RssSourceEditActivity :
|
||||
binding.tabLayout.addTab(binding.tabLayout.newTab().apply {
|
||||
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 {
|
||||
setText(R.string.source_tab_list)
|
||||
})
|
||||
@@ -199,8 +205,9 @@ class RssSourceEditActivity :
|
||||
|
||||
private fun setEditEntities(tabPosition: Int?) {
|
||||
when (tabPosition) {
|
||||
1 -> adapter.editEntities = listEntities
|
||||
2 -> adapter.editEntities = webViewEntities
|
||||
1 -> adapter.editEntities = startEntities
|
||||
2 -> adapter.editEntities = listEntities
|
||||
3 -> adapter.editEntities = webViewEntities
|
||||
else -> adapter.editEntities = sourceEntities
|
||||
}
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
@@ -212,6 +219,21 @@ class RssSourceEditActivity :
|
||||
binding.cbIsEnable.isChecked = rs.enabled
|
||||
binding.cbSingleUrl.isChecked = rs.singleUrl
|
||||
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.apply {
|
||||
@@ -220,6 +242,7 @@ class RssSourceEditActivity :
|
||||
add(EditEntity("sourceIcon", rs.sourceIcon, R.string.source_icon))
|
||||
add(EditEntity("sourceGroup", rs.sourceGroup, R.string.source_group))
|
||||
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("loginUrl", rs.loginUrl, R.string.login_url))
|
||||
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("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.apply {
|
||||
add(EditEntity("ruleArticles", rs.ruleArticles, R.string.r_articles))
|
||||
@@ -258,6 +288,22 @@ class RssSourceEditActivity :
|
||||
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("style", rs.style, R.string.r_style))
|
||||
add(EditEntity("injectJs", rs.injectJs, R.string.r_inject_js))
|
||||
@@ -280,6 +326,9 @@ class RssSourceEditActivity :
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
source.preload = binding.cbIsEnablePreload.isChecked
|
||||
source.type = binding.spType.selectedItemPosition
|
||||
source.articleStyle = binding.lyType.selectedItemPosition
|
||||
sourceEntities.forEach {
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
@@ -295,10 +344,20 @@ class RssSourceEditActivity :
|
||||
"header" -> source.header = it.value
|
||||
"variableComment" -> source.variableComment = it.value
|
||||
"concurrentRate" -> source.concurrentRate = it.value
|
||||
"searchUrl" -> source.searchUrl = it.value
|
||||
"sortUrl" -> source.sortUrl = 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 {
|
||||
it.value = it.value?.takeIf { s -> s.isNotBlank() }
|
||||
when (it.key) {
|
||||
@@ -327,6 +386,8 @@ class RssSourceEditActivity :
|
||||
when (it.key) {
|
||||
"enableJs" -> source.enableJs = it.value.isTrue()
|
||||
"loadWithBaseUrl" -> source.loadWithBaseUrl = it.value.isTrue()
|
||||
"showWebLog" -> source.showWebLog = it.value.isTrue()
|
||||
"cacheFirst" -> source.cacheFirst = it.value.isTrue()
|
||||
"ruleContent" -> source.ruleContent =
|
||||
viewModel.ruleComplete(it.value, source.ruleArticles)
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@Composable
|
||||
fun AppBackground(
|
||||
@@ -29,10 +30,10 @@ fun AppBackground(
|
||||
AsyncImage(
|
||||
model = bgImagePath,
|
||||
contentDescription = null,
|
||||
imageLoader = koinInject(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.blur(blur.dp),
|
||||
//TODO:低版本安卓使用coil-transformations
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
}
|
||||
|
||||
@@ -7,17 +7,145 @@ import androidx.compose.material3.MaterialExpressiveTheme
|
||||
import androidx.compose.material3.MotionScheme
|
||||
import androidx.compose.material3.Shapes
|
||||
import androidx.compose.material3.Typography
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.compositeOver
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
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.theme.LegadoTheme
|
||||
import top.yukonga.miuix.kmp.theme.MiuixTheme
|
||||
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)
|
||||
@Composable
|
||||
@@ -35,12 +163,47 @@ fun AppTheme(
|
||||
val useMiuixMonet = ThemeConfig.useMiuixMonet
|
||||
val customPrimary = ThemeConfig.cPrimary
|
||||
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 miuixColorSchemeMode = remember(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 =
|
||||
remember(
|
||||
@@ -50,32 +213,53 @@ fun AppTheme(
|
||||
isPureBlack,
|
||||
customPrimary,
|
||||
customNightPrimary,
|
||||
enableDeepPersonalization,
|
||||
themeColor,
|
||||
secondaryThemeColor,
|
||||
primaryTextColor,
|
||||
secondaryTextColor,
|
||||
themeBackgroundColor,
|
||||
customLabelContainerColor,
|
||||
paletteStyleValue,
|
||||
materialVersion
|
||||
) {
|
||||
val customSeedColor = if (darkTheme) customNightPrimary else customPrimary
|
||||
ThemeEngine.getColorScheme(
|
||||
context = context,
|
||||
mode = appThemeMode,
|
||||
darkTheme = darkTheme,
|
||||
isAmoled = isPureBlack,
|
||||
paletteStyle = paletteStyleValue,
|
||||
materialVersion = materialVersion,
|
||||
customSeedColor = customSeedColor
|
||||
)
|
||||
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
|
||||
ThemeEngine.getColorScheme(
|
||||
context = context,
|
||||
mode = appThemeMode,
|
||||
darkTheme = darkTheme,
|
||||
isAmoled = isPureBlack,
|
||||
paletteStyle = paletteStyleValue,
|
||||
materialVersion = materialVersion,
|
||||
customSeedColor = customSeedColor
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
val customSeedColor = remember(
|
||||
darkTheme,
|
||||
customPrimary,
|
||||
customNightPrimary,
|
||||
colorScheme.primary
|
||||
) {
|
||||
val seed = if (darkTheme) customNightPrimary else customPrimary
|
||||
if (seed != 0) Color(seed) else colorScheme.primary
|
||||
}
|
||||
val themeSeedColor = remember(appThemeMode, customSeedColor, colorScheme.primary) {
|
||||
if (appThemeMode == AppThemeMode.Custom) customSeedColor else colorScheme.primary
|
||||
val themeSeedColor = remember(appThemeMode, colorScheme.primary) {
|
||||
if (appThemeMode == AppThemeMode.Custom) {
|
||||
val seed = if (darkTheme) customNightPrimary else customPrimary
|
||||
if (seed != 0) Color(seed) else colorScheme.primary
|
||||
} else {
|
||||
colorScheme.primary
|
||||
}
|
||||
}
|
||||
val miuixPaletteStyle = remember(paletteStyleValue) {
|
||||
ThemeResolver.resolveMiuixPaletteStyle(paletteStyleValue)
|
||||
@@ -111,7 +295,9 @@ fun AppTheme(
|
||||
themeColors.useDynamicColor &&
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
|
||||
) {
|
||||
colorResource(id = android.R.color.system_accent1_500)
|
||||
// colorResource 只能在 @Composable 上下文中调用
|
||||
// 这里使用一个固定颜色作为替代
|
||||
Color(0xFF6750A4)
|
||||
} else {
|
||||
themeSeedColor
|
||||
}
|
||||
@@ -142,13 +328,57 @@ fun AppTheme(
|
||||
|
||||
MiuixTheme(controller = controller) {
|
||||
val miuixStyles = MiuixTheme.textStyles
|
||||
val legadoTypography = remember(miuixStyles) {
|
||||
miuixStylesToM3Typography(miuixStyles).toLegadoTypography()
|
||||
val legadoTypography = remember<LegadoTypography>(miuixStyles, customFontFamily) {
|
||||
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 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(
|
||||
primary = miuixColorScheme.primary,
|
||||
onPrimary = miuixColorScheme.onPrimary,
|
||||
@@ -166,13 +396,13 @@ fun AppTheme(
|
||||
tertiaryContainer = miuixColorScheme.primaryContainer,
|
||||
onTertiaryContainer = miuixColorScheme.primaryVariant,
|
||||
|
||||
background = miuixColorScheme.background,
|
||||
onBackground = miuixColorScheme.onBackground,
|
||||
background = customBgColor,
|
||||
onBackground = customFontColor,
|
||||
|
||||
surface = miuixColorScheme.surface,
|
||||
onSurface = miuixColorScheme.onSurface,
|
||||
onSurface = customFontColor,
|
||||
surfaceVariant = miuixColorScheme.surfaceVariant,
|
||||
onSurfaceVariant = miuixColorScheme.onSurfaceSecondary,
|
||||
onSurfaceVariant = customFontColor,
|
||||
surfaceTint = miuixColorScheme.primary,
|
||||
inverseSurface = miuixColorScheme.onSurface,
|
||||
inverseOnSurface = miuixColorScheme.surface,
|
||||
@@ -191,8 +421,7 @@ fun AppTheme(
|
||||
surfaceContainer = miuixColorScheme.surfaceContainer,
|
||||
surfaceContainerHigh = miuixColorScheme.surfaceContainerHigh,
|
||||
surfaceContainerHighest = miuixColorScheme.surfaceContainerHighest,
|
||||
surfaceContainerLow = miuixColorScheme.secondaryContainer.copy(alpha = 0.32f)
|
||||
.compositeOver(miuixColorScheme.surface),
|
||||
surfaceContainerLow = miuixColorScheme.secondaryContainer.copy(alpha = 0.32f),
|
||||
surfaceContainerLowest = miuixColorScheme.background,
|
||||
|
||||
primaryFixed = miuixColorScheme.primaryContainer,
|
||||
@@ -208,10 +437,9 @@ fun AppTheme(
|
||||
onTertiaryFixed = miuixColorScheme.onTertiaryContainer,
|
||||
onTertiaryFixedVariant = miuixColorScheme.onTertiaryContainer,
|
||||
|
||||
cardContainer = miuixColorScheme.primaryContainer.copy(alpha = 0.32f)
|
||||
.compositeOver(miuixColorScheme.surfaceContainer),
|
||||
cardContainer = miuixColorScheme.disabledPrimary,
|
||||
onCardContainer = miuixColorScheme.primary,
|
||||
onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f),
|
||||
onSheetContent = miuixColorScheme.surface.copy(alpha = 0.5f)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -223,17 +451,86 @@ fun AppTheme(
|
||||
}
|
||||
}
|
||||
} 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(
|
||||
colorScheme = colorScheme,
|
||||
typography = materialTypography,
|
||||
motionScheme = MotionScheme.expressive(),
|
||||
shapes = Shapes()
|
||||
) {
|
||||
val legadoTypography = remember(materialTypography) {
|
||||
materialTypography.toLegadoTypography()
|
||||
val legadoTypography = remember(materialTypography, customFontFamily) {
|
||||
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(
|
||||
LocalLegadoTypography provides legadoTypography,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.ui.theme
|
||||
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import dev.chrisbanes.haze.HazeProgressive
|
||||
@@ -9,6 +10,9 @@ import dev.chrisbanes.haze.hazeSource
|
||||
import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
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
|
||||
@@ -27,14 +31,19 @@ fun Modifier.responsiveHazeEffect(
|
||||
): Modifier {
|
||||
val enableBlur = ThemeConfig.enableBlur
|
||||
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
|
||||
|
||||
val style = if (enableProgressiveBlur) {
|
||||
HazeLegado.ultraThin()
|
||||
} else {
|
||||
HazeLegado.regular()
|
||||
}
|
||||
val style = HazeLegado.custom(
|
||||
containerColor = containerColor,
|
||||
blurRadius = ThemeConfig.topBarBlurRadius,
|
||||
blurAlpha = ThemeConfig.topBarBlurAlpha
|
||||
)
|
||||
|
||||
return this.hazeEffect(
|
||||
state = state,
|
||||
@@ -57,10 +66,15 @@ fun Modifier.responsiveHazeEffectFixedStyle(
|
||||
state: HazeState
|
||||
): Modifier {
|
||||
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
|
||||
|
||||
val style = HazeLegado.ultraThinPlus()
|
||||
val style = HazeLegado.ultraThinPlus(containerColor = containerColor)
|
||||
|
||||
return this.hazeEffect(
|
||||
state = state,
|
||||
@@ -80,10 +94,17 @@ fun Modifier.responsiveHazeEffectFixedStyle(
|
||||
@OptIn(ExperimentalHazeMaterialsApi::class)
|
||||
@Composable
|
||||
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(
|
||||
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.ThemeController
|
||||
|
||||
fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
||||
fun ColorScheme.toLegadoColorScheme(
|
||||
customBgColor: Color = background,
|
||||
customFontColor: Color = onSurface,
|
||||
customTopBarColor: Color = surface,
|
||||
customNavBarColor: Color = surface
|
||||
): LegadoColorScheme {
|
||||
return LegadoColorScheme(
|
||||
primary = primary,
|
||||
onPrimary = onPrimary,
|
||||
@@ -32,10 +37,10 @@ fun ColorScheme.toLegadoColorScheme(): LegadoColorScheme {
|
||||
onTertiary = onTertiary,
|
||||
tertiaryContainer = tertiaryContainer,
|
||||
onTertiaryContainer = onTertiaryContainer,
|
||||
background = background,
|
||||
onBackground = onBackground,
|
||||
background = customBgColor,
|
||||
onBackground = customFontColor,
|
||||
surface = surface,
|
||||
onSurface = onSurface,
|
||||
onSurface = customFontColor,
|
||||
surfaceVariant = surfaceVariant,
|
||||
onSurfaceVariant = onSurfaceVariant,
|
||||
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
|
||||
fun ProvideColorSchemeOverride(
|
||||
colorScheme: ColorScheme,
|
||||
|
||||
@@ -33,20 +33,6 @@ fun miuixStylesToM3Typography(miuixStyles: TextStyles): Typography {
|
||||
labelLarge = miuixStyles.footnote1.copy(fontSize = 14.sp), // 14.sp
|
||||
labelMedium = miuixStyles.footnote1, // 13.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 {
|
||||
return LegadoTypography(
|
||||
headlineLarge = headlineLarge,
|
||||
headlineLargeEmphasized = headlineLargeEmphasized,
|
||||
headlineLargeEmphasized = headlineLarge.copy(fontWeight = FontWeight.Medium),
|
||||
headlineMedium = headlineMedium,
|
||||
headlineMediumEmphasized = headlineMediumEmphasized,
|
||||
headlineMediumEmphasized = headlineMedium.copy(fontWeight = FontWeight.Medium),
|
||||
headlineSmall = headlineSmall,
|
||||
headlineSmallEmphasized = headlineSmallEmphasized,
|
||||
headlineSmallEmphasized = headlineSmall.copy(fontWeight = FontWeight.Medium),
|
||||
titleLarge = titleLarge,
|
||||
titleLargeEmphasized = titleLargeEmphasized,
|
||||
titleLargeEmphasized = titleLarge.copy(fontWeight = FontWeight.Medium),
|
||||
titleMedium = titleMedium,
|
||||
titleMediumEmphasized = titleMediumEmphasized,
|
||||
titleMediumEmphasized = titleMedium.copy(fontWeight = FontWeight.Medium),
|
||||
titleSmall = titleSmall,
|
||||
titleSmallEmphasized = titleSmallEmphasized,
|
||||
titleSmallEmphasized = titleSmall.copy(fontWeight = FontWeight.Medium),
|
||||
bodyLarge = bodyLarge,
|
||||
bodyLargeEmphasized = bodyLargeEmphasized,
|
||||
bodyLargeEmphasized = bodyLarge.copy(fontWeight = FontWeight.Medium),
|
||||
bodyMedium = bodyMedium,
|
||||
bodyMediumEmphasized = bodyMediumEmphasized,
|
||||
bodyMediumEmphasized = bodyMedium.copy(fontWeight = FontWeight.Medium),
|
||||
bodySmall = bodySmall,
|
||||
bodySmallEmphasized = bodySmallEmphasized,
|
||||
bodySmallEmphasized = bodySmall.copy(fontWeight = FontWeight.Medium),
|
||||
labelLarge = labelLarge,
|
||||
labelLargeEmphasized = labelLargeEmphasized,
|
||||
labelLargeEmphasized = labelLarge.copy(fontWeight = FontWeight.Medium),
|
||||
labelMedium = labelMedium,
|
||||
labelMediumEmphasized = labelMediumEmphasized,
|
||||
labelMediumEmphasized = labelMedium.copy(fontWeight = FontWeight.Medium),
|
||||
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 dev.chrisbanes.haze.HazeStyle
|
||||
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.ThemeResolver
|
||||
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,
|
||||
): HazeStyle = hazeLegado(
|
||||
containerColor = containerColor,
|
||||
lightAlpha = 0.35f,
|
||||
darkAlpha = 0.55f,
|
||||
lightAlpha = ThemeConfig.topBarBlurAlpha / 100f * 0.35f / 0.73f,
|
||||
darkAlpha = ThemeConfig.topBarBlurAlpha / 100f * 0.55f / 0.8f,
|
||||
)
|
||||
|
||||
@Composable
|
||||
@@ -40,16 +41,30 @@ object HazeLegado {
|
||||
containerColor: Color = if (ThemeResolver.isMiuixEngine(composeEngine)) MiuixTheme.colorScheme.surface else MaterialTheme.colorScheme.surface,
|
||||
): HazeStyle = hazeLegado(
|
||||
containerColor = containerColor,
|
||||
lightAlpha = 0.73f,
|
||||
darkAlpha = 0.8f,
|
||||
lightAlpha = ThemeConfig.topBarBlurAlpha / 100f,
|
||||
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(
|
||||
containerColor: Color,
|
||||
blurRadius: Int = 24,
|
||||
lightAlpha: Float,
|
||||
darkAlpha: Float,
|
||||
): HazeStyle = HazeStyle(
|
||||
blurRadius = 24.dp,
|
||||
blurRadius = blurRadius.dp,
|
||||
backgroundColor = containerColor,
|
||||
tint = HazeTint(
|
||||
containerColor.copy(alpha = if (containerColor.luminance() >= 0.5) lightAlpha else darkAlpha),
|
||||
|
||||
@@ -51,7 +51,7 @@ class SimpleCounterView @JvmOverloads constructor(
|
||||
var max: Int
|
||||
get() = _max
|
||||
set(value) {
|
||||
_max = value
|
||||
_max = value.coerceAtLeast(1)
|
||||
_progress = _progress.coerceAtMost(_max)
|
||||
updateValue()
|
||||
}
|
||||
|
||||
@@ -1,18 +1,34 @@
|
||||
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.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.NavigationBar
|
||||
import androidx.compose.material3.NavigationBarItem
|
||||
import androidx.compose.material3.NavigationBarItemDefaults
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.material3.ripple
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
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.graphics.vector.ImageVector
|
||||
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.LocalHazeState
|
||||
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.theme.MiuixTheme
|
||||
import top.yukonga.miuix.kmp.basic.NavigationBar as MiuixNavigationBar
|
||||
@@ -26,29 +42,43 @@ fun AppNavigationBar(
|
||||
) {
|
||||
val isMiuix = ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)
|
||||
val opacity = (ThemeConfig.bottomBarOpacity.coerceIn(0, 100)) / 100f
|
||||
val hazeState = LocalHazeState.current
|
||||
val hazeModifier = if (hazeState != null) {
|
||||
Modifier.regularHazeEffect(hazeState)
|
||||
} else {
|
||||
Modifier
|
||||
}
|
||||
|
||||
if (isMiuix) {
|
||||
val baseColor = GlassDefaults.glassColor(
|
||||
noBlurColor = MiuixTheme.colorScheme.surface,
|
||||
blurAlpha = GlassDefaults.DefaultBlurAlpha
|
||||
)
|
||||
val baseColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||
Color(ThemeConfig.secondaryThemeColor)
|
||||
} else {
|
||||
GlassDefaults.glassColor(
|
||||
noBlurColor = MiuixTheme.colorScheme.surface,
|
||||
blurAlpha = GlassDefaults.TransparentAlpha
|
||||
)
|
||||
}
|
||||
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
||||
|
||||
MiuixNavigationBar(
|
||||
modifier = modifier,
|
||||
modifier = modifier.then(hazeModifier),
|
||||
color = finalColor,
|
||||
mode = miuixMode,
|
||||
content = content
|
||||
)
|
||||
} else {
|
||||
val baseColor = GlassDefaults.glassColor(
|
||||
noBlurColor = BottomAppBarDefaults.containerColor,
|
||||
blurAlpha = GlassDefaults.DefaultBlurAlpha
|
||||
)
|
||||
val baseColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.secondaryThemeColor != 0) {
|
||||
Color(ThemeConfig.secondaryThemeColor)
|
||||
} else {
|
||||
GlassDefaults.glassColor(
|
||||
noBlurColor = BottomAppBarDefaults.containerColor,
|
||||
blurAlpha = GlassDefaults.TransparentAlpha
|
||||
)
|
||||
}
|
||||
val finalColor = baseColor.copy(alpha = (baseColor.alpha * opacity).coerceIn(0f, 1f))
|
||||
|
||||
NavigationBar(
|
||||
modifier = modifier,
|
||||
modifier = modifier.then(hazeModifier),
|
||||
containerColor = finalColor,
|
||||
content = content
|
||||
)
|
||||
@@ -66,10 +96,35 @@ fun RowScope.AppNavigationBarItem(
|
||||
m3IndicatorColor: Color,
|
||||
m3ShowLabel: Boolean,
|
||||
m3AlwaysShowLabel: Boolean,
|
||||
useCustomIcon: Boolean = false,
|
||||
) {
|
||||
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(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
|
||||
@@ -15,9 +15,14 @@ import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.blur
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.hazeSource
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import io.legado.app.ui.theme.LocalHazeState
|
||||
@@ -70,65 +75,110 @@ fun AppScaffold(
|
||||
FabPosition.Center -> MiuixFabPosition.Center
|
||||
else -> MiuixFabPosition.End
|
||||
}
|
||||
MiuixScaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
topBar(hazeState)
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = miuixFabPosition,
|
||||
containerColor = miuixContainerColor,
|
||||
contentWindowInsets = contentWindowInsets
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
.then(
|
||||
if (contentDrawsBehindBars) Modifier
|
||||
else Modifier.padding(paddingValues)
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
BackgroundImageContent(isDark = isDark, hazeState = hazeState)
|
||||
MiuixScaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
topBar(hazeState)
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = miuixFabPosition,
|
||||
containerColor = miuixContainerColor,
|
||||
contentWindowInsets = contentWindowInsets
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
.then(
|
||||
if (contentDrawsBehindBars) Modifier
|
||||
else Modifier.padding(paddingValues)
|
||||
)
|
||||
) {
|
||||
content(
|
||||
if (contentDrawsBehindBars) paddingValues
|
||||
else PaddingValues(0.dp)
|
||||
)
|
||||
) {
|
||||
content(
|
||||
if (contentDrawsBehindBars) paddingValues
|
||||
else PaddingValues(0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
else -> {
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
topBar(hazeState)
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = contentWindowInsets
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
.then(
|
||||
if (contentDrawsBehindBars) Modifier
|
||||
else Modifier.padding(paddingValues)
|
||||
Box(modifier = modifier.fillMaxSize()) {
|
||||
BackgroundImageContent(isDark = isDark, hazeState = hazeState)
|
||||
Scaffold(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
topBar = {
|
||||
topBar(hazeState)
|
||||
},
|
||||
bottomBar = bottomBar,
|
||||
snackbarHost = snackbarHost,
|
||||
floatingActionButton = floatingActionButton,
|
||||
floatingActionButtonPosition = floatingActionButtonPosition,
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor,
|
||||
contentWindowInsets = contentWindowInsets
|
||||
) { paddingValues ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.responsiveHazeSource(hazeState)
|
||||
.then(
|
||||
if (contentDrawsBehindBars) Modifier
|
||||
else Modifier.padding(paddingValues)
|
||||
)
|
||||
) {
|
||||
content(
|
||||
if (contentDrawsBehindBars) paddingValues
|
||||
else PaddingValues(0.dp)
|
||||
)
|
||||
) {
|
||||
content(
|
||||
if (contentDrawsBehindBars) paddingValues
|
||||
else PaddingValues(0.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@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 io.legado.app.ui.animation.DampedDragAnimation
|
||||
import io.legado.app.ui.animation.InteractiveHighlight
|
||||
import io.legado.app.ui.config.themeConfig.ThemeConfig
|
||||
import io.legado.app.ui.theme.LegadoTheme
|
||||
import kotlinx.coroutines.flow.collectLatest
|
||||
import kotlinx.coroutines.launch
|
||||
@@ -110,12 +111,19 @@ fun FloatingBottomBar(
|
||||
backdrop: Backdrop,
|
||||
tabsCount: Int,
|
||||
isBlurEnabled: Boolean = true,
|
||||
hasCustomIcons: Boolean = false,
|
||||
content: @Composable RowScope.() -> Unit
|
||||
) {
|
||||
val isInLightTheme = !LegadoTheme.isDark
|
||||
val accentColor = LegadoTheme.colorScheme.primary
|
||||
val containerColor = if (isBlurEnabled) {
|
||||
LegadoTheme.colorScheme.surfaceContainer.copy(alpha = 0.4f)
|
||||
val accentColor = if (ThemeConfig.enableDeepPersonalization && ThemeConfig.themeColor != 0) {
|
||||
Color(ThemeConfig.themeColor)
|
||||
} 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 {
|
||||
LegadoTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
@@ -250,8 +258,8 @@ fun FloatingBottomBar(
|
||||
effects = {
|
||||
if (isBlurEnabled) {
|
||||
vibrancy()
|
||||
blur(8f.dp.toPx())
|
||||
lens(24f.dp.toPx(), 24f.dp.toPx())
|
||||
blur(ThemeConfig.bottomBarBlurRadius.toFloat().dp.toPx())
|
||||
lens(ThemeConfig.bottomBarLensRadius.dp.toPx(), ThemeConfig.bottomBarLensRadius.dp.toPx())
|
||||
}
|
||||
},
|
||||
highlight = {
|
||||
@@ -307,8 +315,8 @@ fun FloatingBottomBar(
|
||||
if (isBlurEnabled) {
|
||||
val progress = dampedDragAnimation.pressProgress
|
||||
vibrancy()
|
||||
blur(8f.dp.toPx())
|
||||
lens(24f.dp.toPx() * progress, 24f.dp.toPx() * progress)
|
||||
blur(ThemeConfig.bottomBarBlurRadius.toFloat().dp.toPx())
|
||||
lens(ThemeConfig.bottomBarLensRadius.dp.toPx() * progress, ThemeConfig.bottomBarLensRadius.dp.toPx() * progress)
|
||||
}
|
||||
},
|
||||
highlight = {
|
||||
@@ -331,7 +339,10 @@ fun FloatingBottomBar(
|
||||
)
|
||||
.height(56.dp)
|
||||
.padding(horizontal = 4.dp)
|
||||
.graphicsLayer(colorFilter = ColorFilter.tint(accentColor)),
|
||||
.then(
|
||||
if (hasCustomIcons) Modifier
|
||||
else Modifier.graphicsLayer(colorFilter = ColorFilter.tint(accentColor))
|
||||
),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
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 ThickBlurAlpha = 0.72f
|
||||
val TransparentAlpha = 0f
|
||||
|
||||
@@ -1,35 +1,67 @@
|
||||
package io.legado.app.ui.widget.components
|
||||
|
||||
import androidx.compose.animation.animateContentSize
|
||||
import androidx.compose.foundation.border
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
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.draw.clip
|
||||
import androidx.compose.ui.graphics.Color
|
||||
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.ThemeResolver
|
||||
import io.legado.app.ui.widget.components.divider.SettingItemDivider
|
||||
import io.legado.app.ui.widget.components.title.AdaptiveTitle
|
||||
import top.yukonga.miuix.kmp.basic.Card as MiuixCard
|
||||
|
||||
/**
|
||||
* Settings Group Container by https://github.com/wxxsfxyzm/InstallerX-Revived
|
||||
* Edit by @Kudomaga
|
||||
* @param title the title of the group
|
||||
* @param content a list of composable that will be displayed in the group
|
||||
* @param modifier Modifier
|
||||
*/
|
||||
val LocalSplicedColumnGroupState = compositionLocalOf { SplicedColumnGroupState(
|
||||
enableItemDivider = false,
|
||||
currentIndex = { 0 },
|
||||
incrementIndex = { }
|
||||
) }
|
||||
|
||||
data class SplicedColumnGroupState(
|
||||
val enableItemDivider: Boolean,
|
||||
val currentIndex: () -> Int,
|
||||
val incrementIndex: () -> Unit
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun SplicedColumnGroup(
|
||||
modifier: Modifier = Modifier,
|
||||
title: String = "",
|
||||
content: @Composable ColumnScope.() -> Unit,
|
||||
items: @Composable ColumnScope.() -> Unit,
|
||||
) {
|
||||
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)) {
|
||||
if (title.isNotEmpty()) {
|
||||
AdaptiveTitle(
|
||||
@@ -38,28 +70,70 @@ fun SplicedColumnGroup(
|
||||
)
|
||||
}
|
||||
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixCard {
|
||||
CompositionLocalProvider(LocalSplicedColumnGroupState provides groupState) {
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
MiuixCard {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(16.dp))
|
||||
) {
|
||||
currentIndex.intValue = 0
|
||||
items()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(16.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()
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.animateContentSize()
|
||||
.clip(RoundedCornerShape(16.dp)),
|
||||
verticalArrangement = Arrangement.spacedBy(2.dp)
|
||||
) {
|
||||
content()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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.combinedClickable
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ColumnScope
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
@@ -40,7 +41,26 @@ private fun BaseCard(
|
||||
) {
|
||||
val resolvedContainerColor = (containerColor ?: LegadoTheme.colorScheme.secondaryContainer)
|
||||
.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(
|
||||
color = resolvedContainerColor,
|
||||
contentColor = contentColor ?: LegadoTheme.colorScheme.onSurface
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package io.legado.app.ui.widget.components.card
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -35,7 +36,8 @@ fun TextCard(
|
||||
verticalPadding: Dp = 4.dp,
|
||||
iconSize: Dp = 14.dp,
|
||||
spacing: Dp = 4.dp,
|
||||
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized
|
||||
textStyle: TextStyle = LegadoTheme.typography.labelSmallEmphasized,
|
||||
border: BorderStroke? = null
|
||||
) {
|
||||
val defaultBackground = LegadoTheme.colorScheme.surfaceContainer
|
||||
val defaultContent = LegadoTheme.colorScheme.onSurface
|
||||
@@ -48,7 +50,8 @@ fun TextCard(
|
||||
cornerRadius = cornerRadius,
|
||||
containerColor = finalBackgroundColor,
|
||||
contentColor = finalContentColor,
|
||||
onClick = onClick
|
||||
onClick = onClick,
|
||||
border = border
|
||||
) {
|
||||
Row(
|
||||
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
|
||||
|
||||
import androidx.compose.foundation.BorderStroke
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
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 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.widget.components.card.GlassCard
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
@@ -38,13 +40,24 @@ fun ExploreKindItem(
|
||||
|
||||
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) {
|
||||
GlassCard(
|
||||
onClick = onClick,
|
||||
cornerRadius = cornerRadius,
|
||||
containerColor = backgroundColor,
|
||||
contentColor = LegadoTheme.colorScheme.onSurface,
|
||||
modifier = modifier
|
||||
modifier = modifier,
|
||||
border = if (enableBorder) {
|
||||
BorderStroke(borderWidth, borderColor)
|
||||
} else null
|
||||
) {
|
||||
KindText(
|
||||
text = displayText,
|
||||
@@ -58,7 +71,9 @@ fun ExploreKindItem(
|
||||
containerColor = backgroundColor,
|
||||
contentColor = LegadoTheme.colorScheme.primary,
|
||||
modifier = modifier,
|
||||
border = CardDefaults.outlinedCardBorder()
|
||||
border = if (enableBorder) {
|
||||
BorderStroke(borderWidth, borderColor)
|
||||
} else BorderStroke(1.dp, LegadoTheme.colorScheme.outline)
|
||||
) {
|
||||
KindText(
|
||||
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.ui.widget.components.explore.ExploreKindUiUseCase
|
||||
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.RoundDropdownMenuItem
|
||||
import io.legado.app.ui.widget.components.text.AppText
|
||||
@@ -321,7 +322,11 @@ private fun ExploreKindCompactTextField(
|
||||
val interactionSource = remember { MutableInteractionSource() }
|
||||
val isFocused by interactionSource.collectIsFocusedAsState()
|
||||
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
|
||||
} else {
|
||||
Color.Transparent
|
||||
@@ -338,7 +343,7 @@ private fun ExploreKindCompactTextField(
|
||||
.height(34.dp)
|
||||
.clip(shape)
|
||||
.background(backgroundColor)
|
||||
.border(width = 1.dp, color = borderColor, shape = shape),
|
||||
.border(width = borderWidth, color = borderColor, shape = shape),
|
||||
decorationBox = { innerTextField ->
|
||||
Box(
|
||||
modifier = Modifier
|
||||
|
||||
@@ -96,7 +96,7 @@ object AppIcons {
|
||||
|
||||
val Replay: ImageVector
|
||||
@Composable
|
||||
get() = if (isMiuix) MiuixIcons.Refresh ?: Icons.Default.Replay else Icons.Default.Replay
|
||||
get() = if (isMiuix) MiuixIcons.Refresh else Icons.Default.Replay
|
||||
|
||||
@Composable
|
||||
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 io.legado.app.ui.theme.LegadoTheme
|
||||
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.preference.ArrowPreference
|
||||
|
||||
@@ -24,6 +25,8 @@ fun ClickableSettingItem(
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val composeEngine = LegadoTheme.composeEngine
|
||||
SplicedColumnDivider()
|
||||
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
ArrowPreference(
|
||||
title = title,
|
||||
|
||||
+38
@@ -7,6 +7,7 @@ import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Check
|
||||
import androidx.compose.material.icons.filled.ChevronRight
|
||||
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
|
||||
import androidx.compose.material3.Icon
|
||||
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.menuItem.RoundDropdownMenuItem
|
||||
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.WindowDropdownPreference
|
||||
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.ThemeResolver
|
||||
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 top.yukonga.miuix.kmp.basic.BasicComponent
|
||||
import top.yukonga.miuix.kmp.basic.TextField as MiuixTextField
|
||||
@@ -46,6 +47,7 @@ fun InputSettingItem(
|
||||
}
|
||||
|
||||
val composeEngine = LegadoTheme.composeEngine
|
||||
SplicedColumnDivider()
|
||||
|
||||
if (ThemeResolver.isMiuixEngine(composeEngine)) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user