diff --git a/.gitignore b/.gitignore index 2543443bb..700c700fe 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,7 @@ /release /tmp node_modules/ +/app/app +/app/google +/app/gradle.properties package-lock.json diff --git a/app/build.gradle b/app/build.gradle index 7dadb803b..75fdfb620 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -2,7 +2,7 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-kapt' -apply plugin: "de.timfreiheit.resourceplaceholders" +apply plugin: 'de.timfreiheit.resourceplaceholders' apply plugin: 'io.fabric' androidExtensions { @@ -19,6 +19,7 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec android { compileSdkVersion 29 + flavorDimensions("version") signingConfigs { if (project.hasProperty("RELEASE_STORE_FILE")) { myConfig { @@ -37,7 +38,6 @@ android { targetSdkVersion 29 versionCode gitCommits versionName version - flavorDimensions "versionCode" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" project.ext.set("archivesBaseName", name + "_" + version) multiDexEnabled true @@ -74,13 +74,15 @@ android { } } } - productFlavors{ - app{ - manifestPlaceholders = [APP_CHANNEL_VALUE:"app"] + productFlavors { + app { + dimension "version" + manifestPlaceholders = [APP_CHANNEL_VALUE: "app"] } - google{ + google { + dimension "version" applicationId "io.legado.play" - manifestPlaceholders = [APP_CHANNEL_VALUE:"google"] + manifestPlaceholders = [APP_CHANNEL_VALUE: "google"] } } compileOptions { @@ -108,27 +110,27 @@ kapt { dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - testImplementation 'junit:junit:4.12' + testImplementation 'junit:junit:4.13' androidTestImplementation 'androidx.test:runner:1.2.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' //kotlin implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" //fireBase - implementation 'com.google.firebase:firebase-core:17.2.3' + implementation 'com.google.firebase:firebase-core:17.4.0' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' //androidX implementation 'androidx.core:core-ktx:1.2.0' implementation 'androidx.appcompat:appcompat:1.1.0' implementation 'androidx.media:media:1.1.0' - implementation 'androidx.preference:preference:1.1.0' + implementation 'androidx.preference:preference:1.1.1' implementation 'androidx.constraintlayout:constraintlayout:1.1.3' implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.0.0' implementation 'androidx.viewpager2:viewpager2:1.0.0' implementation 'com.google.android.material:material:1.1.0' implementation 'com.google.android:flexbox:1.1.0' - implementation 'com.google.code.gson:gson:2.8.5' + implementation 'com.google.code.gson:gson:2.8.6' //lifecycle def lifecycle_version = '2.2.0' @@ -157,19 +159,19 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" //规则相关 - implementation 'org.jsoup:jsoup:1.12.1' + implementation 'org.jsoup:jsoup:1.13.1' implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' implementation 'com.jayway.jsonpath:json-path:2.4.0' //JS rhino implementation 'com.github.gedoor:rhino-android:1.4' - //Retrofit - implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' - implementation 'com.squareup.retrofit2:retrofit:2.6.1' + //网络 + //noinspection GradleDependency + implementation 'com.squareup.retrofit2:retrofit:2.7.2' //Glide - implementation 'com.github.bumptech.glide:glide:4.9.0' + implementation 'com.github.bumptech.glide:glide:4.11.0' //webServer implementation 'org.nanohttpd:nanohttpd:2.3.1' @@ -182,15 +184,21 @@ dependencies { implementation 'com.jaredrummler:colorpicker:1.1.0' //apache - implementation 'org.apache.commons:commons-lang3:3.9' + implementation 'org.apache.commons:commons-lang3:3.10' implementation 'org.apache.commons:commons-text:1.8' //MarkDown - implementation 'ru.noties.markwon:core:3.0.2' + implementation 'ru.noties.markwon:core:3.1.0' //转换繁体 - implementation 'com.github.houbb:opencc4j:1.4.0' + implementation 'com.hankcs:hanlp:portable-1.7.7' } -apply plugin: 'com.google.gms.google-services' \ No newline at end of file +apply plugin: 'com.google.gms.google-services' + +afterEvaluate { + for (Task task : project.tasks.matching { it.name.startsWith('crashlyticsUploadDeobs') }) { + task.enabled = false + } +} \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9af1203b9..1e8ab731b 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -160,7 +160,6 @@ -dontwarn rx.** -dontwarn okio.** --dontwarn retrofit2.** -dontwarn javax.annotation.** -dontwarn org.apache.log4j.lf5.viewer.** -dontnote org.apache.log4j.lf5.viewer.** @@ -172,7 +171,6 @@ -dontwarn com.jeremyliao.liveeventbus.** -keep class com.jeremyliao.liveeventbus.** { *; } --keep class retrofit2.**{*;} -keep class okhttp3.**{*;} -keep class okio.**{*;} -keep class com.hwangjr.rxbus.**{*;} diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml index b91572ada..e4464c88d 100644 --- a/app/src/debug/res/values/strings.xml +++ b/app/src/debug/res/values/strings.xml @@ -1,4 +1,4 @@ - 阅读.debug - 阅读.debug·搜索 + 阅读·D + 阅读·D·搜索 \ No newline at end of file diff --git a/app/src/google/res/values-zh-rHK/strings.xml b/app/src/google/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000..daa6a610a --- /dev/null +++ b/app/src/google/res/values-zh-rHK/strings.xml @@ -0,0 +1,6 @@ + + + + 閱讀Pro + + \ No newline at end of file diff --git a/app/src/google/res/values/strings.xml b/app/src/google/res/values/strings.xml new file mode 100644 index 000000000..ed2d735c4 --- /dev/null +++ b/app/src/google/res/values/strings.xml @@ -0,0 +1,6 @@ + + + + 阅读Pro + + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index cfb6793b8..b33892324 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -257,7 +257,7 @@ android:launchMode="singleTop" /> + diff --git a/app/src/main/assets/txtTocRule.json b/app/src/main/assets/txtTocRule.json index ececb6800..aa5408b4c 100644 --- a/app/src/main/assets/txtTocRule.json +++ b/app/src/main/assets/txtTocRule.json @@ -1,98 +1,121 @@ [ { + "id": -1, "enable": true, "name": "目录", - "rule": "^[  \\t]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", - "serialNumber": 0 + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", + "serialNumber": -17 }, { + "id": -2, "enable": false, "name": "目录(去空白)", - "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", - "serialNumber": 1 + "rule": "(?<=[ \\s])(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", + "serialNumber": -16 }, { + "id": -3, "enable": false, - "name": "目录(去简介)", - "rule": "(?<=[ \\s])(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", - "serialNumber": 2 + "name": "目录(匹配简介)", + "rule": "(?<=[ \\s])(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "serialNumber": -15 }, { + "id": -4, "enable": false, "name": "目录(古典、轻小说备用)", - "rule": "^[  \\t]{0,4}(?:前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", - "serialNumber": 3 + "rule": "^[  \\t]{0,4}(?:序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|回(?![合来事去])|场(?![和合比电是])|篇(?!张))).{0,30}$", + "serialNumber": -14 }, { + "id": -5, "enable": false, "name": "数字(纯数字标题)", "rule": "(?<=[ \\s])\\d+[  \\t]{0,4}$", - "serialNumber": 4 + "serialNumber": -13 }, { + "id": -6, "enable": true, "name": "数字 分隔符 标题名称", - "rule": "^[  \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", - "serialNumber": 5 + "rule": "^[  \\t]{0,4}\\d{1,5}[,., 、_—\\-].{1,30}$", + "serialNumber": -12 }, { + "id": -7, + "enable": true, + "name": "大写数字 分隔符 标题名称", + "rule": "^[  \\t]{0,4}[零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}[ 、_—\\-].{1,30}$", + "serialNumber": -11 + }, + { + "id": -8, "enable": true, "name": "正文 标题/序号", "rule": "^[  \\t]{0,4}正文[  ]{1,4}.{0,20}$", - "serialNumber": 6 + "serialNumber": -10 }, { + "id": -9, "enable": true, "name": "Chapter/Section/Part/Episode 序号 标题", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)\\s{0,4}\\d{1,4}.{0,30}$", - "serialNumber": 7 + "serialNumber": -9 }, { + "id": -10, "enable": false, "name": "Chapter(去简介)", "rule": "^[  \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art|PART|[Ee]pisode)\\s{0,4}\\d{1,4}.{0,30}$", - "serialNumber": 8 + "serialNumber": -8 }, { + "id": -11, "enable": true, "name": "特殊符号 序号 标题", - "rule": "(?<=[\\s ]{0,4}).{1,3}(?:第|卷|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节]?[\\.:: \f\t].{0,20}$", - "serialNumber": 9 + "rule": "(?<=[\\s ])[【〔〖「『〈[\\[](?:第|[Cc]hapter)[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节].{0,20}$", + "serialNumber": -7 }, { + "id": -12, "enable": false, "name": "特殊符号 标题(成对)", "rule": "(?<=[\\s ]{0,4})(?:[\\[〈「『〖〔《(【\\(].{1,30}[\\)】)》〕〗』」〉\\]]?|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", - "serialNumber": 10 + "serialNumber": -6 }, { + "id": -13, "enable":true, "name": "特殊符号 标题(单个)", "rule": "(?<=[\\s ]{0,4})(?:[☆★✦✧].{1,30}|(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外)[  ]{0,4}$", - "serialNumber": 11 + "serialNumber": -5 }, { + "id": -14, "enable": true, "name": "章/卷 序号 标题", "rule": "^[ \\t ]{0,4}(?:(?:内容|文章)?简介|文案|前言|序章|楔子|正文(?!完|结)|终章|后记|尾声|番外|[卷章][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8})[  ]{0,4}.{0,30}$", - "serialNumber": 12 + "serialNumber": -4 }, { + "id": -15, "enable":false, "name": "顶格标题", "rule": "^\\S.{1,20}$", - "serialNumber": 13 + "serialNumber": -3 }, { + "id": -16, "enable":false, "name": "双标题(前向)", "rule": "(?m)(?<=[ \\t ]{0,4})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$(?=[\\s ]{0,8}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章)", - "serialNumber": 14 + "serialNumber": -2 }, { + "id": -17, "enable":false, "name": "双标题(后向)", "rule": "(?m)(?<=[ \\t ]{0,4}第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$[\\s ]{0,8})第[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,8}章.{0,30}$", - "serialNumber": 15 + "serialNumber": -1 } ] diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 394bc51f9..34f05518c 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -1,7 +1,101 @@ ## 更新日志 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 -* 请关注[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 -* 弄了个企业公众号[开源阅读](),后面弄好后会把原来的[开源阅读软件]()迁移过来 +* 请关注公众号[开源阅读]()支持我,同时关注合作公众号[小说拾遗](),阅读公众号小编。 +* 新公众号[开源阅读]()已启用,[开源阅读软件]()备用 + +**2020/05/04** +* 优化txt文件目录解析 + +**2020/05/03** +* 优化一些界面显示问题 +* 订阅源添加style +* 修复一些重复目录的bug + +**2020/05/02** +* 修复不停换源的bug +* 修复本地书籍自动换源 +* 修复书源校验的一些问题 + +**2020/05/01** +* 尝试修复朗读时可能错位的bug +* 添加自动换源配置 +* 换源添加禁用菜单 + +**2020/04/29** +* 修复bug +* 订阅界面添加长按菜单 + +**2020/04/26** +* 添加导入旧的书源转换 +* 修复不自动朗读下一章的bug + +**2020/04/25** +* 修复翻页按键设置为空时崩溃的bug +* 翻页按键优先自定义按键,可覆盖音量按键 +* 写书源时的辅助键盘添加※ +* 更改了书源格式,不再需要转义符 + +**2020/04/24** +* 坚果云最近调整了策略,必须使用应用密码才能备份,用户信息,安全,第三方应用 +* text目录规则添加id字段,负值为系统自带规则 +* 其它一些优化 + +**2020/04/20** +* 优化阅读界面信息显示 + +**2020/04/19** +* 添加阅读界面各种信息设置 + +**2020/04/18** +* feat: 中文简繁处理库换成 HanLP, 中文增加 zh-rHK 翻译, hingbong +* 修复更新时间不对的bug + +**2020/04/13** +* 去除rss朗读时的引号 + +**2020/04/13** +* 修复调用webView返回结果多了引号的bug + +**2020/04/12** +* 解决无法取消加粗的bug +* 修复换源自动加入书架的bug + +**2020/04/09** +* 修复书架刷新闪烁 + +**2020/04/08** +* 可以隐藏书架未分组 + +**2020/04/07** +* 书架添加未分组,有未分组书籍时自动显示 +* 其它一些优化 + +**2020/04/04** +* 优化备份逻辑 +* 修复订阅分类太多显示不全的bug +* 修复一些分类要手动刷新的问题 + +**2020/04/02** +* 书架书名和作者作为唯一值 +* 添加订阅分类,分类规则和发现一样,分类一::url1 && 分类2::url2 + +**2020/03/29** +* 添加退出软件后是否响应耳机按键的开关 +* 优化书源校验 + +**2020/03/26** +* 修复txt目录bug +* 最近工作比较忙,只有晚上有时间写软件,bug之类的不要催,白天不回消息 + +**2020/03/25** +* 修复7.1.1的网络问题,是retrofit2库最新版本的bug,暂时退回上版本 +* 去除下载路径的配置,减少错误 +* 添加隐藏状态栏是否扩展到刘海 + +**2020/03/24** +* txt文件第一章之前的文字不再放到简介里 +* 优化txt目录识别,章节超过3万字判断为目录识别错误重新识别 +* 修复文件关联 by wqfantexi **2020/03/22** * 添加文件关联 by wqfantexi diff --git a/app/src/main/assets/web/book.html b/app/src/main/assets/web/book.html deleted file mode 100644 index bc7ea6c90..000000000 --- a/app/src/main/assets/web/book.html +++ /dev/null @@ -1,39 +0,0 @@ - - - - - - 阅读3.0书架 - - - - - - - - - - -
-
-
-
-
-
-
-
-
-
-
-
- - - - \ No newline at end of file diff --git a/app/src/main/assets/web/bookshelf.html b/app/src/main/assets/web/bookshelf.html index 87ee3bf9b..86639426f 100644 --- a/app/src/main/assets/web/bookshelf.html +++ b/app/src/main/assets/web/bookshelf.html @@ -1,46 +1,39 @@ - - + + - - - - - yd-web-tool - - - - - - - - - - - - - - - - - - - - - - + + 阅读3.0书架 + + - - -
- - + + + + + + +
+
+
+
+
+
+
+
+
+
+
+
+ \ No newline at end of file diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html index a0a00ef1c..4339538d9 100644 --- a/app/src/main/assets/web/index.html +++ b/app/src/main/assets/web/index.html @@ -359,8 +359,8 @@
(?i) 前缀表示忽略大小写 代码在线运行工具 - 阅读书架(经典) - 阅读书架(新潮) + 阅读书架(经典) + 阅读书架(新潮) diff --git a/app/src/main/assets/web/index.js b/app/src/main/assets/web/index.js index fb2298bbd..7b0ec52a3 100644 --- a/app/src/main/assets/web/index.js +++ b/app/src/main/assets/web/index.js @@ -36,23 +36,28 @@ const RuleJSON = (() => { // 搜索规则 $$('.rules .ruleSearch').forEach(item => searchJson[item.title] = ''); - ruleJson.ruleSearch = JSON.stringify(searchJson); + //ruleJson.ruleSearch = JSON.stringify(searchJson); + ruleJson.ruleSearch = searchJson; // 发现规则 $$('.rules .ruleExplore').forEach(item => exploreJson[item.title] = ''); - ruleJson.ruleExplore = JSON.stringify(exploreJson); + //ruleJson.ruleExplore = JSON.stringify(exploreJson); + ruleJson.ruleExplore = exploreJson; // 详情页规则 $$('.rules .ruleBookInfo').forEach(item => bookInfoJson[item.title] = ''); - ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson); + //ruleJson.ruleBookInfo = JSON.stringify(bookInfoJson); + ruleJson.ruleBookInfo = bookInfoJson; // 目录规则 $$('.rules .ruleToc').forEach(item => tocJson[item.title] = ''); - ruleJson.ruleToc = JSON.stringify(tocJson); + //ruleJson.ruleToc = JSON.stringify(tocJson); + ruleJson.ruleToc = tocJson; // 正文规则 $$('.rules .ruleContent').forEach(item => contentJson[item.title] = ''); - ruleJson.ruleContent = JSON.stringify(contentJson); + //ruleJson.ruleContent = JSON.stringify(contentJson); + ruleJson.ruleContent = contentJson; return ruleJson; })(); @@ -110,38 +115,48 @@ function rule2json() { // 转换搜索规则 let searchJson = {}; - Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => { + //Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => { + Object.keys(RuleJSON.ruleSearch).forEach(key => { searchJson[key] = $('#' + 'ruleSearch_' + key).value; }); - RuleJSON.ruleSearch = JSON.stringify(searchJson); + //RuleJSON.ruleSearch = JSON.stringify(searchJson); + RuleJSON.ruleSearch = searchJson; // 转换发现规则 let exploreJson = {}; - Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => { + //Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => { + Object.keys(RuleJSON.ruleExplore).forEach(key => { exploreJson[key] = $('#' + 'ruleExplore_' + key).value; }); - RuleJSON.ruleExplore = JSON.stringify(exploreJson); + //RuleJSON.ruleExplore = JSON.stringify(exploreJson); + RuleJSON.ruleExplore = exploreJson; // 转换详情页规则 let bookInfoJson = {}; - Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => { + //Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => { + Object.keys(RuleJSON.ruleBookInfo).forEach(key => { bookInfoJson[key] = $('#' + 'ruleBookInfo_' + key).value; }); - RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson); + //RuleJSON.ruleBookInfo = JSON.stringify(bookInfoJson); + RuleJSON.ruleBookInfo = bookInfoJson; // 转换目录规则 let tocJson = {}; - Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => { + //Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => { + Object.keys(RuleJSON.ruleToc).forEach(key => { tocJson[key] = $('#' + 'ruleToc_' + key).value; }); - RuleJSON.ruleToc = JSON.stringify(tocJson); + //RuleJSON.ruleToc = JSON.stringify(tocJson); + RuleJSON.ruleToc = tocJson; // 转换正文规则 let contentJson = {}; - Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => { + //Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => { + Object.keys(RuleJSON.ruleContent).forEach(key => { contentJson[key] = $('#' + 'ruleContent_' + key).value; }); - RuleJSON.ruleContent = JSON.stringify(contentJson); + //RuleJSON.ruleContent = JSON.stringify(contentJson); + RuleJSON.ruleContent = contentJson; RuleJSON.lastUpdateTime = RuleJSON.lastUpdateTime == '' ? 0 : parseInt(RuleJSON.lastUpdateTime); RuleJSON.customOrder = RuleJSON.customOrder == '' ? 0 : parseInt(RuleJSON.customOrder); @@ -171,40 +186,50 @@ function json2rule(RuleEditor) { // 转换搜索规则 if (RuleEditor.ruleSearch) { - let searchJson = JSON.parse(RuleEditor.ruleSearch); - Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => { + //let searchJson = JSON.parse(RuleEditor.ruleSearch); + let searchJson = RuleEditor.ruleSearch; + //Object.keys(JSON.parse(RuleJSON.ruleSearch)).forEach(key => { + Object.keys(RuleJSON.ruleSearch).forEach(key => { $('#' + 'ruleSearch_' + key).value = searchJson[key] ? searchJson[key] : ''; }); } // 转换发现规则 if (RuleEditor.ruleExplore) { - let exploreJson = JSON.parse(RuleEditor.ruleExplore); - Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => { + //let exploreJson = JSON.parse(RuleEditor.ruleExplore); + //Object.keys(JSON.parse(RuleJSON.ruleExplore)).forEach(key => { + let exploreJson = RuleEditor.ruleExplore; + Object.keys(RuleJSON.ruleExplore).forEach(key => { $('#' + 'ruleExplore_' + key).value = exploreJson[key] ? exploreJson[key] : ''; }); } // 转换详情页规则 if (RuleEditor.ruleBookInfo) { - let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo); - Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => { + //let bookInfoJson = JSON.parse(RuleEditor.ruleBookInfo); + //Object.keys(JSON.parse(RuleJSON.ruleBookInfo)).forEach(key => { + let bookInfoJson = RuleEditor.ruleBookInfo; + Object.keys(RuleJSON.ruleBookInfo).forEach(key => { $('#' + 'ruleBookInfo_' + key).value = bookInfoJson[key] ? bookInfoJson[key] : ''; }); } // 转换目录规则 if (RuleEditor.ruleToc) { - let tocJson = JSON.parse(RuleEditor.ruleToc); - Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => { + //let tocJson = JSON.parse(RuleEditor.ruleToc); + //Object.keys(JSON.parse(RuleJSON.ruleToc)).forEach(key => { + let tocJson = RuleEditor.ruleToc; + Object.keys(RuleJSON.ruleToc).forEach(key => { $('#' + 'ruleToc_' + key).value = tocJson[key] ? tocJson[key] : ''; }); } // 转换正文规则 if (RuleEditor.ruleContent) { - let contentJson = JSON.parse(RuleEditor.ruleContent); - Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => { + //let contentJson = JSON.parse(RuleEditor.ruleContent); + //Object.keys(JSON.parse(RuleJSON.ruleContent)).forEach(key => { + let contentJson = RuleEditor.ruleContent; + Object.keys(RuleJSON.ruleContent).forEach(key => { $('#' + 'ruleContent_' + key).value = contentJson[key] ? contentJson[key] : ''; }); } diff --git a/app/src/main/assets/web/book.css b/app/src/main/assets/web/new/bookshelf.css similarity index 100% rename from app/src/main/assets/web/book.css rename to app/src/main/assets/web/new/bookshelf.css diff --git a/app/src/main/assets/web/new/bookshelf.html b/app/src/main/assets/web/new/bookshelf.html new file mode 100644 index 000000000..87ee3bf9b --- /dev/null +++ b/app/src/main/assets/web/new/bookshelf.html @@ -0,0 +1,46 @@ + + + + + + + + + yd-web-tool + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ + + + + \ No newline at end of file diff --git a/app/src/main/assets/web/book.js b/app/src/main/assets/web/new/bookshelf.js similarity index 100% rename from app/src/main/assets/web/book.js rename to app/src/main/assets/web/new/bookshelf.js diff --git a/app/src/main/assets/web/css/about.f23c15cb.css b/app/src/main/assets/web/new/css/about.f23c15cb.css similarity index 100% rename from app/src/main/assets/web/css/about.f23c15cb.css rename to app/src/main/assets/web/new/css/about.f23c15cb.css diff --git a/app/src/main/assets/web/css/app.e1c0d2e4.css b/app/src/main/assets/web/new/css/app.e1c0d2e4.css similarity index 100% rename from app/src/main/assets/web/css/app.e1c0d2e4.css rename to app/src/main/assets/web/new/css/app.e1c0d2e4.css diff --git a/app/src/main/assets/web/css/chunk-vendors.ad4ff18f.css b/app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css similarity index 100% rename from app/src/main/assets/web/css/chunk-vendors.ad4ff18f.css rename to app/src/main/assets/web/new/css/chunk-vendors.ad4ff18f.css diff --git a/app/src/main/assets/web/css/detail.42c41bd6.css b/app/src/main/assets/web/new/css/detail.42c41bd6.css similarity index 100% rename from app/src/main/assets/web/css/detail.42c41bd6.css rename to app/src/main/assets/web/new/css/detail.42c41bd6.css diff --git a/app/src/main/assets/web/fonts/element-icons.535877f5.woff b/app/src/main/assets/web/new/fonts/element-icons.535877f5.woff similarity index 100% rename from app/src/main/assets/web/fonts/element-icons.535877f5.woff rename to app/src/main/assets/web/new/fonts/element-icons.535877f5.woff diff --git a/app/src/main/assets/web/fonts/element-icons.732389de.ttf b/app/src/main/assets/web/new/fonts/element-icons.732389de.ttf similarity index 100% rename from app/src/main/assets/web/fonts/element-icons.732389de.ttf rename to app/src/main/assets/web/new/fonts/element-icons.732389de.ttf diff --git a/app/src/main/assets/web/fonts/iconfont.f9a3fb0e.woff b/app/src/main/assets/web/new/fonts/iconfont.f9a3fb0e.woff similarity index 100% rename from app/src/main/assets/web/fonts/iconfont.f9a3fb0e.woff rename to app/src/main/assets/web/new/fonts/iconfont.f9a3fb0e.woff diff --git a/app/src/main/assets/web/fonts/popfont.f39ecc1a.ttf b/app/src/main/assets/web/new/fonts/popfont.f39ecc1a.ttf similarity index 100% rename from app/src/main/assets/web/fonts/popfont.f39ecc1a.ttf rename to app/src/main/assets/web/new/fonts/popfont.f39ecc1a.ttf diff --git a/app/src/main/assets/web/fonts/shelffont.6c094b6d.ttf b/app/src/main/assets/web/new/fonts/shelffont.6c094b6d.ttf similarity index 100% rename from app/src/main/assets/web/fonts/shelffont.6c094b6d.ttf rename to app/src/main/assets/web/new/fonts/shelffont.6c094b6d.ttf diff --git a/app/src/main/assets/web/img/icons/android-chrome-192x192.png b/app/src/main/assets/web/new/img/icons/android-chrome-192x192.png similarity index 100% rename from app/src/main/assets/web/img/icons/android-chrome-192x192.png rename to app/src/main/assets/web/new/img/icons/android-chrome-192x192.png diff --git a/app/src/main/assets/web/img/icons/android-chrome-512x512.png b/app/src/main/assets/web/new/img/icons/android-chrome-512x512.png similarity index 100% rename from app/src/main/assets/web/img/icons/android-chrome-512x512.png rename to app/src/main/assets/web/new/img/icons/android-chrome-512x512.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon-120x120.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon-120x120.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon-120x120.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon-120x120.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon-152x152.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon-152x152.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon-152x152.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon-152x152.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon-180x180.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon-180x180.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon-180x180.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon-180x180.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon-60x60.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon-60x60.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon-60x60.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon-60x60.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon-76x76.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon-76x76.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon-76x76.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon-76x76.png diff --git a/app/src/main/assets/web/img/icons/apple-touch-icon.png b/app/src/main/assets/web/new/img/icons/apple-touch-icon.png similarity index 100% rename from app/src/main/assets/web/img/icons/apple-touch-icon.png rename to app/src/main/assets/web/new/img/icons/apple-touch-icon.png diff --git a/app/src/main/assets/web/img/icons/favicon-16x16.png b/app/src/main/assets/web/new/img/icons/favicon-16x16.png similarity index 100% rename from app/src/main/assets/web/img/icons/favicon-16x16.png rename to app/src/main/assets/web/new/img/icons/favicon-16x16.png diff --git a/app/src/main/assets/web/img/icons/favicon-32x32.png b/app/src/main/assets/web/new/img/icons/favicon-32x32.png similarity index 100% rename from app/src/main/assets/web/img/icons/favicon-32x32.png rename to app/src/main/assets/web/new/img/icons/favicon-32x32.png diff --git a/app/src/main/assets/web/img/icons/msapplication-icon-144x144.png b/app/src/main/assets/web/new/img/icons/msapplication-icon-144x144.png similarity index 100% rename from app/src/main/assets/web/img/icons/msapplication-icon-144x144.png rename to app/src/main/assets/web/new/img/icons/msapplication-icon-144x144.png diff --git a/app/src/main/assets/web/img/icons/mstile-150x150.png b/app/src/main/assets/web/new/img/icons/mstile-150x150.png similarity index 100% rename from app/src/main/assets/web/img/icons/mstile-150x150.png rename to app/src/main/assets/web/new/img/icons/mstile-150x150.png diff --git a/app/src/main/assets/web/img/icons/safari-pinned-tab.svg b/app/src/main/assets/web/new/img/icons/safari-pinned-tab.svg similarity index 100% rename from app/src/main/assets/web/img/icons/safari-pinned-tab.svg rename to app/src/main/assets/web/new/img/icons/safari-pinned-tab.svg diff --git a/app/src/main/assets/web/img/noCover.b5c48bc1.jpeg b/app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg similarity index 100% rename from app/src/main/assets/web/img/noCover.b5c48bc1.jpeg rename to app/src/main/assets/web/new/img/noCover.b5c48bc1.jpeg diff --git a/app/src/main/assets/web/js/about.2589b5fe.js b/app/src/main/assets/web/new/js/about.2589b5fe.js similarity index 100% rename from app/src/main/assets/web/js/about.2589b5fe.js rename to app/src/main/assets/web/new/js/about.2589b5fe.js diff --git a/app/src/main/assets/web/js/about~detail.08c372e6.js b/app/src/main/assets/web/new/js/about~detail.08c372e6.js similarity index 100% rename from app/src/main/assets/web/js/about~detail.08c372e6.js rename to app/src/main/assets/web/new/js/about~detail.08c372e6.js diff --git a/app/src/main/assets/web/js/app.b25f3cec.js b/app/src/main/assets/web/new/js/app.b25f3cec.js similarity index 100% rename from app/src/main/assets/web/js/app.b25f3cec.js rename to app/src/main/assets/web/new/js/app.b25f3cec.js diff --git a/app/src/main/assets/web/js/chunk-vendors.b3838a2d.js b/app/src/main/assets/web/new/js/chunk-vendors.b3838a2d.js similarity index 100% rename from app/src/main/assets/web/js/chunk-vendors.b3838a2d.js rename to app/src/main/assets/web/new/js/chunk-vendors.b3838a2d.js diff --git a/app/src/main/assets/web/js/detail.043d6e39.js b/app/src/main/assets/web/new/js/detail.043d6e39.js similarity index 100% rename from app/src/main/assets/web/js/detail.043d6e39.js rename to app/src/main/assets/web/new/js/detail.043d6e39.js diff --git a/app/src/main/assets/web/manifest.json b/app/src/main/assets/web/new/manifest.json similarity index 100% rename from app/src/main/assets/web/manifest.json rename to app/src/main/assets/web/new/manifest.json diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index 7cb9eff1f..5c4c02964 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -73,7 +73,7 @@ class App : Application() { .primaryColor( getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600)) ).accentColor( - getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800)) + getPrefInt("colorAccentNight", getCompatColor(R.color.md_deep_orange_800)) ).backgroundColor( getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) ).bottomBackground( diff --git a/app/src/main/java/io/legado/app/constant/AppConst.kt b/app/src/main/java/io/legado/app/constant/AppConst.kt index 810e6deb2..cf37e2b10 100644 --- a/app/src/main/java/io/legado/app/constant/AppConst.kt +++ b/app/src/main/java/io/legado/app/constant/AppConst.kt @@ -41,17 +41,30 @@ object AppConst { val keyboardToolChars: List by lazy { arrayListOf( - "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", "$", "#", "!", ".", - "href", "src", "textNodes", "xpath", "json", "css", "id", "class", "tag" + "※", "@", "&", "|", "%", "/", ":", "[", "]", "{", "}", "<", ">", "\\", + "$", "#", "!", ".", "href", "src", "textNodes", "xpath", "json", "css", + "id", "class", "tag" ) } val bookGroupAll = BookGroup(-1, App.INSTANCE.getString(R.string.all)) val bookGroupLocal = BookGroup(-2, App.INSTANCE.getString(R.string.local)) val bookGroupAudio = BookGroup(-3, App.INSTANCE.getString(R.string.audio)) + val bookGroupNone = BookGroup(-4, App.INSTANCE.getString(R.string.no_group)) const val notificationIdRead = 1144771 const val notificationIdAudio = 1144772 const val notificationIdWeb = 1144773 const val notificationIdDownload = 1144774 + + val urlOption: String by lazy { + """ + ,{ + "charset": "", + "method": "POST", + "body": "", + "headers": {"User-Agent": ""} + } + """.trimIndent() + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt index e14ec42f6..8913956d3 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -42,4 +42,5 @@ object PreferKey { const val shareLayout = "shareLayout" const val readStyleSelect = "readStyleSelect" const val systemTypefaces = "system_typefaces" + const val readBodyToLh = "readBodyToLh" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt index f32d6a199..d2256dd85 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -4,6 +4,7 @@ import android.content.Context import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase +import androidx.room.migration.Migration import androidx.sqlite.db.SupportSQLiteDatabase import io.legado.app.data.dao.* import io.legado.app.data.entities.* @@ -18,7 +19,7 @@ import kotlinx.coroutines.launch ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, RssStar::class, TxtTocRule::class], - version = 8, + version = 12, exportSchema = true ) abstract class AppDatabase : RoomDatabase() { @@ -30,6 +31,7 @@ abstract class AppDatabase : RoomDatabase() { fun createDatabase(context: Context): AppDatabase { return Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME) .fallbackToDestructiveMigration() + .addMigrations(migration_10_11, migration_11_12) .addCallback(object : Callback() { override fun onDestructiveMigration(db: SupportSQLiteDatabase) { GlobalScope.launch { Restore.restoreDatabase(Backup.backupPath) } @@ -37,6 +39,29 @@ abstract class AppDatabase : RoomDatabase() { }) .build() } + + private val migration_10_11 = object : Migration(10, 11) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL("DROP TABLE txtTocRules") + database.execSQL( + """ + CREATE TABLE txtTocRules(id INTEGER NOT NULL, + name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL, + enable INTEGER NOT NULL, PRIMARY KEY (id)) + """ + ) + } + } + + private val migration_11_12 = object : Migration(11, 12) { + override fun migrate(database: SupportSQLiteDatabase) { + database.execSQL( + """ + ALTER TABLE rssSources ADD style TEXT + """ + ) + } + } } abstract fun bookDao(): BookDao diff --git a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt index 38cc452f2..d0588bec8 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookChapterDao.kt @@ -10,16 +10,16 @@ import io.legado.app.data.entities.BookChapter @Dao interface BookChapterDao { - @Query("select * from chapters where bookUrl = :bookUrl") + @Query("select * from chapters where bookUrl = :bookUrl order by `index`") fun observeByBook(bookUrl: String): LiveData> - @Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%'") + @Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`") fun liveDataSearch(bookUrl: String, key: String): LiveData> - @Query("select * from chapters where bookUrl = :bookUrl") + @Query("select * from chapters where bookUrl = :bookUrl order by `index`") fun getChapterList(bookUrl: String): List - @Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end") + @Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end order by `index`") fun getChapterList(bookUrl: String, start: Int, end: Int): List @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt index d8d960df3..c91e01648 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt @@ -30,6 +30,9 @@ interface BookDao { @Query("select * from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0") fun observeNoGroup(): LiveData> + @Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0") + fun observeNoGroupSize(): LiveData + @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") fun liveDataSearch(key: String): LiveData> @@ -42,6 +45,12 @@ interface BookDao { @Query("SELECT * FROM books WHERE bookUrl = :bookUrl") fun getBook(bookUrl: String): Book? + @Query("SELECT * FROM books WHERE name = :name and author = :author") + fun getBook(name: String, author: String): Book? + + @get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0") + val noGroupSize: Int + @get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0") val webBooks: List diff --git a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt index fbf3c3611..10bf63933 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssArticleDao.kt @@ -12,17 +12,18 @@ interface RssArticleDao { fun get(origin: String, link: String): RssArticle? @Query( - """select t1.link, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read + """select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content, t1.description, t1.image, t1.pubDate, ifNull(t2.read, 0) as read from rssArticles as t1 left join rssReadRecords as t2 - on t1.link = t2.record where origin = :origin order by `order` desc""" + on t1.link = t2.record where origin = :origin and sort = :sort + order by `order` desc""" ) - fun liveByOrigin(origin: String): LiveData> + fun liveByOriginSort(origin: String, sort: String): LiveData> @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg rssArticle: RssArticle) - @Query("delete from rssArticles where origin = :origin and `order` < :order") - fun clearOld(origin: String, order: Long) + @Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order") + fun clearOld(origin: String, sort: String, order: Long) @Update fun update(vararg rssArticle: RssArticle) diff --git a/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt index 1bd502a09..d1b023896 100644 --- a/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt @@ -28,4 +28,6 @@ interface TxtTocRuleDao { @Delete fun delete(vararg rule: TxtTocRule) + @Query("delete from txtTocRules where id < 0") + fun deleteDefault() } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/Book.kt b/app/src/main/java/io/legado/app/data/entities/Book.kt index 0dd5cab9e..fd96d137b 100644 --- a/app/src/main/java/io/legado/app/data/entities/Book.kt +++ b/app/src/main/java/io/legado/app/data/entities/Book.kt @@ -4,7 +4,6 @@ import android.os.Parcelable import androidx.room.Entity import androidx.room.Ignore import androidx.room.Index -import androidx.room.PrimaryKey import io.legado.app.constant.AppPattern import io.legado.app.constant.BookType import io.legado.app.utils.GSON @@ -15,9 +14,12 @@ import java.nio.charset.Charset import kotlin.math.max @Parcelize -@Entity(tableName = "books", indices = [(Index(value = ["bookUrl"], unique = true))]) +@Entity( + tableName = "books", + primaryKeys = ["name", "author"], + indices = [(Index(value = ["bookUrl"], unique = true))] +) data class Book( - @PrimaryKey override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var tocUrl: String = "", // 目录页Url (toc=table of Contents) var origin: String = BookType.local, // 书源URL(默认BookType.local) diff --git a/app/src/main/java/io/legado/app/data/entities/BookSource.kt b/app/src/main/java/io/legado/app/data/entities/BookSource.kt index 78852a4ec..1cb9a0269 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookSource.kt @@ -1,10 +1,7 @@ package io.legado.app.data.entities import android.os.Parcelable -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.Index -import androidx.room.PrimaryKey +import androidx.room.* import io.legado.app.App import io.legado.app.constant.AppConst import io.legado.app.constant.AppConst.userAgent @@ -15,12 +12,12 @@ import io.legado.app.utils.ACache import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import io.legado.app.utils.getPrefString -import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize import java.util.* import javax.script.SimpleBindings @Parcelize +@TypeConverters(BookSource.Converters::class) @Entity( tableName = "book_sources", indices = [(Index(value = ["bookSourceUrl"], unique = false))] @@ -40,12 +37,12 @@ data class BookSource( var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 var weight: Int = 0, // 智能排序的权重 var exploreUrl: String? = null, // 发现url - var ruleExplore: String? = null, // 发现规则 + var ruleExplore: ExploreRule? = null, // 发现规则 var searchUrl: String? = null, // 搜索url - var ruleSearch: String? = null, // 搜索规则 - var ruleBookInfo: String? = null, // 书籍信息页规则 - var ruleToc: String? = null, // 目录页规则 - var ruleContent: String? = null // 正文页规则 + var ruleSearch: SearchRule? = null, // 搜索规则 + var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则 + var ruleToc: TocRule? = null, // 目录页规则 + var ruleContent: ContentRule? = null // 正文页规则 ) : Parcelable, JsExtensions { override fun hashCode(): Int { @@ -59,26 +56,6 @@ data class BookSource( return false } - @Ignore - @IgnoredOnParcel - private var searchRuleV: SearchRule? = null - - @Ignore - @IgnoredOnParcel - private var exploreRuleV: ExploreRule? = null - - @Ignore - @IgnoredOnParcel - private var bookInfoRuleV: BookInfoRule? = null - - @Ignore - @IgnoredOnParcel - private var tocRuleV: TocRule? = null - - @Ignore - @IgnoredOnParcel - private var contentRuleV: ContentRule? = null - @Throws(Exception::class) fun getHeaderMap(): Map { val headerMap = HashMap() @@ -99,43 +76,23 @@ data class BookSource( } fun getSearchRule(): SearchRule { - searchRuleV ?: let { - searchRuleV = GSON.fromJsonObject(ruleSearch) - searchRuleV ?: let { searchRuleV = SearchRule() } - } - return searchRuleV!! + return ruleSearch ?: SearchRule() } fun getExploreRule(): ExploreRule { - exploreRuleV ?: let { - exploreRuleV = GSON.fromJsonObject(ruleExplore) - exploreRuleV ?: let { exploreRuleV = ExploreRule() } - } - return exploreRuleV!! + return ruleExplore ?: ExploreRule() } fun getBookInfoRule(): BookInfoRule { - bookInfoRuleV ?: let { - bookInfoRuleV = GSON.fromJsonObject(ruleBookInfo) - bookInfoRuleV ?: let { bookInfoRuleV = BookInfoRule() } - } - return bookInfoRuleV!! + return ruleBookInfo ?: BookInfoRule() } fun getTocRule(): TocRule { - tocRuleV ?: let { - tocRuleV = GSON.fromJsonObject(ruleToc) - tocRuleV ?: let { tocRuleV = TocRule() } - } - return tocRuleV!! + return ruleToc ?: TocRule() } fun getContentRule(): ContentRule { - contentRuleV ?: let { - contentRuleV = GSON.fromJsonObject(ruleContent) - contentRuleV ?: let { contentRuleV = ContentRule() } - } - return contentRuleV!! + return ruleContent ?: ContentRule() } fun addGroup(group: String) { @@ -169,7 +126,7 @@ data class BookSource( } } val b = a.split("(&&|\n)+".toRegex()) - b.map { c -> + b.forEach { c -> val d = c.split("::") if (d.size > 1) exploreKinds.add(ExploreKind(d[0], d[1])) @@ -219,4 +176,57 @@ data class BookSource( var title: String, var url: String? = null ) + + class Converters { + @TypeConverter + fun exploreRuleToString(exploreRule: ExploreRule?): String { + return GSON.toJson(exploreRule) + } + + @TypeConverter + fun stringToExploreRule(json: String?): ExploreRule? { + return GSON.fromJsonObject(json) + } + + @TypeConverter + fun searchRuleToString(searchRule: SearchRule): String { + return GSON.toJson(searchRule) + } + + @TypeConverter + fun stringToSearchRule(json: String?): SearchRule? { + return GSON.fromJsonObject(json) + } + + @TypeConverter + fun bookInfoRuleToString(bookInfoRule: BookInfoRule): String { + return GSON.toJson(bookInfoRule) + } + + @TypeConverter + fun stringToBookInfoRule(json: String?): BookInfoRule? { + return GSON.fromJsonObject(json) + } + + @TypeConverter + fun tocRuleToString(tocRule: TocRule): String { + return GSON.toJson(tocRule) + } + + @TypeConverter + fun stringToTocRule(json: String?): TocRule? { + return GSON.fromJsonObject(json) + } + + @TypeConverter + fun contentRuleToString(contentRule: ContentRule): String { + return GSON.toJson(contentRule) + } + + @TypeConverter + fun stringToContentRule(json: String?): ContentRule? { + return GSON.fromJsonObject(json) + } + + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt index c74597e74..d9ae5531b 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssArticle.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssArticle.kt @@ -9,6 +9,7 @@ import androidx.room.Entity ) data class RssArticle( var origin: String = "", + var sort: String = "", var title: String = "", var order: Long = 0, var link: String = "", @@ -20,7 +21,7 @@ data class RssArticle( ) { override fun hashCode(): Int { - return super.hashCode() + return link.hashCode() } override fun equals(other: Any?): Boolean { @@ -36,6 +37,7 @@ data class RssArticle( fun toStar(): RssStar { return RssStar( origin = origin, + sort = sort, title = title, starTime = System.currentTimeMillis(), link = link, diff --git a/app/src/main/java/io/legado/app/data/entities/RssSource.kt b/app/src/main/java/io/legado/app/data/entities/RssSource.kt index 999b12523..a7948e6e7 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssSource.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssSource.kt @@ -23,6 +23,7 @@ data class RssSource( var sourceIcon: String = "", var sourceGroup: String? = null, var enabled: Boolean = true, + var sortUrl: String? = null, //列表规则 var ruleArticles: String? = null, var ruleNextPage: String? = null, @@ -33,9 +34,11 @@ data class RssSource( var ruleImage: String? = null, var ruleLink: String? = null, var ruleContent: String? = null, + var style: String? = null, var header: String? = null, var enableJs: Boolean = false, var loadWithBaseUrl: Boolean = false, + var customOrder: Int = 0 ) : Parcelable, JsExtensions { @@ -99,4 +102,16 @@ data class RssSource( return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty()) } + fun sortUrls(): LinkedHashMap { + val sortMap = linkedMapOf() + sortUrl?.split("(&&|\n)+".toRegex())?.forEach { c -> + val d = c.split("::") + if (d.size > 1) + sortMap[d[0]] = d[1] + } + if (sortMap.isEmpty()) { + sortMap[""] = sourceUrl + } + return sortMap + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssStar.kt b/app/src/main/java/io/legado/app/data/entities/RssStar.kt index 1d0c266f8..94196416b 100644 --- a/app/src/main/java/io/legado/app/data/entities/RssStar.kt +++ b/app/src/main/java/io/legado/app/data/entities/RssStar.kt @@ -9,6 +9,7 @@ import androidx.room.Entity ) data class RssStar( var origin: String = "", + var sort: String = "", var title: String = "", var starTime: Long = 0, var link: String = "", @@ -20,6 +21,7 @@ data class RssStar( fun toRssArticle(): RssArticle { return RssArticle( origin = origin, + sort = sort, title = title, link = link, pubDate = pubDate, diff --git a/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt b/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt index cdfca7567..52669dab7 100644 --- a/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt @@ -7,6 +7,7 @@ import androidx.room.PrimaryKey @Entity(tableName = "txtTocRules") data class TxtTocRule( @PrimaryKey + var id: Long = System.currentTimeMillis(), var name: String = "", var rule: String = "", var serialNumber: Int = -1, diff --git a/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt index a28f3c6e6..57fa3c495 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/BookInfoRule.kt @@ -1,5 +1,8 @@ package io.legado.app.data.entities.rule +import android.os.Parcel +import android.os.Parcelable + data class BookInfoRule( var init: String? = null, var name: String? = null, @@ -11,4 +14,45 @@ data class BookInfoRule( var coverUrl: String? = null, var tocUrl: String? = null, var wordCount: String? = null -) \ No newline at end of file +) : Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(init) + dest.writeString(name) + dest.writeString(author) + dest.writeString(intro) + dest.writeString(kind) + dest.writeString(lastChapter) + dest.writeString(updateTime) + dest.writeString(coverUrl) + dest.writeString(tocUrl) + dest.writeString(wordCount) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): BookInfoRule { + return BookInfoRule(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt index b0fe78614..306e7e659 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ContentRule.kt @@ -1,8 +1,39 @@ package io.legado.app.data.entities.rule +import android.os.Parcel +import android.os.Parcelable + data class ContentRule( var content: String? = null, var nextContentUrl: String? = null, var webJs: String? = null, var sourceRegex: String? = null -) \ No newline at end of file +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(content) + dest.writeString(nextContentUrl) + dest.writeString(webJs) + dest.writeString(sourceRegex) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ContentRule { + return ContentRule(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt index 22b267ed3..8ccbe8aba 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/ExploreRule.kt @@ -1,5 +1,8 @@ package io.legado.app.data.entities.rule +import android.os.Parcel +import android.os.Parcelable + data class ExploreRule( override var bookList: String? = null, override var name: String? = null, @@ -11,4 +14,46 @@ data class ExploreRule( override var bookUrl: String? = null, override var coverUrl: String? = null, override var wordCount: String? = null -) : BookListRule \ No newline at end of file +) : BookListRule, Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(bookList) + dest.writeString(name) + dest.writeString(author) + dest.writeString(intro) + dest.writeString(kind) + dest.writeString(lastChapter) + dest.writeString(updateTime) + dest.writeString(bookUrl) + dest.writeString(coverUrl) + dest.writeString(wordCount) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): ExploreRule { + return ExploreRule(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + diff --git a/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt index 83921e56d..3968226e2 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/SearchRule.kt @@ -1,5 +1,8 @@ package io.legado.app.data.entities.rule +import android.os.Parcel +import android.os.Parcelable + data class SearchRule( override var bookList: String? = null, override var name: String? = null, @@ -11,4 +14,46 @@ data class SearchRule( override var bookUrl: String? = null, override var coverUrl: String? = null, override var wordCount: String? = null -) : BookListRule \ No newline at end of file +) : BookListRule, Parcelable { + + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(bookList) + dest.writeString(name) + dest.writeString(author) + dest.writeString(intro) + dest.writeString(kind) + dest.writeString(lastChapter) + dest.writeString(updateTime) + dest.writeString(bookUrl) + dest.writeString(coverUrl) + dest.writeString(wordCount) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): SearchRule { + return SearchRule(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} + diff --git a/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt index c484e7226..d49a08e33 100644 --- a/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/rule/TocRule.kt @@ -1,5 +1,8 @@ package io.legado.app.data.entities.rule +import android.os.Parcel +import android.os.Parcelable + data class TocRule( var chapterList: String? = null, var chapterName: String? = null, @@ -7,4 +10,36 @@ data class TocRule( var isVip: String? = null, var updateTime: String? = null, var nextTocUrl: String? = null -) \ No newline at end of file +) : Parcelable { + constructor(parcel: Parcel) : this( + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString(), + parcel.readString() + ) + + override fun writeToParcel(dest: Parcel, flags: Int) { + dest.writeString(chapterList) + dest.writeString(chapterName) + dest.writeString(chapterUrl) + dest.writeString(isVip) + dest.writeString(updateTime) + dest.writeString(nextTocUrl) + } + + override fun describeContents(): Int { + return 0 + } + + companion object CREATOR : Parcelable.Creator { + override fun createFromParcel(parcel: Parcel): TocRule { + return TocRule(parcel) + } + + override fun newArray(size: Int): Array { + return arrayOfNulls(size) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt index 2e336fdad..547322cce 100644 --- a/app/src/main/java/io/legado/app/help/AppConfig.kt +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -115,12 +115,23 @@ object AppConfig { App.INSTANCE.putPrefBoolean("bookGroupAudio", value) } + var bookGroupNoneShow: Boolean + get() = App.INSTANCE.getPrefBoolean("bookGroupNone", false) + set(value) { + App.INSTANCE.putPrefBoolean("bookGroupNone", value) + } + var elevation: Int get() = App.INSTANCE.getPrefInt("elevation", -1) set(value) { App.INSTANCE.putPrefInt("elevation", value) } + val autoChangeSource: Boolean get() = App.INSTANCE.getPrefBoolean("autoChangeSource", true) + + val readBodyToLh: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.readBodyToLh, true) + + val isGooglePlay: Boolean get() = App.INSTANCE.channel == "google" } val Context.channel: String diff --git a/app/src/main/java/io/legado/app/help/BookHelp.kt b/app/src/main/java/io/legado/app/help/BookHelp.kt index b6e3ec000..3c0fa644e 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -1,16 +1,16 @@ package io.legado.app.help -import android.net.Uri -import androidx.documentfile.provider.DocumentFile -import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap +import com.hankcs.hanlp.HanLP import io.legado.app.App -import io.legado.app.R import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.data.entities.ReplaceRule import io.legado.app.model.localBook.AnalyzeTxtFile -import io.legado.app.utils.* +import io.legado.app.utils.FileUtils +import io.legado.app.utils.MD5Utils +import io.legado.app.utils.postEvent +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.withContext import org.apache.commons.text.similarity.JaccardSimilarity @@ -20,12 +20,9 @@ import kotlin.math.min object BookHelp { private const val cacheFolderName = "book_cache" - val downloadPath: String - get() = App.INSTANCE.getPrefString(R.string.pk_download_path) - ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath - ?: App.INSTANCE.cacheDir.absolutePath - - private val downloadUri get() = Uri.parse(downloadPath) + private val downloadDir: File = + App.INSTANCE.getExternalFilesDir(null) + ?: App.INSTANCE.cacheDir private fun bookFolderName(book: Book): String { return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl) @@ -40,137 +37,73 @@ object BookHelp { } fun clearCache() { - if (downloadPath.isContentPath()) { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri) - ?.findFile(cacheFolderName) - ?.delete() - } else { - FileUtils.deleteFile( - FileUtils.getPath( - File(downloadPath), - subDirs = *arrayOf(cacheFolderName) - ) + FileUtils.deleteFile( + FileUtils.getPath( + downloadDir, + subDirs = *arrayOf(cacheFolderName) ) - } + ) } @Synchronized fun saveContent(book: Book, bookChapter: BookChapter, content: String) { if (content.isEmpty()) return - if (downloadPath.isContentPath()) { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> - DocumentUtils.createFileIfNotExist( - root, - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - )?.uri?.writeText(App.INSTANCE, content) - } - } else { - FileUtils.createFileIfNotExist( - File(downloadPath), - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ).writeText(content) - } + FileUtils.createFileIfNotExist( + downloadDir, + formatChapterName(bookChapter), + subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) + ).writeText(content) postEvent(EventBus.SAVE_CONTENT, bookChapter) } fun getChapterFiles(book: Book): List { val fileNameList = arrayListOf() - if (downloadPath.isContentPath()) { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> - DocumentUtils.createFolderIfNotExist( - root, - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - )?.let { bookDoc -> - DocumentUtils.listFiles(App.INSTANCE, bookDoc.uri).forEach { - fileNameList.add(it.name) - } - } - } - } else { - FileUtils.createFolderIfNotExist( - File(downloadPath), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ).list()?.let { - fileNameList.addAll(it) - } + FileUtils.createFolderIfNotExist( + downloadDir, + subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) + ).list()?.let { + fileNameList.addAll(it) } return fileNameList } fun hasContent(book: Book, bookChapter: BookChapter): Boolean { - when { - book.isLocalBook() -> { - return true - } - downloadPath.isContentPath() -> { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> - return DocumentUtils.exists( - root, - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ) - } - } - else -> { - return FileUtils.exists( - File(downloadPath), - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ) - } + return if (book.isLocalBook()) { + true + } else { + FileUtils.exists( + downloadDir, + formatChapterName(bookChapter), + subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) + ) } - return false } fun getContent(book: Book, bookChapter: BookChapter): String? { - when { - book.isLocalBook() -> { - return AnalyzeTxtFile.getContent(book, bookChapter) - } - downloadPath.isContentPath() -> { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> - return DocumentUtils.getDirDocument( - root, - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - )?.findFile(formatChapterName(bookChapter)) - ?.uri?.readText(App.INSTANCE) - } - } - else -> { - val file = FileUtils.getFile( - File(downloadPath), - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ) - if (file.exists()) { - return file.readText() - } + if (book.isLocalBook()) { + return AnalyzeTxtFile.getContent(book, bookChapter) + } else { + val file = FileUtils.getFile( + downloadDir, + formatChapterName(bookChapter), + subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) + ) + if (file.exists()) { + return file.readText() } } return null } fun delContent(book: Book, bookChapter: BookChapter) { - when { - book.isLocalBook() -> return - downloadPath.isContentPath() -> { - DocumentFile.fromTreeUri(App.INSTANCE, downloadUri)?.let { root -> - DocumentUtils.getDirDocument( - root, - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - )?.findFile(formatChapterName(bookChapter)) - ?.delete() - } - } - else -> { - FileUtils.createFileIfNotExist( - File(downloadPath), - formatChapterName(bookChapter), - subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) - ).delete() - } + if (book.isLocalBook()) { + return + } else { + FileUtils.createFileIfNotExist( + downloadDir, + formatChapterName(bookChapter), + subDirs = *arrayOf(cacheFolderName, bookFolderName(book)) + ).delete() } } @@ -242,24 +175,16 @@ object BookHelp { private var replaceRules: List = arrayListOf() @Synchronized - fun upReplaceRules(name: String? = null, origin: String? = null) { - if (name != null) { - if (bookName != name || bookOrigin != origin) { - replaceRules = if (origin.isNullOrEmpty()) { - App.db.replaceRuleDao().findEnabledByScope(name) - } else { - App.db.replaceRuleDao().findEnabledByScope(name, origin) - } - bookName = name - bookOrigin = origin - } - } else { - val o = bookOrigin - bookName?.let { - replaceRules = if (o.isNullOrEmpty()) { - App.db.replaceRuleDao().findEnabledByScope(it) - } else { - App.db.replaceRuleDao().findEnabledByScope(it, o) + suspend fun upReplaceRules() { + withContext(IO) { + synchronized(this) { + val o = bookOrigin + bookName?.let { + replaceRules = if (o.isNullOrEmpty()) { + App.db.replaceRuleDao().findEnabledByScope(it) + } else { + App.db.replaceRuleDao().findEnabledByScope(it, o) + } } } } @@ -271,10 +196,20 @@ object BookHelp { origin: String?, content: String, enableReplace: Boolean - ): String { + ): List { var c = content if (enableReplace) { - upReplaceRules(name, origin) + synchronized(this) { + if (bookName != name || bookOrigin != origin) { + bookName = name + bookOrigin = origin + replaceRules = if (origin.isNullOrEmpty()) { + App.db.replaceRuleDao().findEnabledByScope(name) + } else { + App.db.replaceRuleDao().findEnabledByScope(name, origin) + } + } + } replaceRules.forEach { item -> item.pattern.let { if (it.isNotEmpty()) { @@ -293,15 +228,29 @@ object BookHelp { } } } - if (!c.substringBefore("\n").contains(title)) { - c = "$title\n$c" + try { + when (AppConfig.chineseConverterType) { + 1 -> c = HanLP.convertToSimplifiedChinese(c) + 2 -> c = HanLP.convertToTraditionalChinese(c) + } + } catch (e: Exception) { + withContext(Main) { + App.INSTANCE.toast("简繁转换出错") + } } - when (AppConfig.chineseConverterType) { - 1 -> c = ZhConvertBootstrap.newInstance().toSimple(c) - 2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c) + val contents = arrayListOf() + c.split("\n").forEach { + val str = it.replace("^\\s+".toRegex(), "") + .replace("\r", "") + if (contents.isEmpty()) { + contents.add(title) + if (str != title && it.isNotEmpty()) { + contents.add("${ReadBookConfig.bodyIndent}$str") + } + } else if (str.isNotEmpty()) { + contents.add("${ReadBookConfig.bodyIndent}$str") + } } - return c - .replace("\\s*\\n+\\s*".toRegex(), "\n${ReadBookConfig.bodyIndent}") - .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 + return contents } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt index d0e67e901..ff4180081 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -27,7 +27,6 @@ object ReadBookConfig { GSON.fromJsonArray(json)!! } val durConfig get() = getConfig(styleSelect) - private val shareConfig get() = getConfig(5) var bg: Drawable? = null var bgMeanColor: Int = 0 @@ -139,129 +138,145 @@ object ReadBookConfig { var bodyIndent = " ".repeat(bodyIndentCount) var hideStatusBar = App.INSTANCE.getPrefBoolean(PreferKey.hideStatusBar) var hideNavigationBar = App.INSTANCE.getPrefBoolean(PreferKey.hideNavigationBar) + + private val config get() = if (shareLayout) getConfig(5) else durConfig + var textBold: Boolean - get() = if (shareLayout) shareConfig.textBold else durConfig.textBold - set(value) = if (shareLayout) shareConfig.textBold = value else durConfig.textBold = value + get() = config.textBold + set(value) { + config.textBold = value + } var textSize: Int - get() = if (shareLayout) shareConfig.textSize else durConfig.textSize - set(value) = if (shareLayout) shareConfig.textSize = value else durConfig.textSize = value + get() = config.textSize + set(value) { + config.textSize = value + } var letterSpacing: Float - get() = if (shareLayout) shareConfig.letterSpacing else durConfig.letterSpacing - set(value) = - if (shareLayout) shareConfig.letterSpacing = value else durConfig.letterSpacing = value + get() = config.letterSpacing + set(value) { + config.letterSpacing = value + } var lineSpacingExtra: Int - get() = if (shareLayout) shareConfig.lineSpacingExtra else durConfig.lineSpacingExtra - set(value) = - if (shareLayout) shareConfig.lineSpacingExtra = value - else durConfig.lineSpacingExtra = value + get() = config.lineSpacingExtra + set(value) { + config.lineSpacingExtra = value + } var paragraphSpacing: Int - get() = if (shareLayout) shareConfig.paragraphSpacing else durConfig.paragraphSpacing - set(value) = - if (shareLayout) shareConfig.paragraphSpacing = value - else durConfig.paragraphSpacing = value + get() = config.paragraphSpacing + set(value) { + config.paragraphSpacing = value + } var titleMode: Int - get() = if (shareLayout) shareConfig.titleMode else durConfig.titleMode - set(value) = - if (shareLayout) shareConfig.titleMode = value else durConfig.titleMode = value + get() = config.titleMode + set(value) { + config.titleMode = value + } var titleSize: Int - get() = if (shareLayout) shareConfig.titleSize else durConfig.titleSize - set(value) = - if (shareLayout) shareConfig.titleSize = value else durConfig.titleSize = value + get() = config.titleSize + set(value) { + config.titleSize = value + } + var titleTopSpacing: Int - get() = if (shareLayout) shareConfig.titleTopSpacing else durConfig.titleTopSpacing - set(value) = - if (shareLayout) shareConfig.titleTopSpacing = value - else durConfig.titleTopSpacing = value + get() = config.titleTopSpacing + set(value) { + config.titleTopSpacing = value + } + var titleBottomSpacing: Int - get() = if (shareLayout) shareConfig.titleBottomSpacing else durConfig.titleBottomSpacing - set(value) = - if (shareLayout) shareConfig.titleBottomSpacing = value - else durConfig.titleBottomSpacing = value + get() = config.titleBottomSpacing + set(value) { + config.titleBottomSpacing = value + } var paddingBottom: Int - get() = if (shareLayout) shareConfig.paddingBottom else durConfig.paddingBottom - set(value) = - if (shareLayout) shareConfig.paddingBottom = value else durConfig.paddingBottom = value + get() = config.paddingBottom + set(value) { + config.paddingBottom = value + } var paddingLeft: Int - get() = if (shareLayout) shareConfig.paddingLeft else durConfig.paddingLeft - set(value) = - if (shareLayout) shareConfig.paddingLeft = value else durConfig.paddingLeft = value + get() = config.paddingLeft + set(value) { + config.paddingLeft = value + } var paddingRight: Int - get() = if (shareLayout) shareConfig.paddingRight else durConfig.paddingRight - set(value) = - if (shareLayout) shareConfig.paddingRight = value else durConfig.paddingRight = value + get() = config.paddingRight + set(value) { + config.paddingRight = value + } var paddingTop: Int - get() = if (shareLayout) shareConfig.paddingTop else durConfig.paddingTop - set(value) = - if (shareLayout) shareConfig.paddingTop = value else durConfig.paddingTop = value + get() = config.paddingTop + set(value) { + config.paddingTop = value + } var headerPaddingBottom: Int - get() = if (shareLayout) shareConfig.headerPaddingBottom else durConfig.headerPaddingBottom - set(value) = - if (shareLayout) shareConfig.headerPaddingBottom = value - else durConfig.headerPaddingBottom = value + get() = config.headerPaddingBottom + set(value) { + config.headerPaddingBottom = value + } var headerPaddingLeft: Int - get() = if (shareLayout) shareConfig.headerPaddingLeft else durConfig.headerPaddingLeft - set(value) = - if (shareLayout) shareConfig.headerPaddingLeft = value - else durConfig.headerPaddingLeft = value + get() = config.headerPaddingLeft + set(value) { + config.headerPaddingLeft = value + } var headerPaddingRight: Int - get() = if (shareLayout) shareConfig.headerPaddingRight else durConfig.headerPaddingRight - set(value) = - if (shareLayout) shareConfig.headerPaddingRight = value - else durConfig.headerPaddingRight = value + get() = config.headerPaddingRight + set(value) { + config.headerPaddingRight = value + } var headerPaddingTop: Int - get() = if (shareLayout) shareConfig.headerPaddingTop else durConfig.headerPaddingTop - set(value) = - if (shareLayout) shareConfig.headerPaddingTop = value - else durConfig.headerPaddingTop = value + get() = config.headerPaddingTop + set(value) { + config.headerPaddingTop = value + } var footerPaddingBottom: Int - get() = if (shareLayout) shareConfig.footerPaddingBottom else durConfig.footerPaddingBottom - set(value) = - if (shareLayout) shareConfig.footerPaddingBottom = value - else durConfig.footerPaddingBottom = value + get() = config.footerPaddingBottom + set(value) { + config.footerPaddingBottom = value + } var footerPaddingLeft: Int - get() = if (shareLayout) shareConfig.footerPaddingLeft else durConfig.footerPaddingLeft - set(value) = - if (shareLayout) shareConfig.footerPaddingLeft = value - else durConfig.footerPaddingLeft = value + get() = config.footerPaddingLeft + set(value) { + config.footerPaddingLeft = value + } var footerPaddingRight: Int - get() = if (shareLayout) shareConfig.footerPaddingRight else durConfig.footerPaddingRight - set(value) = - if (shareLayout) shareConfig.footerPaddingRight = value - else durConfig.footerPaddingRight = value + get() = config.footerPaddingRight + set(value) { + config.footerPaddingRight = value + } var footerPaddingTop: Int - get() = if (shareLayout) shareConfig.footerPaddingTop else durConfig.footerPaddingTop - set(value) = - if (shareLayout) shareConfig.footerPaddingTop = value - else durConfig.footerPaddingTop = value + get() = config.footerPaddingTop + set(value) { + config.footerPaddingTop = value + } var showHeaderLine: Boolean - get() = if (shareLayout) shareConfig.showHeaderLine else durConfig.showHeaderLine - set(value) = - if (shareLayout) shareConfig.showHeaderLine = value - else durConfig.showHeaderLine = value + get() = config.showHeaderLine + set(value) { + config.showHeaderLine = value + } var showFooterLine: Boolean - get() = if (shareLayout) shareConfig.showFooterLine else durConfig.showFooterLine - set(value) = - if (shareLayout) shareConfig.showFooterLine = value - else durConfig.showFooterLine = value + get() = config.showFooterLine + set(value) { + config.showFooterLine = value + } @Keep class Config( diff --git a/app/src/main/java/io/legado/app/help/ReadTipConfig.kt b/app/src/main/java/io/legado/app/help/ReadTipConfig.kt new file mode 100644 index 000000000..9d6452b38 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/ReadTipConfig.kt @@ -0,0 +1,74 @@ +package io.legado.app.help + +import io.legado.app.App +import io.legado.app.R +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getPrefInt +import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.putPrefInt + +object ReadTipConfig { + val tipArray: Array = App.INSTANCE.resources.getStringArray(R.array.read_tip) + const val none = 0 + const val chapterTitle = 1 + const val time = 2 + const val battery = 3 + const val page = 4 + const val totalProgress = 5 + const val pageAndTotal = 6 + + val tipHeaderLeftStr: String get() = tipArray.getOrElse(tipHeaderLeft) { tipArray[none] } + val tipHeaderMiddleStr: String get() = tipArray.getOrElse(tipHeaderMiddle) { tipArray[none] } + val tipHeaderRightStr: String get() = tipArray.getOrElse(tipHeaderRight) { tipArray[none] } + val tipFooterLeftStr: String get() = tipArray.getOrElse(tipFooterLeft) { tipArray[none] } + val tipFooterMiddleStr: String get() = tipArray.getOrElse(tipFooterMiddle) { tipArray[none] } + val tipFooterRightStr: String get() = tipArray.getOrElse(tipFooterRight) { tipArray[none] } + + var tipHeaderLeft: Int + get() = App.INSTANCE.getPrefInt("tipHeaderLeft", time) + set(value) { + App.INSTANCE.putPrefInt("tipHeaderLeft", value) + } + + var tipHeaderMiddle: Int + get() = App.INSTANCE.getPrefInt("tipHeaderMiddle", none) + set(value) { + App.INSTANCE.putPrefInt("tipHeaderMiddle", value) + } + + var tipHeaderRight: Int + get() = App.INSTANCE.getPrefInt("tipHeaderRight", battery) + set(value) { + App.INSTANCE.putPrefInt("tipHeaderRight", value) + } + + var tipFooterLeft: Int + get() = App.INSTANCE.getPrefInt("tipFooterLeft", chapterTitle) + set(value) { + App.INSTANCE.putPrefInt("tipFooterLeft", value) + } + + var tipFooterMiddle: Int + get() = App.INSTANCE.getPrefInt("tipFooterMiddle", none) + set(value) { + App.INSTANCE.putPrefInt("tipFooterMiddle", value) + } + + var tipFooterRight: Int + get() = App.INSTANCE.getPrefInt("tipFooterRight", pageAndTotal) + set(value) { + App.INSTANCE.putPrefInt("tipFooterRight", value) + } + + var hideHeader: Boolean + get() = App.INSTANCE.getPrefBoolean("hideHeader", true) + set(value) { + App.INSTANCE.putPrefBoolean("hideHeader", value) + } + + var hideFooter: Boolean + get() = App.INSTANCE.getPrefBoolean("hideFooter", false) + set(value) { + App.INSTANCE.putPrefBoolean("hideFooter", value) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt index f501fcb71..4f5cbda46 100644 --- a/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt +++ b/app/src/main/java/io/legado/app/help/http/AjaxWebView.kt @@ -180,6 +180,7 @@ class AjaxWebView { mWebView.get()?.evaluateJavascript(mJavaScript) { if (it.isNotEmpty() && it != "null") { val content = StringEscapeUtils.unescapeJson(it) + .replace("^\"|\"$".toRegex(), "") handler.obtainMessage(MSG_SUCCESS, Res(url, content)) .sendToTarget() handler.removeCallbacks(this) diff --git a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt index df417e2f9..31064e268 100644 --- a/app/src/main/java/io/legado/app/help/http/HttpHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/HttpHelper.kt @@ -3,9 +3,11 @@ package io.legado.app.help.http import io.legado.app.help.http.api.HttpGetApi import io.legado.app.utils.NetworkUtils import kotlinx.coroutines.suspendCancellableCoroutine -import okhttp3.* +import okhttp3.ConnectionSpec +import okhttp3.Interceptor +import okhttp3.OkHttpClient +import okhttp3.Protocol import retrofit2.Retrofit -import java.util.* import java.util.concurrent.TimeUnit import kotlin.coroutines.resume @@ -13,14 +15,12 @@ import kotlin.coroutines.resume object HttpHelper { val client: OkHttpClient by lazy { - val default = ConnectionSpec.Builder(ConnectionSpec.MODERN_TLS) - .tlsVersions(TlsVersion.TLS_1_2) - .build() - val specs = ArrayList() - specs.add(default) - specs.add(ConnectionSpec.COMPATIBLE_TLS) - specs.add(ConnectionSpec.CLEARTEXT) + val specs = arrayListOf( + ConnectionSpec.MODERN_TLS, + ConnectionSpec.COMPATIBLE_TLS, + ConnectionSpec.CLEARTEXT + ) val builder = OkHttpClient.Builder() .connectTimeout(15, TimeUnit.SECONDS) diff --git a/app/src/main/java/io/legado/app/help/storage/Backup.kt b/app/src/main/java/io/legado/app/help/storage/Backup.kt index c47951385..5b1e88186 100644 --- a/app/src/main/java/io/legado/app/help/storage/Backup.kt +++ b/app/src/main/java/io/legado/app/help/storage/Backup.kt @@ -32,9 +32,7 @@ object Backup { val lastBackup = context.getPrefLong(PreferKey.lastBackup) if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { Coroutine.async { - context.getPrefString(PreferKey.backupPath)?.let { - backup(context, it, true) - } + backup(context, context.getPrefString(PreferKey.backupPath) ?: "", true) } } } @@ -71,7 +69,11 @@ object Backup { if (path.isContentPath()) { copyBackup(context, Uri.parse(path), isAuto) } else { - copyBackup(File(path), isAuto) + if (path.isEmpty()) { + copyBackup(context.getExternalFilesDir(null)!!, false) + } else { + copyBackup(File(path), isAuto) + } } } } diff --git a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt index 4af39cbb2..9882e4d4f 100644 --- a/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt +++ b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt @@ -136,14 +136,12 @@ object ImportOldData { return bookSources.size } - - fun importOldReplaceRule(json: String): Int { val replaceRules = mutableListOf() val items: List> = Restore.jsonPath.parse(json).read("$") for (item in items) { val jsonItem = Restore.jsonPath.parse(item) - OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { + OldReplace.jsonToReplaceRule(jsonItem.jsonString())?.let { if (it.isValid()){ replaceRules.add(it) } diff --git a/app/src/main/java/io/legado/app/help/storage/OldReplace.kt b/app/src/main/java/io/legado/app/help/storage/OldReplace.kt new file mode 100644 index 000000000..d658106dc --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/OldReplace.kt @@ -0,0 +1,32 @@ +package io.legado.app.help.storage + +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.utils.* + +object OldReplace { + + fun jsonToReplaceRule(json: String): ReplaceRule? { + var replaceRule: ReplaceRule? = null + runCatching { + replaceRule = GSON.fromJsonObject(json.trim()) + } + runCatching { + if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) { + val jsonItem = Restore.jsonPath.parse(json.trim()) + val rule = ReplaceRule() + rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis() + rule.pattern = jsonItem.readString("$.regex") ?: "" + if (rule.pattern.isEmpty()) return null + rule.name = jsonItem.readString("$.replaceSummary") ?: "" + rule.replacement = jsonItem.readString("$.replacement") ?: "" + rule.isRegex = jsonItem.readBool("$.isRegex") == true + rule.scope = jsonItem.readString("$.useTo") + rule.isEnabled = jsonItem.readBool("$.enable") == true + rule.order = jsonItem.readInt("$.serialNumber") ?: 0 + return rule + } + } + return replaceRule + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/OldRule.kt b/app/src/main/java/io/legado/app/help/storage/OldRule.kt index 7696bb383..f5919eb2c 100644 --- a/app/src/main/java/io/legado/app/help/storage/OldRule.kt +++ b/app/src/main/java/io/legado/app/help/storage/OldRule.kt @@ -1,9 +1,9 @@ package io.legado.app.help.storage +import androidx.annotation.Keep import io.legado.app.constant.AppConst import io.legado.app.constant.BookType import io.legado.app.data.entities.BookSource -import io.legado.app.data.entities.ReplaceRule import io.legado.app.data.entities.rule.* import io.legado.app.help.storage.Restore.jsonPath import io.legado.app.utils.* @@ -14,13 +14,15 @@ object OldRule { private val jsPattern = Pattern.compile("\\{\\{.+?\\}\\}", Pattern.CASE_INSENSITIVE) fun jsonToBookSource(json: String): BookSource? { - var source: BookSource? = null - runCatching { - source = GSON.fromJsonObject(json.trim()) + val source = BookSource() + val sourceAny = try { + GSON.fromJsonObject(json.trim()) + } catch (e: Exception) { + null } runCatching { - if (source == null || source?.searchUrl.isNullOrBlank()) { - source = BookSource().apply { + if (sourceAny?.ruleToc == null) { + source.apply { val jsonItem = jsonPath.parse(json.trim()) bookSourceUrl = jsonItem.readString("bookSourceUrl") ?: "" bookSourceName = jsonItem.readString("bookSourceName") ?: "" @@ -37,7 +39,7 @@ object OldRule { if (exploreUrl.isNullOrBlank()) { enabledExplore = false } - val searchRule = SearchRule( + ruleSearch = SearchRule( bookList = toNewRule(jsonItem.readString("ruleSearchList")), name = toNewRule(jsonItem.readString("ruleSearchName")), author = toNewRule(jsonItem.readString("ruleSearchAuthor")), @@ -47,8 +49,7 @@ object OldRule { coverUrl = toNewRule(jsonItem.readString("ruleSearchCoverUrl")), lastChapter = toNewRule(jsonItem.readString("ruleSearchLastChapter")) ) - ruleSearch = GSON.toJson(searchRule) - val exploreRule = ExploreRule( + ruleExplore = ExploreRule( bookList = toNewRule(jsonItem.readString("ruleFindList")), name = toNewRule(jsonItem.readString("ruleFindName")), author = toNewRule(jsonItem.readString("ruleFindAuthor")), @@ -58,8 +59,7 @@ object OldRule { coverUrl = toNewRule(jsonItem.readString("ruleFindCoverUrl")), lastChapter = toNewRule(jsonItem.readString("ruleFindLastChapter")) ) - ruleExplore = GSON.toJson(exploreRule) - val bookInfoRule = BookInfoRule( + ruleBookInfo = BookInfoRule( init = toNewRule(jsonItem.readString("ruleBookInfoInit")), name = toNewRule(jsonItem.readString("ruleBookName")), author = toNewRule(jsonItem.readString("ruleBookAuthor")), @@ -69,29 +69,89 @@ object OldRule { lastChapter = toNewRule(jsonItem.readString("ruleBookLastChapter")), tocUrl = toNewRule(jsonItem.readString("ruleChapterUrl")) ) - ruleBookInfo = GSON.toJson(bookInfoRule) - val chapterRule = TocRule( + ruleToc = TocRule( chapterList = toNewRule(jsonItem.readString("ruleChapterList")), chapterName = toNewRule(jsonItem.readString("ruleChapterName")), chapterUrl = toNewRule(jsonItem.readString("ruleContentUrl")), nextTocUrl = toNewRule(jsonItem.readString("ruleChapterUrlNext")) ) - ruleToc = GSON.toJson(chapterRule) var content = toNewRule(jsonItem.readString("ruleBookContent")) ?: "" if (content.startsWith("$") && !content.startsWith("$.")) { content = content.substring(1) } - val contentRule = ContentRule( + ruleContent = ContentRule( content = content, nextContentUrl = toNewRule(jsonItem.readString("ruleContentUrlNext")) ) - ruleContent = GSON.toJson(contentRule) + } + } else { + source.bookSourceUrl = sourceAny.bookSourceUrl + source.bookSourceName = sourceAny.bookSourceName + source.bookSourceGroup = sourceAny.bookSourceGroup + source.bookSourceType = sourceAny.bookSourceType + source.bookUrlPattern = sourceAny.bookUrlPattern + source.customOrder = sourceAny.customOrder + source.enabled = sourceAny.enabled + source.enabledExplore = sourceAny.enabledExplore + source.header = sourceAny.header + source.loginUrl = sourceAny.loginUrl + source.lastUpdateTime = sourceAny.lastUpdateTime + source.weight = sourceAny.weight + source.exploreUrl = sourceAny.exploreUrl + source.ruleExplore = if (sourceAny.ruleExplore is String) { + GSON.fromJsonObject(sourceAny.ruleExplore as? String) + } else { + GSON.fromJsonObject(GSON.toJson(sourceAny.ruleExplore)) + } + source.searchUrl = sourceAny.searchUrl + source.ruleSearch = if (sourceAny.ruleSearch is String) { + GSON.fromJsonObject(sourceAny.ruleSearch as? String) + } else { + GSON.fromJsonObject(GSON.toJson(sourceAny.ruleSearch)) + } + source.ruleBookInfo = if (sourceAny.ruleBookInfo is String) { + GSON.fromJsonObject(sourceAny.ruleBookInfo as? String) + } else { + GSON.fromJsonObject(GSON.toJson(sourceAny.ruleBookInfo)) + } + source.ruleToc = if (sourceAny.ruleToc is String) { + GSON.fromJsonObject(sourceAny.ruleToc as? String) + } else { + GSON.fromJsonObject(GSON.toJson(sourceAny.ruleToc)) + } + source.ruleContent = if (sourceAny.ruleContent is String) { + GSON.fromJsonObject(sourceAny.ruleContent as? String) + } else { + GSON.fromJsonObject(GSON.toJson(sourceAny.ruleContent)) } } } return source } + @Keep + data class BookSourceAny( + var bookSourceName: String = "", // 名称 + var bookSourceGroup: String? = null, // 分组 + var bookSourceUrl: String = "", // 地址,包括 http/https + var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频 + var bookUrlPattern: String? = null, // 详情页url正则 + var customOrder: Int = 0, // 手动排序编号 + var enabled: Boolean = true, // 是否启用 + var enabledExplore: Boolean = true, // 启用发现 + var header: String? = null, // 请求头 + var loginUrl: String? = null, // 登录地址 + var lastUpdateTime: Long = 0, // 最后更新时间,用于排序 + var weight: Int = 0, // 智能排序的权重 + var exploreUrl: String? = null, // 发现url + var ruleExplore: Any? = null, // 发现规则 + var searchUrl: String? = null, // 搜索url + var ruleSearch: Any? = null, // 搜索规则 + var ruleBookInfo: Any? = null, // 书籍信息页规则 + var ruleToc: Any? = null, // 目录页规则 + var ruleContent: Any? = null // 正文页规则 + ) + // default规则适配 // #正则#替换内容 替换成 ##正则##替换内容 // | 替换成 || @@ -114,8 +174,8 @@ object OldRule { !newRule.startsWith("//") && !newRule.startsWith("##") && !newRule.startsWith(":") && - !newRule.contains("@js:",true) && - !newRule.contains("",true) + !newRule.contains("@js:", true) && + !newRule.contains("", true) ) { if (newRule.contains("#") && !newRule.contains("##")) { newRule = oldRule.replace("#", "##") @@ -142,10 +202,10 @@ object OldRule { } } if (allinone) { - newRule = "+" + newRule + newRule = "+$newRule" } if (reverse) { - newRule = "-" + newRule + newRule = "-$newRule" } return newRule } @@ -216,27 +276,4 @@ object OldRule { return GSON.toJson(map) } - fun jsonToReplaceRule(json: String): ReplaceRule? { - var replaceRule: ReplaceRule? = null - runCatching { - replaceRule = GSON.fromJsonObject(json.trim()) - } - runCatching { - if (replaceRule == null || replaceRule?.pattern.isNullOrBlank()) { - val jsonItem = jsonPath.parse(json.trim()) - val rule = ReplaceRule() - rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis() - rule.pattern = jsonItem.readString("$.regex") ?: "" - if (rule.pattern.isEmpty()) return null - rule.name = jsonItem.readString("$.replaceSummary") ?: "" - rule.replacement = jsonItem.readString("$.replacement") ?: "" - rule.isRegex = jsonItem.readBool("$.isRegex") == true - rule.scope = jsonItem.readString("$.useTo") - rule.isEnabled = jsonItem.readBool("$.enable") == true - rule.order = jsonItem.readInt("$.serialNumber") ?: 0 - return rule - } - } - return replaceRule - } } diff --git a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt index 47aa59344..d1648c7b7 100644 --- a/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt +++ b/app/src/main/java/io/legado/app/lib/webdav/WebDav.kt @@ -3,12 +3,7 @@ package io.legado.app.lib.webdav import io.legado.app.help.http.HttpHelper import io.legado.app.lib.webdav.http.Handler import io.legado.app.lib.webdav.http.HttpAuth -import okhttp3.Credentials -import okhttp3.MediaType.Companion.toMediaTypeOrNull -import okhttp3.Request -import okhttp3.RequestBody.Companion.asRequestBody -import okhttp3.RequestBody.Companion.toRequestBody -import okhttp3.Response +import okhttp3.* import org.jsoup.Jsoup import java.io.File import java.io.IOException @@ -82,7 +77,7 @@ constructor(urlStr: String) { this.exists = false return false } - response.body?.let { + response.body()?.let { if (it.string().isNotEmpty()) { return true } @@ -102,7 +97,7 @@ constructor(urlStr: String) { fun listFiles(propsList: ArrayList = ArrayList()): List { propFindResponse(propsList)?.let { response -> if (response.isSuccessful) { - response.body?.let { body -> + response.body()?.let { body -> return parseDir(body.string()) } } @@ -127,7 +122,10 @@ constructor(urlStr: String) { .url(url) // 添加RequestBody对象,可以只返回的属性。如果设为null,则会返回全部属性 // 注意:尽量手动指定需要返回的属性。若返回全部属性,可能后由于Prop.java里没有该属性名,而崩溃。 - .method("PROPFIND", requestPropsStr.toRequestBody("text/plain".toMediaTypeOrNull())) + .method( + "PROPFIND", + RequestBody.create(MediaType.parse("text/plain"), requestPropsStr) + ) HttpAuth.auth?.let { request.header( @@ -205,9 +203,9 @@ constructor(urlStr: String) { fun upload(localPath: String, contentType: String? = null): Boolean { val file = File(localPath) if (!file.exists()) return false - val mediaType = contentType?.toMediaTypeOrNull() + val mediaType = contentType?.let { MediaType.parse(it) } // 务必注意RequestBody不要嵌套,不然上传时内容可能会被追加多余的文件信息 - val fileBody = file.asRequestBody(mediaType) + val fileBody = RequestBody.create(mediaType, file) httpUrl?.let { val request = Request.Builder() .url(it) @@ -241,7 +239,7 @@ constructor(urlStr: String) { request.header("Authorization", Credentials.basic(it.user, it.pass)) } try { - return HttpHelper.client.newCall(request.build()).execute().body?.byteStream() + return HttpHelper.client.newCall(request.build()).execute().body()?.byteStream() } catch (e: IOException) { e.printStackTrace() } catch (e: IllegalArgumentException) { diff --git a/app/src/main/java/io/legado/app/model/Debug.kt b/app/src/main/java/io/legado/app/model/Debug.kt index 51ae12afd..58944e05b 100644 --- a/app/src/main/java/io/legado/app/model/Debug.kt +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -54,7 +54,8 @@ object Debug { cancelDebug() debugSource = rssSource.sourceUrl log(debugSource, "︾开始解析") - Rss.getArticles(rssSource, null) + val sort = rssSource.sortUrls().entries.first() + Rss.getArticles(sort.key, sort.value, rssSource, null) .onSuccess { if (it.articles.isEmpty()) { log(debugSource, "⇒列表页解析成功,为空") diff --git a/app/src/main/java/io/legado/app/model/Rss.kt b/app/src/main/java/io/legado/app/model/Rss.kt index 24dadade0..8b29913d9 100644 --- a/app/src/main/java/io/legado/app/model/Rss.kt +++ b/app/src/main/java/io/legado/app/model/Rss.kt @@ -15,6 +15,8 @@ import kotlin.coroutines.CoroutineContext object Rss { fun getArticles( + sortName: String, + sortUrl: String, rssSource: RssSource, pageUrl: String? = null, scope: CoroutineScope = Coroutine.DEFAULT, @@ -22,11 +24,11 @@ object Rss { ): Coroutine { return Coroutine.async(scope, context) { val analyzeUrl = AnalyzeUrl( - pageUrl ?: rssSource.sourceUrl, + pageUrl ?: sortUrl, headerMapF = rssSource.getHeaderMap() ) val body = analyzeUrl.getResponseAwait(rssSource.sourceUrl).body - RssParserByRule.parseXML(body, rssSource) + RssParserByRule.parseXML(sortName, sortUrl, body, rssSource) } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt index 7b1488c96..0e6031755 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeUrl.kt @@ -13,9 +13,8 @@ import io.legado.app.help.http.api.HttpGetApi import io.legado.app.help.http.api.HttpPostApi import io.legado.app.utils.* import okhttp3.FormBody -import okhttp3.MediaType.Companion.toMediaTypeOrNull +import okhttp3.MediaType import okhttp3.RequestBody -import okhttp3.RequestBody.Companion.toRequestBody import retrofit2.Call import java.net.URLEncoder import java.util.* @@ -40,7 +39,7 @@ class AnalyzeUrl( ) : JsExtensions { companion object { private val pagePattern = Pattern.compile("<(.*?)>") - private val jsonType = "application/json; charset=utf-8".toMediaTypeOrNull() + private val jsonType = MediaType.parse("application/json; charset=utf-8") } private var baseUrl: String = "" @@ -52,8 +51,8 @@ class AnalyzeUrl( private var queryStr: String? = null private val fieldMap = LinkedHashMap() private var charset: String? = null - private var bodyTxt: String? = null - private var body: RequestBody? = null + private var body: String? = null + private var requestBody: RequestBody? = null private var method = RequestMethod.GET init { @@ -158,15 +157,30 @@ class AnalyzeUrl( baseUrl = it } if (urlArray.size > 1) { - val options = GSON.fromJsonObject>(urlArray[1]) - options?.let { _ -> - options["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST } - options["headers"]?.let { headers -> - GSON.fromJsonObject>(headers)?.let { headerMap.putAll(it) } + val option = GSON.fromJsonObject(urlArray[1]) + option?.let { _ -> + option.method?.let { if (it.equals("POST", true)) method = RequestMethod.POST } + option.headers?.let { headers -> + if (headers is Map<*, *>) { + @Suppress("unchecked_cast") + headerMap.putAll(headers as Map) + } + if (headers is String) { + GSON.fromJsonObject>(headers) + ?.let { headerMap.putAll(it) } + } + } + charset = option.charset + body = if (option.body is String) { + option.body + } else { + GSON.toJson(option.body) + } + option.webView?.let { + if (it.toString().isNotEmpty()) { + useWebView = true + } } - options["body"]?.let { bodyTxt = it } - options["charset"]?.let { charset = it } - options["webView"]?.let { if (it.isNotEmpty()) useWebView = true } } } when (method) { @@ -180,20 +194,19 @@ class AnalyzeUrl( } } RequestMethod.POST -> { - bodyTxt?.let { + body?.let { if (it.isJson()) { - body = it.toRequestBody(jsonType) + requestBody = RequestBody.create(jsonType, it) } else { analyzeFields(it) } } ?: let { - body = FormBody.Builder().build() + requestBody = FormBody.Builder().build() } } } } - /** * 解析QueryMap */ @@ -254,7 +267,7 @@ class AnalyzeUrl( } else { HttpHelper .getApiService(baseUrl, charset) - .postBody(url, body!!, headerMap) + .postBody(url, requestBody!!, headerMap) } } fieldMap.isEmpty() -> HttpHelper @@ -278,7 +291,7 @@ class AnalyzeUrl( params.requestMethod = method params.javaScript = jsStr params.sourceRegex = sourceRegex - params.postData = bodyTxt?.toByteArray() + params.postData = body?.toByteArray() params.tag = tag return HttpHelper.ajax(params) } @@ -295,7 +308,7 @@ class AnalyzeUrl( } else { HttpHelper .getApiService(baseUrl, charset) - .postBodyAsync(url, body!!, headerMap) + .postBodyAsync(url, requestBody!!, headerMap) } } fieldMap.isEmpty() -> HttpHelper @@ -308,4 +321,12 @@ class AnalyzeUrl( return Res(NetworkUtils.getUrl(res), res.body()) } + data class UrlOption( + val method: String?, + val charset: String?, + val webView: Any?, + val headers: Any?, + val body: Any? + ) + } diff --git a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt b/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt index 9199ae05a..15e8597c2 100644 --- a/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt +++ b/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt @@ -13,32 +13,46 @@ import java.nio.charset.Charset import java.util.regex.Matcher import java.util.regex.Pattern -object AnalyzeTxtFile { - private const val folderName = "bookTxt" - private const val BLANK: Byte = 0x0a - //默认从文件中获取数据的长度 - private const val BUFFER_SIZE = 512 * 1024 - //没有标题的时候,每个章节的最大长度 - private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024 - val cacheFolder: File by lazy { - val rootFile = App.INSTANCE.getExternalFilesDir(null) - ?: App.INSTANCE.externalCacheDir - ?: App.INSTANCE.cacheDir - FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName)) - } +class AnalyzeTxtFile { + + private val tocRules = arrayListOf() + private lateinit var charset: Charset + @Throws(Exception::class) fun analyze(context: Context, book: Book): ArrayList { val bookFile = getBookFile(context, book) book.charset = EncodingDetect.getEncode(bookFile) - val charset = book.fileCharset() - val toc = arrayListOf() + charset = book.fileCharset() + val rulePattern = if (book.tocUrl.isNotEmpty()) { + Pattern.compile(book.tocUrl, Pattern.MULTILINE) + } else { + tocRules.addAll(getTocRules()) + null + } //获取文件流 val bookStream = RandomAccessFile(bookFile, "r") - val rulePattern = getTocRule(book, bookStream, charset) + return analyze(bookStream, book, rulePattern) + } + @Throws(Exception::class) + private fun analyze( + bookStream: RandomAccessFile, + book: Book, + pattern: Pattern? + ): ArrayList { + bookStream.seek(0) + val toc = arrayListOf() + var tocRule: TxtTocRule? = null + val rulePattern = pattern ?: let { + tocRule = getTocRule(bookStream) + tocRule?.let { + Pattern.compile(it.rule, Pattern.MULTILINE) + } + } //加载章节 val buffer = ByteArray(BUFFER_SIZE) //获取到的块起始点,在文件中的位置 + bookStream.seek(0) var curOffset: Long = 0 //block的个数 var blockPos = 0 @@ -47,13 +61,14 @@ object AnalyzeTxtFile { var allLength = 0 //获取文件中的数据到buffer,直到没有数据为止 - while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) { - ++blockPos + while (bookStream.read(buffer).also { length = it } > 0) { + blockPos++ //如果存在Chapter - if (rulePattern != null) { //将数据转换成String + if (rulePattern != null) { + //将数据转换成String, 不能超过length var blockContent = String(buffer, 0, length, charset) val lastN = blockContent.lastIndexOf("\n") - if (lastN != 0) { + if (lastN > 0) { blockContent = blockContent.substring(0, lastN) length = blockContent.toByteArray(charset).size allLength += length @@ -66,40 +81,50 @@ object AnalyzeTxtFile { //如果存在相应章节 while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 val chapterStart = matcher.start() + //获取章节内容 + val chapterContent = blockContent.substring(seekPos, chapterStart) + val chapterLength = chapterContent.toByteArray(charset).size + val lastStart = toc.lastOrNull()?.start ?: 0 + if (curOffset + chapterLength - lastStart > 50000 && pattern == null) { + //移除不匹配的规则 + tocRules.remove(tocRule) + return analyze(bookStream, book, null) + } //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 - //第一种情况一定是序章 第二种情况可能是上一个章节的内容 + //第一种情况一定是序章 第二种情况是上一个章节的内容 if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容 - val chapterContent = blockContent.substring(seekPos, chapterStart) - //设置指针偏移 - seekPos += chapterContent.length - if (toc.size == 0) { //如果当前没有章节,那么就是序章 + if (toc.isEmpty()) { //如果当前没有章节,那么就是序章 //加入简介 - book.intro = chapterContent + if (StringUtils.trim(chapterContent).isNotEmpty()) { + val qyChapter = BookChapter() + qyChapter.title = "前言" + qyChapter.start = 0 + qyChapter.end = chapterLength.toLong() + toc.add(qyChapter) + } //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() - curChapter.start = chapterContent.toByteArray(charset).size.toLong() + curChapter.start = chapterLength.toLong() toc.add(curChapter) } else { //否则就block分割之后,上一个章节的剩余内容 //获取上一章节 val lastChapter = toc.last() //将当前段落添加上一章去 lastChapter.end = - lastChapter.end!! + chapterContent.toByteArray(charset).size + lastChapter.end!! + chapterLength.toLong() //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() curChapter.start = lastChapter.end toc.add(curChapter) } - } else { //是否存在章节 - if (toc.size != 0) { //获取章节内容 - val chapterContent = blockContent.substring(seekPos, matcher.start()) - seekPos += chapterContent.length + } else { + if (toc.isNotEmpty()) { //获取章节内容 //获取上一章节 val lastChapter = toc.last() lastChapter.end = - lastChapter.start!! + chapterContent.toByteArray(charset).size + lastChapter.start!! + chapterContent.toByteArray(charset).size.toLong() //创建当前章节 val curChapter = BookChapter() curChapter.title = matcher.group() @@ -108,11 +133,18 @@ object AnalyzeTxtFile { } else { //如果章节不存在则创建章节 val curChapter = BookChapter() curChapter.title = matcher.group() - curChapter.start = 0L - curChapter.end = 0L + curChapter.start = 0 + curChapter.end = 0 toc.add(curChapter) } } + //设置指针偏移 + seekPos += chapterContent.length + } + if (seekPos == 0 && length > 50000 && pattern == null) { + //移除不匹配的规则 + tocRules.remove(tocRule) + return analyze(bookStream, book, null) } } else { //进行本地虚拟分章 //章节在buffer的偏移量 @@ -156,7 +188,8 @@ object AnalyzeTxtFile { //block的偏移点 curOffset += length.toLong() - if (rulePattern != null) { //设置上一章的结尾 + if (rulePattern != null) { + //设置上一章的结尾 val lastChapter = toc.last() lastChapter.end = curOffset } @@ -175,44 +208,18 @@ object AnalyzeTxtFile { bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "") } book.latestChapterTitle = toc.last().title + book.totalChapterNum = toc.size System.gc() System.runFinalization() - return toc - } - - fun getContent(book: Book, bookChapter: BookChapter): String { - val bookFile = getBookFile(App.INSTANCE, book) - //获取文件流 - val bookStream = RandomAccessFile(bookFile, "r") - bookStream.seek(bookChapter.start ?: 0) - val extent = (bookChapter.end!! - bookChapter.start!!).toInt() - val content = ByteArray(extent) - bookStream.read(content, 0, extent) - return String(content, book.fileCharset()) - } - - private fun getBookFile(context: Context, book: Book): File { - if (book.bookUrl.isContentPath()) { - val uri = Uri.parse(book.bookUrl) - val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf()) - if (!bookFile.exists()) { - bookFile.createNewFile() - DocumentUtils.readBytes(context, uri)?.let { - bookFile.writeBytes(it) - } - } - return bookFile + tocRule?.let { + book.tocUrl = it.rule } - return File(book.bookUrl) + return toc } - private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? { - if (book.tocUrl.isNotEmpty()) { - return Pattern.compile(book.tocUrl, Pattern.MULTILINE) - } - val tocRules = getTocRules() - var rulePattern: Pattern? = null + private fun getTocRule(bookStream: RandomAccessFile): TxtTocRule? { + var txtTocRule: TxtTocRule? = null //首先获取128k的数据 val buffer = ByteArray(BUFFER_SIZE / 4) val length = bookStream.read(buffer, 0, buffer.size) @@ -221,30 +228,72 @@ object AnalyzeTxtFile { val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE) val matcher = pattern.matcher(content) if (matcher.find()) { - book.tocUrl = tocRule.rule - rulePattern = pattern + txtTocRule = tocRule break } } - bookStream.seek(0) - return rulePattern + + return txtTocRule } - private fun getTocRules(): List { - val rules = App.db.txtTocRule().all - if (rules.isEmpty()) { - return getDefaultRules() + companion object { + private const val folderName = "bookTxt" + private const val BLANK: Byte = 0x0a + + //默认从文件中获取数据的长度 + private const val BUFFER_SIZE = 512 * 1024 + + //没有标题的时候,每个章节的最大长度 + private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024 + val cacheFolder: File by lazy { + val rootFile = App.INSTANCE.getExternalFilesDir(null) + ?: App.INSTANCE.externalCacheDir + ?: App.INSTANCE.cacheDir + FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName)) + } + + fun getContent(book: Book, bookChapter: BookChapter): String { + val bookFile = getBookFile(App.INSTANCE, book) + //获取文件流 + val bookStream = RandomAccessFile(bookFile, "r") + val content = ByteArray((bookChapter.end!! - bookChapter.start!!).toInt()) + bookStream.seek(bookChapter.start!!) + bookStream.read(content) + return String(content, book.fileCharset()) } - return rules - } - fun getDefaultRules(): List { - App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> - GSON.fromJsonArray(String(byteArray))?.let { - App.db.txtTocRule().insert(*it.toTypedArray()) - return it + private fun getBookFile(context: Context, book: Book): File { + if (book.bookUrl.isContentPath()) { + val uri = Uri.parse(book.bookUrl) + val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf()) + if (!bookFile.exists()) { + bookFile.createNewFile() + DocumentUtils.readBytes(context, uri)?.let { + bookFile.writeBytes(it) + } + } + return bookFile + } + return File(book.bookUrl) + } + + private fun getTocRules(): List { + val rules = App.db.txtTocRule().all + if (rules.isEmpty()) { + return getDefaultRules() } + return rules + } + + fun getDefaultRules(): List { + App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> + GSON.fromJsonArray(String(byteArray))?.let { + App.db.txtTocRule().insert(*it.toTypedArray()) + return it + } + } + return emptyList() } - return emptyList() } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/rss/RssParser.kt b/app/src/main/java/io/legado/app/model/rss/RssParser.kt index 7bda16e6d..09cd756bb 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParser.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParser.kt @@ -11,7 +11,7 @@ import java.io.StringReader object RssParser { @Throws(XmlPullParserException::class, IOException::class) - fun parseXML(xml: String, sourceUrl: String): Result { + fun parseXML(sortName: String, xml: String, sourceUrl: String): Result { val articleList = mutableListOf() var currentArticle = RssArticle() @@ -87,6 +87,7 @@ object RssParser { // The item is correctly parsed insideItem = false currentArticle.origin = sourceUrl + currentArticle.sort = sortName articleList.add(currentArticle) currentArticle = RssArticle() } diff --git a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt index b06950031..cd90b6b1d 100644 --- a/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt +++ b/app/src/main/java/io/legado/app/model/rss/RssParserByRule.kt @@ -13,7 +13,7 @@ import io.legado.app.utils.NetworkUtils object RssParserByRule { @Throws(Exception::class) - fun parseXML(body: String?, rssSource: RssSource): Result { + fun parseXML(sortName: String, sortUrl: String, body: String?, rssSource: RssSource): Result { val sourceUrl = rssSource.sourceUrl var nextUrl: String? = null if (body.isNullOrBlank()) { @@ -28,11 +28,11 @@ object RssParserByRule { var ruleArticles = rssSource.ruleArticles if (ruleArticles.isNullOrBlank()) { Debug.log(sourceUrl, "⇒列表规则为空, 使用默认规则解析") - return RssParser.parseXML(body, sourceUrl) + return RssParser.parseXML(sortName, body, sourceUrl) } else { val articleList = mutableListOf() val analyzeRule = AnalyzeRule() - analyzeRule.setContent(body, rssSource.sourceUrl) + analyzeRule.setContent(body, sortUrl) var reverse = false if (ruleArticles.startsWith("-")) { reverse = true @@ -45,7 +45,7 @@ object RssParserByRule { Debug.log(sourceUrl, "┌获取下一页链接") nextUrl = analyzeRule.getString(rssSource.ruleNextPage) if (nextUrl.isNotEmpty()) { - nextUrl = NetworkUtils.getAbsoluteURL(sourceUrl, nextUrl) + nextUrl = NetworkUtils.getAbsoluteURL(sortUrl, nextUrl) } Debug.log(sourceUrl, "└$nextUrl") } @@ -59,7 +59,8 @@ object RssParserByRule { sourceUrl, item, analyzeRule, index == 0, ruleTitle, rulePubDate, ruleDescription, ruleImage, ruleLink )?.let { - it.origin = rssSource.sourceUrl + it.sort = sortName + it.origin = sourceUrl articleList.add(it) } } diff --git a/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt index 8559351f4..78a139345 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt @@ -193,6 +193,7 @@ object BookChapterList { list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle if (book.totalChapterNum < list.size) { book.lastCheckCount = list.size - book.totalChapterNum + book.latestChapterTime = System.currentTimeMillis() } book.totalChapterNum = list.size return list diff --git a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt index 873b6b3a9..d83ffb832 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookContent.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookContent.kt @@ -101,6 +101,7 @@ object BookContent { } } + content.deleteCharAt(content.length - 1) Debug.log(bookSource.bookSourceUrl, "┌获取章节名称") Debug.log(bookSource.bookSourceUrl, "└${bookChapter.title}") Debug.log(bookSource.bookSourceUrl, "┌获取正文内容") diff --git a/app/src/main/java/io/legado/app/model/webBook/BookList.kt b/app/src/main/java/io/legado/app/model/webBook/BookList.kt index 372a4eec6..162db6dd0 100644 --- a/app/src/main/java/io/legado/app/model/webBook/BookList.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookList.kt @@ -4,6 +4,7 @@ import io.legado.app.App import io.legado.app.R import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook +import io.legado.app.data.entities.rule.BookListRule import io.legado.app.help.BookHelp import io.legado.app.model.Debug import io.legado.app.model.analyzeRule.AnalyzeRule @@ -47,7 +48,7 @@ object BookList { } val collections: List var reverse = false - val bookListRule = when { + val bookListRule: BookListRule = when { isSearch -> bookSource.getSearchRule() bookSource.getExploreRule().bookList.isNullOrBlank() -> bookSource.getSearchRule() else -> bookSource.getExploreRule() diff --git a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt index 00c9d0522..fb1f5df59 100644 --- a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt @@ -8,9 +8,14 @@ import io.legado.app.App import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.help.ActivityHelp +import io.legado.app.service.AudioPlayService +import io.legado.app.service.BaseReadAloudService +import io.legado.app.service.help.AudioPlay +import io.legado.app.service.help.ReadAloud import io.legado.app.ui.audio.AudioPlayActivity import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.main.MainActivity +import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main @@ -50,11 +55,23 @@ class MediaButtonReceiver : BroadcastReceiver() { private fun readAloud(context: Context) { when { + BaseReadAloudService.isRun -> if (BaseReadAloudService.isPlay()) { + ReadAloud.pause(context) + AudioPlay.pause(context) + } else { + ReadAloud.resume(context) + AudioPlay.resume(context) + } + AudioPlayService.isRun -> if (AudioPlayService.pause) { + AudioPlay.resume(context) + } else { + AudioPlay.pause(context) + } ActivityHelp.isExist(AudioPlayActivity::class.java) -> postEvent(EventBus.MEDIA_BUTTON, true) ActivityHelp.isExist(ReadBookActivity::class.java) -> postEvent(EventBus.MEDIA_BUTTON, true) - else -> { + else -> if (context.getPrefBoolean("mediaButtonOnExit", true)) { GlobalScope.launch(Main) { val lastBook: Book? = withContext(IO) { App.db.bookDao().lastReadBook diff --git a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt index 1a4de9e6f..6dee23a92 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -44,7 +44,7 @@ abstract class BaseReadAloudService : BaseService(), private lateinit var audioManager: AudioManager private var mFocusRequest: AudioFocusRequest? = null private var broadcastReceiver: BroadcastReceiver? = null - private var mediaSessionCompat: MediaSessionCompat? = null + private lateinit var mediaSessionCompat: MediaSessionCompat private var title: String = "" private var subtitle: String = "" internal val contentList = arrayListOf() @@ -59,6 +59,7 @@ abstract class BaseReadAloudService : BaseService(), isRun = true audioManager = getSystemService(Context.AUDIO_SERVICE) as AudioManager mFocusRequest = MediaHelp.getFocusRequest(this) + mediaSessionCompat = MediaSessionCompat(this, "readAloud") initMediaSession() initBroadcastReceiver() upNotification() @@ -72,7 +73,7 @@ abstract class BaseReadAloudService : BaseService(), unregisterReceiver(broadcastReceiver) postEvent(EventBus.ALOUD_STATE, Status.STOP) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) - mediaSessionCompat?.release() + mediaSessionCompat.release() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { @@ -109,11 +110,9 @@ abstract class BaseReadAloudService : BaseService(), readAloudNumber = textChapter.getReadLength(pageIndex) contentList.clear() if (getPrefBoolean(PreferKey.readAloudByPage)) { - for (index in pageIndex..textChapter.lastIndex()) { + for (index in pageIndex..textChapter.lastIndex) { textChapter.page(index)?.text?.split("\n")?.let { - if (it.isNotEmpty()) { - contentList.addAll(it) - } + contentList.addAll(it) } } } else { @@ -204,7 +203,7 @@ abstract class BaseReadAloudService : BaseService(), * 更新媒体状态 */ private fun upMediaSessionPlaybackState(state: Int) { - mediaSessionCompat?.setPlaybackState( + mediaSessionCompat.setPlaybackState( PlaybackStateCompat.Builder() .setActions(MediaHelp.MEDIA_SESSION_ACTIONS) .setState(state, nowSpeak.toLong(), 1f) @@ -216,13 +215,12 @@ abstract class BaseReadAloudService : BaseService(), * 初始化MediaSession, 注册多媒体按钮 */ private fun initMediaSession() { - mediaSessionCompat = MediaSessionCompat(this, "readAloud") - mediaSessionCompat?.setCallback(object : MediaSessionCompat.Callback() { + mediaSessionCompat.setCallback(object : MediaSessionCompat.Callback() { override fun onMediaButtonEvent(mediaButtonEvent: Intent): Boolean { return MediaButtonReceiver.handleIntent(this@BaseReadAloudService, mediaButtonEvent) } }) - mediaSessionCompat?.setMediaButtonReceiver( + mediaSessionCompat.setMediaButtonReceiver( PendingIntent.getBroadcast( this, 0, @@ -235,7 +233,7 @@ abstract class BaseReadAloudService : BaseService(), PendingIntent.FLAG_CANCEL_CURRENT ) ) - mediaSessionCompat?.isActive = true + mediaSessionCompat.isActive = true } /** @@ -325,7 +323,7 @@ abstract class BaseReadAloudService : BaseService(), ) builder.setStyle( androidx.media.app.NotificationCompat.MediaStyle() - .setMediaSession(mediaSessionCompat?.sessionToken) + .setMediaSession(mediaSessionCompat.sessionToken) .setShowActionsInCompactView(0, 1, 2) ) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) diff --git a/app/src/main/java/io/legado/app/service/CheckSourceService.kt b/app/src/main/java/io/legado/app/service/CheckSourceService.kt index 336dfb18d..896e7bb75 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -7,20 +7,22 @@ import io.legado.app.R import io.legado.app.base.BaseService import io.legado.app.constant.AppConst import io.legado.app.constant.IntentAction +import io.legado.app.data.entities.BookSource import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp -import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.model.WebBook import io.legado.app.ui.book.source.manage.BookSourceActivity import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher import org.jetbrains.anko.toast import java.util.concurrent.Executors +import kotlin.math.min class CheckSourceService : BaseService() { - private val threadCount = AppConfig.threadCount + private var threadCount = AppConfig.threadCount private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() - private var task: Coroutine<*>? = null + private var tasks = CompositeCoroutine() private val allIds = ArrayList() private val checkedIds = ArrayList() private var processIndex = 0 @@ -42,52 +44,70 @@ class CheckSourceService : BaseService() { override fun onDestroy() { super.onDestroy() - task?.cancel() + tasks.clear() searchPool.close() } private fun check(ids: List) { - task?.cancel() + if (allIds.isNotEmpty()) { + toast("已有书源在校验,等完成后再试") + return + } + tasks.clear() allIds.clear() checkedIds.clear() allIds.addAll(ids) processIndex = 0 + threadCount = min(allIds.size, threadCount) updateNotification(0, getString(R.string.progress_show, 0, allIds.size)) - task = execute { - for (i in 0 until threadCount) { - check() - } - }.onError { - toast("校验书源出错:${it.localizedMessage}") + for (i in 0 until threadCount) { + check() } } - + /** + * 检测 + */ private fun check() { + val index = processIndex synchronized(this) { processIndex++ } - if (processIndex < allIds.size) { - val sourceUrl = allIds[processIndex] - App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> - val webBook = WebBook(source) - webBook.searchBook("我的", scope = this, context = searchPool) - .onError(IO) { - source.addGroup("失效") - App.db.bookSourceDao().update(source) - }.onFinally(IO) { - check() - checkedIds.add(sourceUrl) - updateNotification( - checkedIds.size, - getString(R.string.progress_show, checkedIds.size, allIds.size) - ) - synchronized(this) { - if (processIndex >= allIds.size + threadCount - 1) { - stopSelf() - } - } + execute { + if (index < allIds.size) { + val sourceUrl = allIds[index] + App.db.bookSourceDao().getBookSource(sourceUrl)?.let { source -> + if (source.searchUrl.isNullOrEmpty()) { + onNext(sourceUrl) + } else { + check(source) } + } ?: onNext(sourceUrl) + } + } + } + + private fun check(source: BookSource) { + val webBook = WebBook(source) + tasks.add(webBook.searchBook("我的", scope = this, context = searchPool) + .onError(IO) { + source.addGroup("失效") + App.db.bookSourceDao().update(source) + }.onFinally(IO) { + onNext(source.bookSourceUrl) + }) + } + + private fun onNext(sourceUrl: String) { + synchronized(this) { + check() + checkedIds.add(sourceUrl) + updateNotification( + checkedIds.size, + getString(R.string.progress_show, checkedIds.size, allIds.size) + ) + if (processIndex >= allIds.size + threadCount - 1) { + stopSelf() } } } diff --git a/app/src/main/java/io/legado/app/service/DownloadService.kt b/app/src/main/java/io/legado/app/service/DownloadService.kt index dd48933e8..bf6b90629 100644 --- a/app/src/main/java/io/legado/app/service/DownloadService.kt +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -13,6 +13,7 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp import io.legado.app.help.IntentHelp +import io.legado.app.help.coroutine.CompositeCoroutine import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import io.legado.app.utils.postEvent @@ -25,11 +26,11 @@ import java.util.concurrent.Executors class DownloadService : BaseService() { private var searchPool = Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() - private var tasks: ArrayList> = arrayListOf() + private var tasks = CompositeCoroutine() private val handler = Handler() private var runnable: Runnable = Runnable { upDownload() } private val downloadMap = hashMapOf>() - private val downloadCount = hashMapOf(); + private val downloadCount = hashMapOf() private val finalMap = hashMapOf>() private var notificationContent = "正在启动下载" @@ -125,11 +126,7 @@ class DownloadService : BaseService() { chapter, scope = this, context = searchPool - ) - //.onStart { - // notificationContent = "启动:" + chapter.title - //} - .onSuccess(IO) { content -> + ).onSuccess(IO) { content -> downloadCount[entry.key]?.increaseSuccess() BookHelp.saveContent(book, chapter, content) } @@ -165,7 +162,7 @@ class DownloadService : BaseService() { tasks.add(task) task.invokeOnCompletion { tasks.remove(task) - if (tasks.isEmpty()) { + if (tasks.isEmpty) { stopSelf() } } @@ -192,17 +189,21 @@ class DownloadService : BaseService() { val notification = builder.build() startForeground(AppConst.notificationIdDownload, notification) } -} -class DownloadCount{ - @Volatile public var downloadFinishedCount = 0 // 下载完成的条目数量 - @Volatile public var successCount = 0 //下载成功的条目数量 - fun increaseSuccess(){ - ++successCount; - } + class DownloadCount { + @Volatile + var downloadFinishedCount = 0 // 下载完成的条目数量 + + @Volatile + var successCount = 0 //下载成功的条目数量 - fun increaseFinished(){ - ++downloadFinishedCount; + fun increaseSuccess() { + ++successCount + } + + fun increaseFinished() { + ++downloadFinishedCount + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt index 1f63dac89..0517ca2fc 100644 --- a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -1,7 +1,6 @@ package io.legado.app.service import android.app.PendingIntent -import android.os.Build import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import io.legado.app.R @@ -20,35 +19,46 @@ import java.util.* class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener { companion object { - var textToSpeech: TextToSpeech? = null + private var textToSpeech: TextToSpeech? = null + private var ttsInitFinish = false fun clearTTS() { - textToSpeech?.stop() - textToSpeech?.shutdown() + textToSpeech?.let { + it.stop() + it.shutdown() + } textToSpeech = null + ttsInitFinish = false } } - private var ttsInitFinish = false + private val ttsUtteranceListener = TTSUtteranceListener() override fun onCreate() { super.onCreate() - textToSpeech = TextToSpeech(this, this) + initTts() upSpeechRate() } + private fun initTts() { + ttsInitFinish = false + textToSpeech = TextToSpeech(this, this).apply { + setOnUtteranceProgressListener(ttsUtteranceListener) + } + } + override fun onDestroy() { super.onDestroy() clearTTS() } - @Synchronized override fun onInit(status: Int) { if (status == TextToSpeech.SUCCESS) { - textToSpeech?.language = Locale.CHINA - textToSpeech?.setOnUtteranceProgressListener(TTSUtteranceListener()) - ttsInitFinish = true - play() + textToSpeech?.let { + it.language = Locale.CHINA + ttsInitFinish = true + play() + } } else { launch { toast(R.string.tts_init_failed) @@ -58,35 +68,21 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener @Synchronized override fun play() { - if (contentList.isEmpty() || !ttsInitFinish) { - return - } - if (requestFocus()) { + if (contentList.isNotEmpty() && ttsInitFinish && requestFocus()) { MediaHelp.playSilentSound(this) super.play() + textToSpeech?.stop() for (i in nowSpeak until contentList.size) { - if (i == 0) { - speak(contentList[i], TextToSpeech.QUEUE_FLUSH, AppConst.APP_TAG + i) - } else { - speak(contentList[i], TextToSpeech.QUEUE_ADD, AppConst.APP_TAG + i) - } + textToSpeech?.speak( + contentList[i], + TextToSpeech.QUEUE_ADD, + null, + AppConst.APP_TAG + i + ) } } } - private fun speak(content: String, queueMode: Int, utteranceId: String) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - textToSpeech?.speak(content, queueMode, null, utteranceId) - } else { - @Suppress("DEPRECATION") - textToSpeech?.speak( - content, - queueMode, - hashMapOf(Pair(TextToSpeech.Engine.KEY_PARAM_UTTERANCE_ID, utteranceId)) - ) - } - } - /** * 更新朗读速度 */ @@ -94,7 +90,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener if (this.getPrefBoolean("ttsFollowSys", true)) { if (reset) { clearTTS() - textToSpeech = TextToSpeech(this, this) + initTts() } } else { textToSpeech?.setSpeechRate((AppConfig.ttsSpeechRate + 5) / 10f) @@ -152,8 +148,8 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener pageIndex++ ReadBook.moveToNextPage() } + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } - postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } override fun onDone(s: String) { diff --git a/app/src/main/java/io/legado/app/service/help/ReadBook.kt b/app/src/main/java/io/legado/app/service/help/ReadBook.kt index f48f13d11..b7338596c 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadBook.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadBook.kt @@ -36,7 +36,7 @@ object ReadBook { var msg: String? = null private val loadingChapters = arrayListOf() - fun resetData(book: Book, noSource: (name: String, author: String) -> Unit) { + fun resetData(book: Book) { this.book = book titleDate.postValue(book.name) durChapterIndex = book.durChapterIndex @@ -46,20 +46,19 @@ object ReadBook { prevTextChapter = null curTextChapter = null nextTextChapter = null - upWebBook(book, noSource) + upWebBook(book) } - fun upWebBook(book: Book?, noSource: (name: String, author: String) -> Unit) { + fun upWebBook(book: Book?) { book ?: return - if (book.origin == BookType.local) { - webBook = null + webBook = if (book.origin == BookType.local) { + null } else { val bookSource = App.db.bookSourceDao().getBookSource(book.origin) if (bookSource != null) { - webBook = WebBook(bookSource) + WebBook(bookSource) } else { - webBook = null - noSource.invoke(book.name, book.author) + null } } } @@ -107,7 +106,7 @@ object ReadBook { fun moveToPrevChapter(upContent: Boolean, toLast: Boolean = true): Boolean { if (durChapterIndex > 0) { - durPageIndex = if (toLast) prevTextChapter?.lastIndex() ?: 0 else 0 + durPageIndex = if (toLast) prevTextChapter?.lastIndex ?: 0 else 0 durChapterIndex-- nextTextChapter = curTextChapter curTextChapter = prevTextChapter @@ -176,10 +175,10 @@ object ReadBook { fun durChapterPos(): Int { curTextChapter?.let { - if (durPageIndex < it.pageSize()) { + if (durPageIndex < it.pageSize) { return durPageIndex } - return it.pageSize() - 1 + return it.pageSize - 1 } return durPageIndex } @@ -293,7 +292,7 @@ object ReadBook { ) { Coroutine.async { if (chapter.index in durChapterIndex - 1..durChapterIndex + 1) { - val c = BookHelp.disposeContent( + val contents = BookHelp.disposeContent( chapter.title, book!!.name, webBook?.bookSource?.bookSourceUrl, @@ -302,18 +301,21 @@ object ReadBook { ) when (chapter.index) { durChapterIndex -> { - curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) + curTextChapter = + ChapterProvider.getTextChapter(chapter, contents, chapterSize) if (upContent) callBack?.upContent(resetPageOffset = resetPageOffset) callBack?.upView() curPageChanged() callBack?.contentLoadFinish() } durChapterIndex - 1 -> { - prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) + prevTextChapter = + ChapterProvider.getTextChapter(chapter, contents, chapterSize) if (upContent) callBack?.upContent(-1, resetPageOffset) } durChapterIndex + 1 -> { - nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) + nextTextChapter = + ChapterProvider.getTextChapter(chapter, contents, chapterSize) if (upContent) callBack?.upContent(1, resetPageOffset) } } @@ -331,7 +333,7 @@ object ReadBook { book.durChapterTime = System.currentTimeMillis() book.durChapterIndex = durChapterIndex book.durChapterPos = durPageIndex - curTextChapter?.let { + App.db.bookChapterDao().getChapter(book.bookUrl, durChapterIndex)?.let { book.durChapterTitle = it.title } App.db.bookDao().update(book) diff --git a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt index d86cb39a1..1ce1202de 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutActivity.kt @@ -26,9 +26,10 @@ class AboutActivity : BaseActivity(R.layout.activity_about) { tv_app_summary.post { val span = ForegroundColorSpan(accentColor) val spannableString = SpannableString(tv_app_summary.text) - val start = spannableString.indexOf("开源阅读软件") + val gzh = getString(R.string.legado_gzh) + val start = spannableString.indexOf(gzh) spannableString.setSpan( - span, start, start + 6, + span, start, start + gzh.length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE ) tv_app_summary.text = spannableString diff --git a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt index 365381fbd..3b56737cf 100644 --- a/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/AboutFragment.kt @@ -22,6 +22,7 @@ class AboutFragment : PreferenceFragmentCompat() { private val qqGroups = linkedMapOf( Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"), Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"), + Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"), Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"), Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"), Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"), diff --git a/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt b/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt index f82fa1b64..b716c8f05 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt @@ -35,7 +35,7 @@ class DonateFragment : PreferenceFragmentCompat() { "zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl) "qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl) "zfbHbSsm" -> getZfbHb(requireContext()) - "gzGzh" -> requireContext().sendToClip("开源阅读软件") + "gzGzh" -> requireContext().sendToClip("开源阅读") } return super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt b/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt index 8f0cd9fa4..124fd710f 100644 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/audio/AudioPlayViewModel.kt @@ -11,7 +11,6 @@ import io.legado.app.help.BookHelp import io.legado.app.model.WebBook import io.legado.app.service.help.AudioPlay import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.withContext class AudioPlayViewModel(application: Application) : BaseViewModel(application) { @@ -93,9 +92,6 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application) AudioPlay.book?.let { book1.order = it.order App.db.bookDao().delete(it) - } - withContext(Dispatchers.Main) { - } App.db.bookDao().insert(book1) AudioPlay.book = book1 diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt index e7d78a6f4..491922cb1 100644 --- a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity +import io.legado.app.constant.AppConst import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup @@ -105,10 +106,10 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi booksLiveData?.removeObservers(this) booksLiveData = when (groupId) { - -1 -> App.db.bookDao().observeAll() - -2 -> App.db.bookDao().observeLocal() - -3 -> App.db.bookDao().observeAudio() - -11 -> App.db.bookDao().observeNoGroup() + AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll() + AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal() + AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio() + AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() else -> App.db.bookDao().observeByGroup(groupId) } booksLiveData?.observe(this, Observer { list -> @@ -129,22 +130,22 @@ class ArrangeBookActivity : VMBaseActivity(R.layout.activi .show(supportFragmentManager, "groupManage") R.id.menu_no_group -> { title_bar.subtitle = getString(R.string.no_group) - groupId = -11 + groupId = AppConst.bookGroupNone.groupId initBookData() } R.id.menu_all -> { title_bar.subtitle = item.title - groupId = -1 + groupId = AppConst.bookGroupAll.groupId initBookData() } R.id.menu_local -> { title_bar.subtitle = item.title - groupId = -2 + groupId = AppConst.bookGroupLocal.groupId initBookData() } R.id.menu_audio -> { title_bar.subtitle = item.title - groupId = -3 + groupId = AppConst.bookGroupAudio.groupId initBookData() } else -> if (item.groupId == R.id.menu_group) { diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt index 132858e0f..3a148a2c2 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceAdapter.kt @@ -2,6 +2,8 @@ package io.legado.app.ui.book.changesource import android.content.Context import android.os.Bundle +import android.view.View +import androidx.appcompat.widget.PopupMenu import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -10,6 +12,7 @@ import io.legado.app.utils.invisible import io.legado.app.utils.visible import kotlinx.android.synthetic.main.item_change_source.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.sdk27.listeners.onLongClick class ChangeSourceAdapter(context: Context, val callBack: CallBack) : @@ -43,10 +46,31 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) : callBack.changeTo(it) } } + holder.itemView.onLongClick { + showMenu(holder.itemView, getItem(holder.layoutPosition)) + true + } + } + + private fun showMenu(view: View, searchBook: SearchBook?) { + searchBook ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.change_source_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_disable_book_source -> { + callBack.disableSource(searchBook) + removeItem(searchBook) + } + } + true + } + popupMenu.show() } interface CallBack { val bookUrl: String? fun changeTo(searchBook: SearchBook) + fun disableSource(searchBook: SearchBook) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt index bb60c19d4..09fa066ec 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceDialog.kt @@ -185,6 +185,10 @@ class ChangeSourceDialog : BaseDialogFragment(), override val bookUrl: String? get() = callBack?.oldBook?.bookUrl + override fun disableSource(searchBook: SearchBook) { + viewModel.disableSource(searchBook) + } + interface CallBack { val oldBook: Book? fun changeTo(book: Book) diff --git a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt index b375aed11..e5adf9e85 100644 --- a/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/changesource/ChangeSourceViewModel.kt @@ -165,4 +165,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio searchPool.close() } + fun disableSource(searchBook: SearchBook) { + execute { + App.db.bookSourceDao().getBookSource(searchBook.origin)?.let { source -> + source.enabled = false + App.db.bookSourceDao().update(source) + } + searchBooks.remove(searchBook) + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt index d717cb25f..104f78e23 100644 --- a/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt +++ b/app/src/main/java/io/legado/app/ui/book/chapterlist/ChapterListFragment.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.chapterlist +import android.annotation.SuppressLint import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle @@ -13,7 +14,7 @@ import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp -import io.legado.app.lib.theme.backgroundColor +import io.legado.app.lib.theme.bottomBackground import io.legado.app.ui.widget.recycler.UpLinearLayoutManager import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.getViewModelOfActivity @@ -54,7 +55,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme } private fun initView() { - ll_chapter_base_info.setBackgroundColor(backgroundColor) + ll_chapter_base_info.setBackgroundColor(bottomBackground) iv_chapter_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } iv_chapter_bottom.onClick { if (adapter.itemCount > 0) { @@ -66,6 +67,7 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme } } + @SuppressLint("SetTextI18n") private fun initBook() { launch { withContext(IO) { @@ -74,7 +76,8 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme initDoc() book?.let { durChapterIndex = it.durChapterIndex - tv_current_chapter_info.text = it.durChapterTitle + tv_current_chapter_info.text = + "${it.durChapterTitle}(${it.durChapterIndex + 1}/${it.totalChapterNum})" initCacheFileNames(it) } } diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt index 60010f46d..a5d719b57 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt @@ -99,6 +99,8 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { .isChecked = AppConfig.bookGroupLocalShow it.findItem(R.id.menu_group_audio) .isChecked = AppConfig.bookGroupAudioShow + it.findItem(R.id.menu_group_none) + .isChecked = AppConfig.bookGroupNoneShow } } @@ -120,6 +122,10 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { AppConfig.bookGroupAudioShow = item.isChecked callBack?.upGroup() } + R.id.menu_group_none -> { + item.isChecked = !item.isChecked + AppConfig.bookGroupNoneShow = item.isChecked + } } return true } diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt index 29df624e7..9440a7c46 100644 --- a/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt @@ -94,8 +94,7 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { tool_bar.inflateMenu(R.menu.book_group_manage) tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) tool_bar.setOnMenuItemClickListener(this) - tool_bar.menu.findItem(R.id.menu_group_local).isVisible = false - tool_bar.menu.findItem(R.id.menu_group_audio).isVisible = false + tool_bar.menu.setGroupVisible(R.id.menu_groups, false) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) recycler_view.addItemDecoration(VerticalDivider(requireContext())) diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt index e44018ee7..47dc6c33f 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoViewModel.kt @@ -81,7 +81,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { ) { execute { if (book.isLocalBook()) { - AnalyzeTxtFile.analyze(context, book).let { + AnalyzeTxtFile().analyze(context, book).let { App.db.bookDao().update(book) App.db.bookChapterDao().insert(*it.toTypedArray()) chapterListData.postValue(it) @@ -112,6 +112,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { toast(R.string.error_no_source) } } + }.onError { + toast("LoadTocError:${it.localizedMessage}") } } @@ -156,8 +158,10 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { chapters ) book.durChapterTitle = chapters[book.durChapterIndex].title - App.db.bookDao().insert(book) - App.db.bookChapterDao().insert(*chapters.toTypedArray()) + if (inBookshelf) { + App.db.bookDao().insert(book) + App.db.bookChapterDao().insert(*chapters.toTypedArray()) + } bookData.postValue(book) chapterListData.postValue(chapters) } @@ -169,6 +173,10 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { if (book.order == 0) { book.order = App.db.bookDao().maxOrder + 1 } + App.db.bookDao().getBook(book.name, book.author)?.let { + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } App.db.bookDao().insert(book) } }.onSuccess { @@ -192,6 +200,10 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { if (book.order == 0) { book.order = App.db.bookDao().maxOrder + 1 } + App.db.bookDao().getBook(book.name, book.author)?.let { + book.durChapterPos = it.durChapterPos + book.durChapterTitle = it.durChapterTitle + } App.db.bookDao().insert(book) } chapterListData.value?.let { diff --git a/app/src/main/java/io/legado/app/ui/book/read/Help.kt b/app/src/main/java/io/legado/app/ui/book/read/Help.kt index f5144977e..853a070da 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/Help.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/Help.kt @@ -63,7 +63,9 @@ object Help { } } - + /** + * 屏幕方向 + */ @SuppressLint("SourceLockedOrientationActivity") fun setOrientation(activity: Activity) = activity.apply { when (AppConfig.requestedDirection) { @@ -112,7 +114,7 @@ object Help { * 适配刘海 */ fun upLayoutInDisplayCutoutMode(window: Window) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && AppConfig.readBodyToLh) { window.attributes = window.attributes.apply { layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt index 043c419eb..dcd84634e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt @@ -134,6 +134,10 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo timeBatteryReceiver = null } upSystemUiVisibility() + if (!BuildConfig.DEBUG) { + SyncBookProgress.uploadBookProgress() + Backup.autoBack(this) + } } /** @@ -225,6 +229,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo R.id.menu_copy_text -> TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) R.id.menu_update_toc -> ReadBook.book?.let { + ReadBook.upMsg(getString(R.string.toc_updateing)) viewModel.loadChapterList(it) } R.id.menu_enable_replace -> ReadBook.book?.let { @@ -275,6 +280,18 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo */ override fun onKeyDown(keyCode: Int, event: KeyEvent?): Boolean { when (keyCode) { + getPrefInt(PreferKey.prevKey) -> { + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) + return true + } + } + getPrefInt(PreferKey.nextKey) -> { + if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { + page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) + return true + } + } KeyEvent.KEYCODE_VOLUME_UP -> { if (volumeKeyPage(PageDelegate.Direction.PREV)) { return true @@ -289,18 +306,6 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) return true } - getPrefInt(PreferKey.prevKey) -> { - if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.PREV) - return true - } - } - getPrefInt(PreferKey.nextKey) -> { - if (keyCode != KeyEvent.KEYCODE_UNKNOWN) { - page_view.pageDelegate?.keyTurnPage(PageDelegate.Direction.NEXT) - return true - } - } } return super.onKeyDown(keyCode, event) } @@ -502,7 +507,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } else { tv_chapter_url.gone() } - seek_read_page.max = it.pageSize().minus(1) + seek_read_page.max = it.pageSize.minus(1) seek_read_page.progress = ReadBook.durPageIndex tv_pre.isEnabled = ReadBook.durChapterIndex != 0 tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 @@ -705,6 +710,7 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo observeEvent(EventBus.UP_CONFIG) { upSystemUiVisibility() page_view.upBg() + page_view.upTipStyle() page_view.upStyle() if (it) { ReadBook.loadContent(resetPageOffset = false) diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt index 19c10ee83..a4755213d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadBookViewModel.kt @@ -7,6 +7,7 @@ import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter +import io.legado.app.help.AppConfig import io.legado.app.help.BookHelp import io.legado.app.help.IntentDataHelp import io.legado.app.model.WebBook @@ -39,8 +40,10 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { private fun initBook(book: Book) { if (ReadBook.book?.bookUrl != book.bookUrl) { - ReadBook.resetData(book) { name, author -> - autoChangeSource(name, author) + ReadBook.resetData(book) + if (!book.isLocalBook() && ReadBook.webBook == null) { + autoChangeSource(book.name, book.author) + return } isInitFinish = true ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) @@ -62,8 +65,10 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } else { isInitFinish = true ReadBook.titleDate.postValue(book.name) - ReadBook.upWebBook(book) { name, author -> - autoChangeSource(name, author) + ReadBook.upWebBook(book) + if (!book.isLocalBook() && ReadBook.webBook == null) { + autoChangeSource(book.name, book.author) + return } ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) if (ReadBook.chapterSize == 0) { @@ -102,11 +107,12 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ) { execute { if (book.isLocalBook()) { - AnalyzeTxtFile.analyze(context, book).let { + AnalyzeTxtFile().analyze(context, book).let { App.db.bookChapterDao().delByBook(book.bookUrl) App.db.bookChapterDao().insert(*it.toTypedArray()) App.db.bookDao().update(book) ReadBook.chapterSize = it.size + ReadBook.upMsg(null) ReadBook.loadContent(resetPageOffset = true) } } else { @@ -129,6 +135,8 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ReadBook.upMsg(context.getString(R.string.error_load_toc)) } } + }.onError { + ReadBook.upMsg("LoadTocError:${it.localizedMessage}") } } @@ -160,6 +168,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } private fun autoChangeSource(name: String, author: String) { + if (!AppConfig.autoChangeSource) return execute { App.db.bookSourceDao().allTextEnabled.forEach { source -> try { @@ -175,7 +184,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } }.onStart { - ReadBook.upMsg("正在自动换源") + ReadBook.upMsg(context.getString(R.string.source_auto_changing)) }.onFinally { ReadBook.upMsg(null) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt index a23725143..5d8ec5d82 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/MoreConfigDialog.kt @@ -18,6 +18,7 @@ import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.bottomBackground import io.legado.app.ui.book.read.Help +import io.legado.app.utils.dp import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent @@ -38,7 +39,7 @@ class MoreConfigDialog : DialogFragment() { attr.dimAmount = 0.0f attr.gravity = Gravity.BOTTOM it.attributes = attr - it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT) + it.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, 360.dp) } } @@ -94,6 +95,7 @@ class MoreConfigDialog : DialogFragment() { key: String? ) { when (key) { + PreferKey.readBodyToLh -> activity?.recreate() PreferKey.hideStatusBar -> { ReadBookConfig.hideStatusBar = getPrefBoolean(PreferKey.hideStatusBar) postEvent(EventBus.UP_CONFIG, true) diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt index ed1c296de..4534898e4 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PaddingConfigDialog.kt @@ -11,10 +11,7 @@ import io.legado.app.R import io.legado.app.constant.EventBus import io.legado.app.help.ReadBookConfig import io.legado.app.ui.book.read.Help -import io.legado.app.utils.dp -import io.legado.app.utils.gone import io.legado.app.utils.postEvent -import io.legado.app.utils.visible import kotlinx.android.synthetic.main.dialog_read_padding.* class PaddingConfigDialog : DialogFragment() { @@ -54,13 +51,6 @@ class PaddingConfigDialog : DialogFragment() { } private fun initData() = ReadBookConfig.apply { - if (hideStatusBar) { - ll_header_padding.visible() - tv_body_padding.setPadding(0, 10.dp, 0, 10.dp) - } else { - ll_header_padding.gone() - tv_body_padding.setPadding(0, 0.dp, 0, 10.dp) - } //正文 dsb_padding_top.progress = paddingTop dsb_padding_bottom.progress = paddingBottom diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt index 28a46b7f2..d85aae294 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/PageKeyDialog.kt @@ -8,6 +8,7 @@ import io.legado.app.constant.PreferKey import io.legado.app.utils.getPrefInt import io.legado.app.utils.hideSoftInput import io.legado.app.utils.putPrefInt +import io.legado.app.utils.removePref import kotlinx.android.synthetic.main.dialog_page_key.* import org.jetbrains.anko.sdk27.listeners.onClick @@ -19,11 +20,17 @@ class PageKeyDialog(context: Context) : Dialog(context, R.style.AppTheme_AlertDi et_prev.setText(context.getPrefInt(PreferKey.prevKey).toString()) et_next.setText(context.getPrefInt(PreferKey.nextKey).toString()) tv_ok.onClick { - et_prev.text?.let { - context.putPrefInt(PreferKey.prevKey, it.toString().toInt()) + val prevKey = et_prev.text?.toString() + if (prevKey.isNullOrEmpty()) { + context.removePref(PreferKey.prevKey) + } else { + context.putPrefInt(PreferKey.prevKey, prevKey.toInt()) } - et_next.text?.let { - context.putPrefInt(PreferKey.nextKey, it.toString().toInt()) + val nextKey = et_next.text?.toString() + if (nextKey.isNullOrEmpty()) { + context.removePref(PreferKey.nextKey) + } else { + context.putPrefInt(PreferKey.nextKey, nextKey.toInt()) } dismiss() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt index 294a1c127..cdb10159e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudConfigDialog.kt @@ -16,6 +16,7 @@ import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.help.AppConfig import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.backgroundColor import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.ui.book.read.Help @@ -42,8 +43,8 @@ class ReadAloudConfigDialog : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - val view = LinearLayout(context) - view.setBackgroundResource(R.color.background) + val view = LinearLayout(requireContext()) + view.setBackgroundColor(requireContext().backgroundColor) view.id = R.id.tag1 container?.addView(view) return view diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt index f29c4ddba..19cad6738 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadStyleDialog.kt @@ -104,10 +104,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { showTitleConfig() } tv_text_bold.onClick { - ReadBookConfig.apply { - textBold = !textBold - tv_text_bold.isSelected = textBold - } + ReadBookConfig.textBold = !ReadBookConfig.textBold + tv_text_bold.isSelected = ReadBookConfig.textBold postEvent(EventBus.UP_CONFIG, true) } tv_text_font.onClick { @@ -126,6 +124,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { dismiss() callBack?.showPaddingConfig() } + tv_tip.onClick { + TipConfigDialog().show(childFragmentManager, "tipConfigDialog") + } rg_page_anim.onCheckedChange { _, checkedId -> ReadBookConfig.pageAnim = rg_page_anim.getIndexById(checkedId) callBack?.page_view?.upPageAnim() diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt new file mode 100644 index 000000000..2635b70b2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TipConfigDialog.kt @@ -0,0 +1,261 @@ +package io.legado.app.ui.book.read.config + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import io.legado.app.R +import io.legado.app.base.BaseDialogFragment +import io.legado.app.constant.EventBus +import io.legado.app.help.ReadTipConfig +import io.legado.app.lib.dialogs.selector +import io.legado.app.ui.book.read.Help +import io.legado.app.utils.postEvent +import kotlinx.android.synthetic.main.dialog_tip_config.* +import org.jetbrains.anko.sdk27.listeners.onCheckedChange +import org.jetbrains.anko.sdk27.listeners.onClick + +class TipConfigDialog : BaseDialogFragment() { + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.let { + Help.upSystemUiVisibility(it) + it.windowManager?.defaultDisplay?.getMetrics(dm) + } + dialog?.window?.let { + val attr = it.attributes + attr.dimAmount = 0.0f + it.attributes = attr + it.setLayout((dm.widthPixels * 0.9).toInt(), ViewGroup.LayoutParams.WRAP_CONTENT) + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_tip_config, container) + } + + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + initView() + initEvent() + } + + private fun initView() { + tv_header_left.text = ReadTipConfig.tipHeaderLeftStr + tv_header_middle.text = ReadTipConfig.tipHeaderMiddleStr + tv_header_right.text = ReadTipConfig.tipHeaderRightStr + tv_footer_left.text = ReadTipConfig.tipFooterLeftStr + tv_footer_middle.text = ReadTipConfig.tipFooterMiddleStr + tv_footer_right.text = ReadTipConfig.tipFooterRightStr + sw_hide_header.isChecked = ReadTipConfig.hideHeader + sw_hide_footer.isChecked = ReadTipConfig.hideFooter + } + + private fun initEvent() { + tv_header_left.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderMiddle == i) { + tipHeaderMiddle = none + tv_header_middle.text = tipArray[none] + } + if (tipHeaderRight == i) { + tipHeaderRight = none + tv_header_right.text = tipArray[none] + } + if (tipFooterLeft == i) { + tipFooterLeft = none + tv_footer_left.text = tipArray[none] + } + if (tipFooterMiddle == i) { + tipFooterMiddle = none + tv_footer_middle.text = tipArray[none] + } + if (tipFooterRight == i) { + tipFooterRight = none + tv_footer_right.text = tipArray[none] + } + } + tipHeaderLeft = i + tv_header_left.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + tv_header_middle.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderLeft == i) { + tipHeaderLeft = none + tv_header_left.text = tipArray[none] + } + if (tipHeaderRight == i) { + tipHeaderRight = none + tv_header_right.text = tipArray[none] + } + if (tipFooterLeft == i) { + tipFooterLeft = none + tv_footer_left.text = tipArray[none] + } + if (tipFooterMiddle == i) { + tipFooterMiddle = none + tv_footer_middle.text = tipArray[none] + } + if (tipFooterRight == i) { + tipFooterRight = none + tv_footer_right.text = tipArray[none] + } + } + tipHeaderMiddle = i + tv_header_middle.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + tv_header_right.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderLeft == i) { + tipHeaderLeft = none + tv_header_left.text = tipArray[none] + } + if (tipHeaderMiddle == i) { + tipHeaderMiddle = none + tv_header_middle.text = tipArray[none] + } + if (tipFooterLeft == i) { + tipFooterLeft = none + tv_footer_left.text = tipArray[none] + } + if (tipFooterMiddle == i) { + tipFooterMiddle = none + tv_footer_middle.text = tipArray[none] + } + if (tipFooterRight == i) { + tipFooterRight = none + tv_footer_right.text = tipArray[none] + } + } + tipHeaderRight = i + tv_header_right.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + tv_footer_left.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderLeft == i) { + tipHeaderLeft = none + tv_header_left.text = tipArray[none] + } + if (tipHeaderMiddle == i) { + tipHeaderMiddle = none + tv_header_middle.text = tipArray[none] + } + if (tipHeaderRight == i) { + tipHeaderRight = none + tv_header_right.text = tipArray[none] + } + if (tipFooterMiddle == i) { + tipFooterMiddle = none + tv_footer_middle.text = tipArray[none] + } + if (tipFooterRight == i) { + tipFooterRight = none + tv_footer_right.text = tipArray[none] + } + } + tipFooterLeft = i + tv_footer_left.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + tv_footer_middle.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderLeft == i) { + tipHeaderLeft = none + tv_header_left.text = tipArray[none] + } + if (tipHeaderMiddle == i) { + tipHeaderMiddle = none + tv_header_middle.text = tipArray[none] + } + if (tipHeaderRight == i) { + tipHeaderRight = none + tv_header_right.text = tipArray[none] + } + if (tipFooterLeft == i) { + tipFooterLeft = none + tv_footer_left.text = tipArray[none] + } + if (tipFooterRight == i) { + tipFooterRight = none + tv_footer_right.text = tipArray[none] + } + } + tipFooterMiddle = i + tv_footer_middle.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + tv_footer_right.onClick { + selector(items = ReadTipConfig.tipArray.toList()) { _, i -> + ReadTipConfig.apply { + if (i != none) { + if (tipHeaderLeft == i) { + tipHeaderLeft = none + tv_header_left.text = tipArray[none] + } + if (tipHeaderMiddle == i) { + tipHeaderMiddle = none + tv_header_middle.text = tipArray[none] + } + if (tipHeaderRight == i) { + tipHeaderRight = none + tv_header_right.text = tipArray[none] + } + if (tipFooterLeft == i) { + tipFooterLeft = none + tv_footer_left.text = tipArray[none] + } + if (tipFooterMiddle == i) { + tipFooterMiddle = none + tv_footer_middle.text = tipArray[none] + } + } + tipFooterRight = i + tv_footer_right.text = tipArray[i] + } + postEvent(EventBus.UP_CONFIG, true) + } + } + sw_hide_header.onCheckedChange { buttonView, isChecked -> + if (buttonView?.isPressed == true) { + ReadTipConfig.hideHeader = isChecked + postEvent(EventBus.UP_CONFIG, true) + } + } + sw_hide_footer.onCheckedChange { buttonView, isChecked -> + if (buttonView?.isPressed == true) { + ReadTipConfig.hideFooter = isChecked + postEvent(EventBus.UP_CONFIG, true) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt index d4a6711bc..300a507ea 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt @@ -189,7 +189,7 @@ class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { rootView?.apply { tocRule.name = tv_rule_name.text.toString() tocRule.rule = tv_rule_regex.text.toString() - viewModel.saveRule(tocRule, rule) + viewModel.saveRule(tocRule) } } cancelButton() diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt index 4a63120c8..e94649684 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexViewModel.kt @@ -11,14 +11,11 @@ import io.legado.app.utils.fromJsonArray class TocRegexViewModel(application: Application) : BaseViewModel(application) { - fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) { + fun saveRule(rule: TxtTocRule) { execute { if (rule.serialNumber < 0) { rule.serialNumber = App.db.txtTocRule().lastOrderNum + 1 } - oldRule?.let { - App.db.txtTocRule().delete(oldRule) - } App.db.txtTocRule().insert(rule) } } @@ -26,6 +23,7 @@ class TocRegexViewModel(application: Application) : BaseViewModel(application) { fun importDefault() { execute { AnalyzeTxtFile.getDefaultRules().let { + App.db.txtTocRule().deleteDefault() App.db.txtTocRule().insert(*it.toTypedArray()) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt index 0d6a0009e..e0cee0b5c 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ChapterProvider.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.book.read.page import android.graphics.Typeface +import android.os.Build import android.text.Layout import android.text.StaticLayout import android.text.TextPaint @@ -31,8 +32,8 @@ object ChapterProvider { private var titleTopSpacing = 0 private var titleBottomSpacing = 0 var typeface: Typeface = Typeface.SANS_SERIF - var titlePaint = TextPaint() - var contentPaint = TextPaint() + lateinit var titlePaint: TextPaint + lateinit var contentPaint: TextPaint init { upStyle() @@ -43,23 +44,23 @@ object ChapterProvider { */ fun getTextChapter( bookChapter: BookChapter, - content: String, + contents: List, chapterSize: Int ): TextChapter { val textPages = arrayListOf() val pageLines = arrayListOf() val pageLengths = arrayListOf() val stringBuilder = StringBuilder() - val contents = content.split("\n") var durY = 0f textPages.add(TextPage()) - for ((index, text) in contents.withIndex()) { + contents.forEachIndexed { index, text -> val isTitle = index == 0 - if (isTitle && ReadBookConfig.titleMode == 2) { - continue + if (!(isTitle && ReadBookConfig.titleMode == 2)) { + durY = setTypeText( + text, durY, textPages, pageLines, + pageLengths, stringBuilder, isTitle + ) } - durY = - setTypeText(text, durY, textPages, pageLines, pageLengths, stringBuilder, isTitle) } textPages.last().height = durY + 20.dp textPages.last().text = stringBuilder.toString() @@ -69,7 +70,7 @@ object ChapterProvider { if (pageLengths.size < textPages.size) { pageLengths.add(textPages.last().text.length) } - for ((index, item) in textPages.withIndex()) { + textPages.forEachIndexed { index, item -> item.index = index item.pageSize = textPages.size item.chapterIndex = bookChapter.index @@ -188,11 +189,11 @@ object ChapterProvider { val gapCount: Int = words.length - 1 val d = (visibleWidth - desiredWidth) / gapCount var x = startX - for ((i, char) in words.toStringArray().withIndex()) { - val cw = StaticLayout.getDesiredWidth(char, textPaint) - val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw) + words.toStringArray().forEachIndexed { index, s -> + val cw = StaticLayout.getDesiredWidth(s, textPaint) + val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw) textLine.addTextChar( - charData = char, + charData = s, start = paddingLeft + x, end = paddingLeft + x1 ) @@ -262,30 +263,36 @@ object ChapterProvider { Typeface.SANS_SERIF } //标题 - titlePaint.isAntiAlias = true + titlePaint = TextPaint() titlePaint.color = ReadBookConfig.durConfig.textColor() titlePaint.letterSpacing = ReadBookConfig.letterSpacing - titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD) + titlePaint.typeface = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + Typeface.create(typeface, if (ReadBookConfig.textBold) 900 else 700, false) + } else { + Typeface.create(typeface, Typeface.BOLD) + } titlePaint.textSize = with(ReadBookConfig) { textSize + titleSize }.sp.toFloat() + titlePaint.isAntiAlias = true //正文 - contentPaint.isAntiAlias = true + contentPaint = TextPaint() contentPaint.color = ReadBookConfig.durConfig.textColor() contentPaint.letterSpacing = ReadBookConfig.letterSpacing val style = if (ReadBookConfig.textBold) Typeface.BOLD else Typeface.NORMAL contentPaint.typeface = Typeface.create(typeface, style) contentPaint.textSize = ReadBookConfig.textSize.sp.toFloat() + contentPaint.isAntiAlias = true //间距 lineSpacingExtra = ReadBookConfig.lineSpacingExtra paragraphSpacing = ReadBookConfig.paragraphSpacing titleTopSpacing = ReadBookConfig.titleTopSpacing.dp titleBottomSpacing = ReadBookConfig.titleBottomSpacing.dp - upSize() + upViewSize() } /** * 更新View尺寸 */ - fun upSize() { + fun upViewSize() { paddingLeft = ReadBookConfig.paddingLeft.dp paddingTop = ReadBookConfig.paddingTop.dp visibleWidth = viewWidth - paddingLeft - ReadBookConfig.paddingRight.dp diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt index 53f03462f..62f240303 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt @@ -61,7 +61,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at super.onSizeChanged(w, h, oldw, oldh) ChapterProvider.viewWidth = w ChapterProvider.viewHeight = h - ChapterProvider.upSize() + ChapterProvider.upViewSize() upVisibleRect() textPage.format() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt index 1cc006512..8058c0a04 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ContentView.kt @@ -5,13 +5,20 @@ import android.content.Context import android.graphics.drawable.Drawable import android.view.MotionEvent import android.widget.FrameLayout -import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap +import androidx.core.view.isGone +import androidx.core.view.isInvisible +import com.hankcs.hanlp.HanLP import io.legado.app.R import io.legado.app.constant.AppConst.timeFormat import io.legado.app.help.AppConfig import io.legado.app.help.ReadBookConfig +import io.legado.app.help.ReadTipConfig import io.legado.app.ui.book.read.page.entities.TextPage -import io.legado.app.utils.* +import io.legado.app.ui.widget.BatteryView +import io.legado.app.utils.dp +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.statusBarHeight +import io.legado.app.utils.visible import kotlinx.android.synthetic.main.view_book_page.view.* import java.util.* @@ -19,14 +26,22 @@ import java.util.* class ContentView(context: Context) : FrameLayout(context) { private var battery = 100 + private var tvTitle: BatteryView? = null + private var tvTime: BatteryView? = null + private var tvBattery: BatteryView? = null + private var tvPage: BatteryView? = null + private var tvTotalProgress: BatteryView? = null + private var tvPageAndTotal: BatteryView? = null + + val headerHeight: Int + get() = if (ReadBookConfig.hideStatusBar) ll_header.height else context.statusBarHeight init { //设置背景防止切换背景时文字重叠 setBackgroundColor(context.getCompatColor(R.color.background)) inflate(context, R.layout.view_book_page, this) - + upTipStyle() upStyle() - upTime() content_text_view.upView = { setProgress(it) } @@ -34,28 +49,31 @@ class ContentView(context: Context) : FrameLayout(context) { fun upStyle() { ReadBookConfig.apply { - tv_top_left.typeface = ChapterProvider.typeface - tv_top_right.typeface = ChapterProvider.typeface - tv_bottom_left.typeface = ChapterProvider.typeface - tv_bottom_right.typeface = ChapterProvider.typeface - battery_view.typeface = ChapterProvider.typeface + bv_header_left.typeface = ChapterProvider.typeface + tv_header_left.typeface = ChapterProvider.typeface + tv_header_middle.typeface = ChapterProvider.typeface + tv_header_right.typeface = ChapterProvider.typeface + bv_footer_left.typeface = ChapterProvider.typeface + tv_footer_left.typeface = ChapterProvider.typeface + tv_footer_middle.typeface = ChapterProvider.typeface + tv_footer_right.typeface = ChapterProvider.typeface + bv_header_left.setColor(durConfig.textColor()) + tv_header_left.setColor(durConfig.textColor()) + tv_header_middle.setColor(durConfig.textColor()) + tv_header_right.setColor(durConfig.textColor()) + bv_footer_left.setColor(durConfig.textColor()) + tv_footer_left.setColor(durConfig.textColor()) + tv_footer_middle.setColor(durConfig.textColor()) + tv_footer_right.setColor(durConfig.textColor()) //显示状态栏时隐藏header - if (hideStatusBar) { - ll_header.layoutParams = ll_header.layoutParams.apply { - height = context.statusBarHeight + headerPaddingTop.dp + headerPaddingBottom.dp - } - ll_header.setPadding( - headerPaddingLeft.dp, - headerPaddingTop.dp, - headerPaddingRight.dp, - headerPaddingBottom.dp - ) - ll_header.visible() - page_panel.setPadding(0, 0, 0, 0) - } else { - ll_header.gone() - page_panel.setPadding(0, context.statusBarHeight, 0, 0) - } + vw_status_bar.setPadding(0, context.statusBarHeight, 0, 0) + vw_status_bar.isGone = hideStatusBar + ll_header.setPadding( + headerPaddingLeft.dp, + headerPaddingTop.dp, + headerPaddingRight.dp, + headerPaddingBottom.dp + ) ll_footer.setPadding( footerPaddingLeft.dp, footerPaddingTop.dp, @@ -65,47 +83,115 @@ class ContentView(context: Context) : FrameLayout(context) { vw_top_divider.visible(showHeaderLine) vw_bottom_divider.visible(showFooterLine) content_text_view.upVisibleRect() - durConfig.textColor().let { - tv_top_left.setTextColor(it) - tv_top_right.setTextColor(it) - tv_bottom_left.setTextColor(it) - tv_bottom_right.setTextColor(it) - battery_view.setColor(it) - } - if (hideStatusBar) { - tv_bottom_left.text = timeFormat.format(Date(System.currentTimeMillis())) - battery_view.visible() - battery_view.setBattery(battery) - } else { - battery_view.gone() - } } + upTime() + upBattery(battery) } - val headerHeight: Int - get() { - return if (ReadBookConfig.hideStatusBar) { - ll_header.height - } else { - context.statusBarHeight - } + fun upTipStyle() { + ReadTipConfig.apply { + tv_header_left.isInvisible = tipHeaderLeft != chapterTitle + bv_header_left.isInvisible = tipHeaderLeft == none || !tv_header_left.isInvisible + tv_header_right.isGone = tipHeaderRight == none + tv_header_middle.isGone = tipHeaderMiddle == none + tv_footer_left.isInvisible = tipFooterLeft != chapterTitle + bv_footer_left.isInvisible = tipFooterLeft == none || !tv_footer_left.isInvisible + tv_footer_right.isGone = tipFooterRight == none + tv_footer_middle.isGone = tipFooterMiddle == none + ll_header.isGone = hideHeader + ll_footer.isGone = hideFooter + } + tvTitle = when (ReadTipConfig.chapterTitle) { + ReadTipConfig.tipHeaderLeft -> tv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> tv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTitle?.apply { + isBattery = false + textSize = 12f + } + tvTime = when (ReadTipConfig.time) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTime?.apply { + isBattery = false + textSize = 12f + } + tvBattery = when (ReadTipConfig.battery) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null } + tvBattery?.apply { + isBattery = true + textSize = 10f + } + tvPage = when (ReadTipConfig.page) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvPage?.apply { + isBattery = false + textSize = 12f + } + tvTotalProgress = when (ReadTipConfig.totalProgress) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvTotalProgress?.apply { + isBattery = false + textSize = 12f + } + tvPageAndTotal = when (ReadTipConfig.pageAndTotal) { + ReadTipConfig.tipHeaderLeft -> bv_header_left + ReadTipConfig.tipHeaderMiddle -> tv_header_middle + ReadTipConfig.tipHeaderRight -> tv_header_right + ReadTipConfig.tipFooterLeft -> bv_footer_left + ReadTipConfig.tipFooterMiddle -> tv_footer_middle + ReadTipConfig.tipFooterRight -> tv_footer_right + else -> null + } + tvPageAndTotal?.apply { + isBattery = false + textSize = 12f + } + } fun setBg(bg: Drawable?) { page_panel.background = bg } fun upTime() { - if (ReadBookConfig.hideStatusBar) { - tv_bottom_right.text = timeFormat.format(Date(System.currentTimeMillis())) - } + tvTime?.text = timeFormat.format(Date(System.currentTimeMillis())) } fun upBattery(battery: Int) { this.battery = battery - if (ReadBookConfig.hideStatusBar) { - battery_view.setBattery(battery) - } + tvBattery?.setBattery(battery) } fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) { @@ -122,18 +208,14 @@ class ContentView(context: Context) : FrameLayout(context) { @SuppressLint("SetTextI18n") fun setProgress(textPage: TextPage) = textPage.apply { val title = when (AppConfig.chineseConverterType) { - 1 -> ZhConvertBootstrap.newInstance().toSimple(textPage.title) - 2 -> ZhConvertBootstrap.newInstance().toTraditional(textPage.title) + 1 -> HanLP.convertToSimplifiedChinese(textPage.title) + 2 -> HanLP.convertToTraditionalChinese(textPage.title) else -> textPage.title } - if (ReadBookConfig.hideStatusBar) { - tv_top_left.text = title - tv_top_right.text = readProgress - tv_bottom_left.text = "${index.plus(1)}/$pageSize" - } else { - tv_bottom_left.text = title - tv_bottom_right.text = "${index.plus(1)}/$pageSize $readProgress" - } + tvTitle?.text = title + tvPage?.text = "${index.plus(1)}/$pageSize" + tvTotalProgress?.text = readProgress + tvPageAndTotal?.text = "${index.plus(1)}/$pageSize $readProgress" } fun onScroll(offset: Float) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt index dd19fdc0e..3a996bfe9 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt @@ -110,6 +110,12 @@ class PageView(context: Context, attrs: AttributeSet) : callBack.screenOffTimerStart() } + fun upTipStyle() { + curPage.upTipStyle() + prevPage.upTipStyle() + nextPage.upTipStyle() + } + fun upStyle() { ChapterProvider.upStyle() curPage.upStyle() diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt b/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt index a38ed6e51..95917ae11 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/TextPageFactory.kt @@ -14,7 +14,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource } override fun hasNextPlus(): Boolean = with(dataSource) { - return hasNextChapter() || pageIndex < (currentChapter?.pageSize() ?: 1) - 2 + return hasNextChapter() || pageIndex < (currentChapter?.pageSize ?: 1) - 2 } override fun moveToFirst() { @@ -23,10 +23,10 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource override fun moveToLast() = with(dataSource) { currentChapter?.let { - if (it.pageSize() == 0) { + if (it.pageSize == 0) { ReadBook.setPageIndex(0) } else { - ReadBook.setPageIndex(it.pageSize().minus(1)) + ReadBook.setPageIndex(it.pageSize.minus(1)) } } ?: ReadBook.setPageIndex(0) } @@ -75,7 +75,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource return@with TextPage(text = it).format() } currentChapter?.let { - if (pageIndex < it.pageSize() - 1) { + if (pageIndex < it.pageSize - 1) { return@with it.page(pageIndex + 1)?.removePageAloudSpan() ?: TextPage(title = it.title).format() } @@ -102,7 +102,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource } } prevChapter?.let { - return@with it.lastPage()?.removePageAloudSpan() + return@with it.lastPage?.removePageAloudSpan() ?: TextPage(title = it.title).format() } return TextPage().format() @@ -111,12 +111,12 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource override val nextPagePlus: TextPage get() = with(dataSource) { currentChapter?.let { - if (pageIndex < it.pageSize() - 2) { + if (pageIndex < it.pageSize - 2) { return@with it.page(pageIndex + 2)?.removePageAloudSpan() ?: TextPage(title = it.title).format() } nextChapter?.let { nc -> - if (pageIndex < it.pageSize() - 1) { + if (pageIndex < it.pageSize - 1) { return@with nc.page(0)?.removePageAloudSpan() ?: TextPage(title = nc.title).format() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt index 8fe4fb699..f8f9bec74 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt @@ -37,7 +37,10 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie override fun onTouch(event: MotionEvent) { when (event.action) { MotionEvent.ACTION_DOWN -> { - abort() + if (abort()) { + onAnimStop() + stopScroll() + } } MotionEvent.ACTION_MOVE -> { if (isTextSelected) { diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt index d83f60edb..cd455b641 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/PageDelegate.kt @@ -154,10 +154,12 @@ abstract class PageDelegate(protected val pageView: PageView) : } } - fun abort() { + fun abort(): Boolean { if (!scroller.isFinished) { scroller.abortAnimation() + return true } + return false } open fun onAnimStart() {}//scroller start diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt index e05b7746b..3f183830d 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt @@ -15,25 +15,16 @@ data class TextChapter( return pages.getOrNull(index) } - fun lastPage(): TextPage? { - if (pages.isNotEmpty()) { - return pages[pages.lastIndex] - } - return null - } + val lastPage: TextPage? get() = pages.lastOrNull() - fun lastIndex(): Int { - return pages.size - 1 - } + val lastIndex: Int get() = pages.lastIndex + + val pageSize: Int get() = pages.size fun isLastIndex(index: Int): Boolean { return index >= pages.size - 1 } - fun pageSize(): Int { - return pages.size - } - fun getReadLength(pageIndex: Int): Int { var length = 0 val maxIndex = min(pageIndex, pages.size) @@ -45,8 +36,8 @@ data class TextChapter( fun getUnRead(pageIndex: Int): String { val stringBuilder = StringBuilder() - if (pageIndex < pages.size && pages.isNotEmpty()) { - for (index in pageIndex..lastIndex()) { + if (pages.isNotEmpty()) { + for (index in pageIndex..pages.lastIndex) { stringBuilder.append(pages[index].text) } } @@ -60,5 +51,4 @@ data class TextChapter( } return stringBuilder.toString() } -} - +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt index 9613947c7..e81af6986 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -20,7 +20,7 @@ data class TextPage( fun upLinesPosition() = ChapterProvider.apply { if (textLines.size <= 1) return@apply - if (visibleHeight - height > with(textLines.last()) { lineBottom - lineTop }) return@apply + if (visibleHeight - height >= with(textLines.last()) { lineBottom - lineTop }) return@apply val surplus = (visibleBottom - textLines.last().lineBottom) if (surplus == 0f) return@apply height += surplus @@ -44,12 +44,11 @@ data class TextPage( if (y < 0) y = 0f for (lineIndex in 0 until layout.lineCount) { val textLine = TextLine() - textLine.lineTop = (ChapterProvider.paddingTop + y - - (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))) - textLine.lineBase = (ChapterProvider.paddingTop + y - - (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))) + textLine.lineTop = ChapterProvider.paddingTop + y + layout.getLineTop(lineIndex) + textLine.lineBase = + ChapterProvider.paddingTop + y + layout.getLineBaseline(lineIndex) textLine.lineBottom = - textLine.lineBase + ChapterProvider.contentPaint.fontMetrics.descent + ChapterProvider.paddingTop + y + layout.getLineBottom(lineIndex) var x = ChapterProvider.paddingLeft + (ChapterProvider.visibleWidth - layout.getLineMax(lineIndex)) / 2 textLine.text = diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt index 46f3eb49f..71ee36149 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt @@ -334,11 +334,11 @@ class BookSourceEditActivity : "sourceRegex" -> contentRule.sourceRegex = it.value } } - source.ruleSearch = GSON.toJson(searchRule) - source.ruleExplore = GSON.toJson(exploreRule) - source.ruleBookInfo = GSON.toJson(bookInfoRule) - source.ruleToc = GSON.toJson(tocRule) - source.ruleContent = GSON.toJson(contentRule) + source.ruleSearch = searchRule + source.ruleExplore = exploreRule + source.ruleBookInfo = bookInfoRule + source.ruleToc = tocRule + source.ruleContent = contentRule return source } @@ -350,7 +350,7 @@ class BookSourceEditActivity : return true } - override fun sendText(text: String) { + private fun insertText(text: String) { if (text.isBlank()) return val view = window.decorView.findFocus() if (view is EditText) { @@ -365,6 +365,14 @@ class BookSourceEditActivity : } } + override fun sendText(text: String) { + if (text == AppConst.keyboardToolChars[0]) { + insertText(AppConst.urlOption) + } else { + insertText(text) + } + } + private fun showKeyboardTopPopupWindow() { mSoftKeyboardTool?.let { if (it.isShowing) return diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt index c84497536..5857fe8ef 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceViewModel.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.book.source.manage import android.app.Application +import android.net.Uri import android.text.TextUtils import androidx.documentfile.provider.DocumentFile import com.jayway.jsonpath.JsonPath @@ -156,9 +157,20 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) fun importSourceFromFilePath(path: String, finally: (msg: String) -> Unit) { execute { - val file = File(path) - if (file.exists()) { - importSource(file.readText(), finally) + val content = if (path.isContentPath()) { + //在前面被解码了,如果不进行编码,中文会无法识别 + val newPath = Uri.encode(path, ":/.") + DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) + } else { + val file = File(path) + if (file.exists()) { + file.readText() + } else { + null + } + } + if (content != null) { + importSource(content, finally) } else { withContext(Dispatchers.Main) { finally("打开文件出错") @@ -215,7 +227,7 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } private fun importSourceUrl(url: String): Int { - HttpHelper.simpleGet(url)?.let { body -> + HttpHelper.simpleGet(url, "UTF-8")?.let { body -> val bookSources = mutableListOf() val items: List> = jsonPath.parse(body).read("$") for (item in items) { diff --git a/app/src/main/java/io/legado/app/ui/config/FileAssociationViewModel.kt b/app/src/main/java/io/legado/app/ui/config/FileAssociationViewModel.kt index 80db1af37..02aab3668 100644 --- a/app/src/main/java/io/legado/app/ui/config/FileAssociationViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/config/FileAssociationViewModel.kt @@ -45,7 +45,11 @@ class FileAssociationViewModel(application: Application) : BaseViewModel(applica } if (TextUtils.isEmpty(scheme)) { execute { - LocalBook.importFile(uri.path.toString()) + if (uri.scheme == "content"){ + LocalBook.importFile(uri.toString()) + }else{ + LocalBook.importFile(uri.path.toString()) + } toast("添加本地文件成功${uri.path}") } return null @@ -54,8 +58,13 @@ class FileAssociationViewModel(application: Application) : BaseViewModel(applica toast("文件不存在") return null } + // content模式下,需要传递完整的路径,方便后续解析 + url = if (uri.scheme == "content"){ + "yuedu://${scheme}/importonline?src=$uri" + }else{ + "yuedu://${scheme}/importonline?src=${uri.path}" + } - url = "yuedu://${scheme}/importonline?src=${uri.path}" } else if (uri.scheme == "yuedu") { url = uri.toString() } else { diff --git a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt index a010428fd..f23fee053 100644 --- a/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -1,8 +1,6 @@ package io.legado.app.ui.config -import android.app.Activity.RESULT_OK import android.content.ComponentName -import android.content.Intent import android.content.SharedPreferences import android.content.pm.PackageManager import android.os.Bundle @@ -19,17 +17,13 @@ import io.legado.app.help.BookHelp import io.legado.app.lib.theme.ATH import io.legado.app.receiver.SharedReceiverActivity import io.legado.app.service.WebService -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.ui.filechooser.FilePicker import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.utils.* class OtherConfigFragment : PreferenceFragmentCompat(), - FileChooserDialog.CallBack, SharedPreferences.OnSharedPreferenceChangeListener { - private val requestCodeDownloadPath = 25324 private val packageManager = App.INSTANCE.packageManager private val componentName = ComponentName( App.INSTANCE, @@ -40,7 +34,6 @@ class OtherConfigFragment : PreferenceFragmentCompat(), override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) addPreferencesFromResource(R.xml.pref_config_other) - upPreferenceSummary(getString(R.string.pk_download_path), BookHelp.downloadPath) upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString()) upPreferenceSummary(PreferKey.webPort, webPort.toString()) } @@ -74,7 +67,6 @@ class OtherConfigFragment : PreferenceFragmentCompat(), .show { putPrefInt(PreferKey.webPort, it) } - getString(R.string.pk_download_path) -> selectDownloadPath() PreferKey.cleanCache -> { BookHelp.clearCache() toast(R.string.clear_cache_success) @@ -85,9 +77,6 @@ class OtherConfigFragment : PreferenceFragmentCompat(), override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { when (key) { - getString(R.string.pk_download_path) -> { - upPreferenceSummary(key, BookHelp.downloadPath) - } PreferKey.threadCount -> upPreferenceSummary( key, AppConfig.threadCount.toString() ) @@ -139,34 +128,4 @@ class OtherConfigFragment : PreferenceFragmentCompat(), } } - private fun selectDownloadPath() { - FilePicker.selectFolder(this, requestCodeDownloadPath) { - removePref(getString(R.string.pk_download_path)) - } - } - - private fun putDownloadPath(path: String) { - putPrefString(getString(R.string.pk_download_path), path) - } - - override fun onFilePicked(requestCode: Int, currentPath: String) { - if (requestCode == requestCodeDownloadPath) { - putDownloadPath(currentPath) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - requestCodeDownloadPath -> if (resultCode == RESULT_OK) { - data?.data?.let { uri -> - requireContext().contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - putDownloadPath(uri.toString()) - } - } - } - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt b/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt index 25ad9d56b..f07371518 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/utils/ConvertUtils.kt @@ -5,7 +5,6 @@ import android.graphics.Bitmap import android.graphics.BitmapFactory import android.graphics.drawable.BitmapDrawable import android.graphics.drawable.Drawable -import okhttp3.internal.and import java.io.BufferedReader import java.io.IOException import java.io.InputStream @@ -36,7 +35,7 @@ object ConvertUtils { var abyte: Byte for (i in bytes.indices) { abyte = bytes[i] - result += abyte and 0xFF shl 8 * i + result += (abyte.toInt() and 0xFF).shl(8 * i) } return result } diff --git a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt index 859f63d9e..e8b2870cb 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainActivity.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainActivity.kt @@ -20,6 +20,7 @@ import io.legado.app.base.VMBaseActivity import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.help.AppConfig +import io.legado.app.help.storage.Backup import io.legado.app.lib.theme.ATH import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud @@ -83,7 +84,7 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode) if (!BuildConfig.DEBUG) { val log = String(assets.open("updateLog.md").readBytes()) - TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000) + TextDialog.show(supportFragmentManager, log, TextDialog.MD, 5000, true) } } } @@ -119,6 +120,13 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), return super.onKeyUp(keyCode, event) } + override fun onPause() { + super.onPause() + if (!BuildConfig.DEBUG) { + Backup.autoBack(this) + } + } + override fun onDestroy() { super.onDestroy() ReadAloud.stop(this) diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt index 5cf6de284..329c19e85 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfFragment.kt @@ -42,6 +42,9 @@ import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.fragment_bookshelf.* import kotlinx.android.synthetic.main.view_tab_layout.* import kotlinx.android.synthetic.main.view_title_bar.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.anko.startActivity @@ -54,8 +57,10 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b get() = getViewModel(BookshelfViewModel::class.java) private var bookGroupLiveData: LiveData>? = null + private var noGroupLiveData: LiveData? = null private val bookGroups = mutableListOf() private val fragmentMap = hashMapOf() + private var showGroupNone = false override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) @@ -102,22 +107,49 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b bookGroupLiveData = App.db.bookGroupDao().liveDataAll() bookGroupLiveData?.observe(viewLifecycleOwner, Observer { viewModel.checkGroup(it) - synchronized(this) { - tab_layout.removeOnTabSelectedListener(this) - bookGroups.clear() - if (AppConfig.bookGroupAllShow) { - bookGroups.add(AppConst.bookGroupAll) + launch { + synchronized(this) { + tab_layout.removeOnTabSelectedListener(this@BookshelfFragment) } - if (AppConfig.bookGroupLocalShow) { - bookGroups.add(AppConst.bookGroupLocal) + var noGroupSize = 0 + withContext(IO) { + if (AppConfig.bookGroupNoneShow) { + noGroupSize = App.db.bookDao().noGroupSize + } } - if (AppConfig.bookGroupAudioShow) { - bookGroups.add(AppConst.bookGroupAudio) + synchronized(this@BookshelfFragment) { + bookGroups.clear() + if (AppConfig.bookGroupAllShow) { + bookGroups.add(AppConst.bookGroupAll) + } + if (AppConfig.bookGroupLocalShow) { + bookGroups.add(AppConst.bookGroupLocal) + } + if (AppConfig.bookGroupAudioShow) { + bookGroups.add(AppConst.bookGroupAudio) + } + showGroupNone = if (noGroupSize > 0 && it.isNotEmpty()) { + bookGroups.add(AppConst.bookGroupNone) + true + } else { + false + } + bookGroups.addAll(it) + view_pager_bookshelf.adapter?.notifyDataSetChanged() + tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() + tab_layout.addOnTabSelectedListener(this@BookshelfFragment) } - bookGroups.addAll(it) - view_pager_bookshelf.adapter?.notifyDataSetChanged() - tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() - tab_layout.addOnTabSelectedListener(this) + } + }) + noGroupLiveData?.removeObservers(viewLifecycleOwner) + noGroupLiveData = App.db.bookDao().observeNoGroupSize() + noGroupLiveData?.observe(viewLifecycleOwner, Observer { + if (it > 0 && !showGroupNone && AppConfig.bookGroupNoneShow) { + showGroupNone = true + upGroup() + } else if (it == 0 && showGroupNone) { + showGroupNone = false + upGroup() } }) } @@ -132,20 +164,36 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b } override fun upGroup() { - synchronized(this) { - bookGroups.remove(AppConst.bookGroupAll) - bookGroups.remove(AppConst.bookGroupLocal) - bookGroups.remove(AppConst.bookGroupAudio) - if (AppConfig.bookGroupAudioShow) { - bookGroups.add(0, AppConst.bookGroupAudio) - } - if (AppConfig.bookGroupLocalShow) { - bookGroups.add(0, AppConst.bookGroupLocal) + launch { + var noGroupSize = 0 + withContext(IO) { + if (AppConfig.bookGroupNoneShow) { + noGroupSize = App.db.bookDao().noGroupSize + } } - if (AppConfig.bookGroupAllShow) { - bookGroups.add(0, AppConst.bookGroupAll) + synchronized(this@BookshelfFragment) { + bookGroups.remove(AppConst.bookGroupAll) + bookGroups.remove(AppConst.bookGroupLocal) + bookGroups.remove(AppConst.bookGroupAudio) + bookGroups.remove(AppConst.bookGroupNone) + showGroupNone = + if (noGroupSize > 0 && bookGroups.isNotEmpty()) { + bookGroups.add(0, AppConst.bookGroupNone) + true + } else { + false + } + if (AppConfig.bookGroupAudioShow) { + bookGroups.add(0, AppConst.bookGroupAudio) + } + if (AppConfig.bookGroupLocalShow) { + bookGroups.add(0, AppConst.bookGroupLocal) + } + if (AppConfig.bookGroupAllShow) { + bookGroups.add(0, AppConst.bookGroupAll) + } + view_pager_bookshelf.adapter?.notifyDataSetChanged() } - view_pager_bookshelf.adapter?.notifyDataSetChanged() } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt index d82480ca2..ab8984752 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksFragment.kt @@ -11,6 +11,7 @@ import androidx.recyclerview.widget.RecyclerView import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment +import io.legado.app.constant.AppConst import io.legado.app.constant.BookType import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey @@ -98,9 +99,10 @@ class BooksFragment : BaseFragment(R.layout.fragment_books), private fun upRecyclerData() { bookshelfLiveData?.removeObservers(this) bookshelfLiveData = when (groupId) { - -1 -> App.db.bookDao().observeAll() - -2 -> App.db.bookDao().observeLocal() - -3 -> App.db.bookDao().observeAudio() + AppConst.bookGroupAll.groupId -> App.db.bookDao().observeAll() + AppConst.bookGroupLocal.groupId -> App.db.bookDao().observeLocal() + AppConst.bookGroupAudio.groupId -> App.db.bookDao().observeAudio() + AppConst.bookGroupNone.groupId -> App.db.bookDao().observeNoGroup() else -> App.db.bookDao().observeByGroup(groupId) } bookshelfLiveData?.observe(this, Observer { list -> diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt index d58f7587e..955c5ded3 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssAdapter.kt @@ -1,6 +1,8 @@ package io.legado.app.ui.main.rss import android.content.Context +import android.view.View +import androidx.appcompat.widget.PopupMenu import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -8,6 +10,7 @@ import io.legado.app.data.entities.RssSource import io.legado.app.help.ImageLoader import kotlinx.android.synthetic.main.item_rss.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.sdk27.listeners.onLongClick class RssAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_rss) { @@ -29,9 +32,32 @@ class RssAdapter(context: Context, val callBack: CallBack) : callBack.openRss(it) } } + holder.itemView.onLongClick { + getItem(holder.layoutPosition)?.let { + showMenu(holder.itemView.iv_icon, it) + } + true + } + } + + private fun showMenu(view: View, rssSource: RssSource) { + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.rss_main_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_top -> callBack.toTop(rssSource) + R.id.menu_edit -> callBack.edit(rssSource) + R.id.menu_del -> callBack.del(rssSource) + } + true + } + popupMenu.show() } interface CallBack { fun openRss(rssSource: RssSource) + fun toTop(rssSource: RssSource) + fun edit(rssSource: RssSource) + fun del(rssSource: RssSource) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt index b960041d8..334ce9e64 100644 --- a/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/rss/RssFragment.kt @@ -8,22 +8,27 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.App import io.legado.app.R -import io.legado.app.base.BaseFragment +import io.legado.app.base.VMBaseFragment import io.legado.app.data.entities.RssSource import io.legado.app.lib.theme.ATH import io.legado.app.ui.main.MainViewModel -import io.legado.app.ui.rss.article.RssArticlesActivity +import io.legado.app.ui.rss.article.RssSortActivity import io.legado.app.ui.rss.favorites.RssFavoritesActivity +import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity +import io.legado.app.ui.rss.source.manage.RssSourceViewModel +import io.legado.app.utils.getViewModel import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.fragment_rss.* import kotlinx.android.synthetic.main.view_title_bar.* -class RssFragment : BaseFragment(R.layout.fragment_rss), +class RssFragment : VMBaseFragment(R.layout.fragment_rss), RssAdapter.CallBack { private lateinit var adapter: RssAdapter + override val viewModel: RssSourceViewModel + get() = getViewModel(RssSourceViewModel::class.java) override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) @@ -60,6 +65,18 @@ class RssFragment : BaseFragment(R.layout.fragment_rss), } override fun openRss(rssSource: RssSource) { - startActivity(Pair("url", rssSource.sourceUrl)) + startActivity(Pair("url", rssSource.sourceUrl)) + } + + override fun toTop(rssSource: RssSource) { + viewModel.topSource(rssSource) + } + + override fun edit(rssSource: RssSource) { + startActivity(Pair("data", rssSource.sourceUrl)) + } + + override fun del(rssSource: RssSource) { + viewModel.del(rssSource) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt index ec75e78ed..d11d04d4a 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleViewModel.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.replacerule import android.app.Application +import android.net.Uri import android.text.TextUtils import androidx.documentfile.provider.DocumentFile import io.legado.app.App @@ -18,9 +19,20 @@ import java.io.File class ReplaceRuleViewModel(application: Application) : BaseViewModel(application) { fun importSourceFromFilePath(path: String, finally: (msg: String) -> Unit) { execute { - val file = File(path) - if (file.exists()) { - importSource(file.readText(), finally) + val content = if (path.isContentPath()) { + //在前面被解码了,如果不进行编码,中文会无法识别 + val newPath = Uri.encode(path, ":/.") + DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) + } else { + val file = File(path) + if (file.exists()) { + file.readText() + } else { + null + } + } + if (content != null) { + importSource(content, finally) } else { withContext(Dispatchers.Main) { finally("打开文件出错") diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt similarity index 51% rename from app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt rename to app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt index 31ac9cdda..f7a1b6bb6 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesFragment.kt @@ -1,83 +1,67 @@ package io.legado.app.ui.rss.article import android.os.Bundle -import android.view.Menu -import android.view.MenuItem +import android.view.View import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.App import io.legado.app.R -import io.legado.app.base.VMBaseActivity +import io.legado.app.base.VMBaseFragment import io.legado.app.data.entities.RssArticle import io.legado.app.lib.theme.ATH import io.legado.app.ui.rss.read.ReadRssActivity -import io.legado.app.ui.rss.source.edit.RssSourceEditActivity import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.ui.widget.recycler.VerticalDivider import io.legado.app.utils.getViewModel -import kotlinx.android.synthetic.main.activity_rss_artivles.* +import io.legado.app.utils.getViewModelOfActivity +import io.legado.app.utils.startActivity +import kotlinx.android.synthetic.main.fragment_rss_articles.* import kotlinx.android.synthetic.main.view_load_more.view.* import kotlinx.android.synthetic.main.view_refresh_recycler.* -import org.jetbrains.anko.startActivity -import org.jetbrains.anko.startActivityForResult -class RssArticlesActivity : VMBaseActivity(R.layout.activity_rss_artivles), - RssArticlesViewModel.CallBack, +class RssArticlesFragment : VMBaseFragment(R.layout.fragment_rss_articles), RssArticlesAdapter.CallBack { + companion object { + fun create(sortName: String, sortUrl: String): RssArticlesFragment { + return RssArticlesFragment().apply { + val bundle = Bundle() + bundle.putString("sortName", sortName) + bundle.putString("sortUrl", sortUrl) + arguments = bundle + } + } + } + + private val activityViewModel: RssSortViewModel + get() = getViewModelOfActivity(RssSortViewModel::class.java) override val viewModel: RssArticlesViewModel get() = getViewModel(RssArticlesViewModel::class.java) - - override lateinit var adapter: RssArticlesAdapter - private val editSource = 12319 + lateinit var adapter: RssArticlesAdapter private lateinit var loadMoreView: LoadMoreView private var rssArticlesData: LiveData>? = null - override fun onActivityCreated(savedInstanceState: Bundle?) { - viewModel.callBack = this - viewModel.titleLiveData.observe(this, Observer { - title_bar.title = it - }) + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { + viewModel.init(arguments) initView() - viewModel.initData(intent) { - initData() - refresh_recycler_view.startLoading() - } - } - - override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.rss_articles, menu) - return super.onCompatCreateOptionsMenu(menu) - } - - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let { - startActivityForResult(editSource, Pair("data", it)) - } - R.id.menu_clear -> { - viewModel.url?.let { - refresh_progress_bar.isAutoLoading = true - viewModel.clearArticles() - } - } - } - return super.onCompatOptionsItemSelected(item) + refresh_recycler_view.startLoading() + initView() + initData() } private fun initView() { ATH.applyEdgeEffectColor(recycler_view) - recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(VerticalDivider(this)) - adapter = RssArticlesAdapter(this, this) + recycler_view.layoutManager = LinearLayoutManager(requireContext()) + recycler_view.addItemDecoration(VerticalDivider(requireContext())) + adapter = RssArticlesAdapter(requireContext(), this) recycler_view.adapter = adapter - loadMoreView = LoadMoreView(this) + loadMoreView = LoadMoreView(requireContext()) adapter.addFooterView(loadMoreView) refresh_recycler_view.onRefreshStart = { - viewModel.url?.let { - viewModel.loadContent() + activityViewModel.rssSource?.let { + viewModel.loadContent(it) } } recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { @@ -91,10 +75,10 @@ class RssArticlesActivity : VMBaseActivity(R.layout.activi } private fun initData() { - viewModel.url?.let { + activityViewModel.url?.let { rssArticlesData?.removeObservers(this) - rssArticlesData = App.db.rssArticleDao().liveByOrigin(it) - rssArticlesData?.observe(this, Observer { list -> + rssArticlesData = App.db.rssArticleDao().liveByOriginSort(it, viewModel.sortName) + rssArticlesData?.observe(viewLifecycleOwner, Observer { list -> adapter.setItems(list) }) } @@ -104,21 +88,25 @@ class RssArticlesActivity : VMBaseActivity(R.layout.activi if (viewModel.isLoading) return if (loadMoreView.hasMore && adapter.getActualItemCount() > 0) { loadMoreView.rotate_loading.show() - viewModel.loadMore() + activityViewModel.rssSource?.let { + viewModel.loadMore(it) + } } } - override fun loadFinally(hasMore: Boolean) { - refresh_recycler_view.stopLoading() - if (hasMore) { - loadMoreView.startLoad() - } else { - loadMoreView.noMore() - } + override fun observeLiveBus() { + viewModel.loadFinally.observe(viewLifecycleOwner, Observer { + refresh_recycler_view.stopLoading() + if (it) { + loadMoreView.startLoad() + } else { + loadMoreView.noMore() + } + }) } override fun readRss(rssArticle: RssArticle) { - viewModel.read(rssArticle) + activityViewModel.read(rssArticle) startActivity( Pair("title", rssArticle.title), Pair("origin", rssArticle.origin), diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt index 61a2b14be..8d2d06df8 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesViewModel.kt @@ -1,124 +1,93 @@ package io.legado.app.ui.rss.article import android.app.Application -import android.content.Intent +import android.os.Bundle import androidx.lifecycle.MutableLiveData import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.RssArticle -import io.legado.app.data.entities.RssReadRecord import io.legado.app.data.entities.RssSource import io.legado.app.model.Rss -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext - class RssArticlesViewModel(application: Application) : BaseViewModel(application) { - var callBack: CallBack? = null - var url: String? = null - var rssSource: RssSource? = null - val titleLiveData = MutableLiveData() + val loadFinally = MutableLiveData() var isLoading = true var order = System.currentTimeMillis() private var nextPageUrl: String? = null + var sortName: String = "" + var sortUrl: String = "" - fun initData(intent: Intent, finally: () -> Unit) { - execute { - url = intent.getStringExtra("url") - url?.let { url -> - rssSource = App.db.rssSourceDao().getByKey(url) - rssSource?.let { - titleLiveData.postValue(it.sourceName) - } ?: let { - rssSource = RssSource(sourceUrl = url) - } - } - }.onFinally { - finally() + fun init(bundle: Bundle?) { + bundle?.let { + sortName = it.getString("sortName") ?: "" + sortUrl = it.getString("sortUrl") ?: "" } } - fun loadContent() { + + fun loadContent(rssSource: RssSource) { isLoading = true - rssSource?.let { rssSource -> - Rss.getArticles(rssSource, null) - .onSuccess(IO) { - nextPageUrl = it.nextPageUrl - it.articles.let { list -> - list.forEach { rssArticle -> - rssArticle.order = order-- - } - App.db.rssArticleDao().insert(*list.toTypedArray()) - if (!rssSource.ruleNextPage.isNullOrEmpty()) { - App.db.rssArticleDao().clearOld(url!!, order) - withContext(Main) { - callBack?.loadFinally(true) - } - } else { - withContext(Main) { - callBack?.loadFinally(false) - } + Rss.getArticles(sortName, sortUrl, rssSource, null) + .onSuccess(Dispatchers.IO) { + nextPageUrl = it.nextPageUrl + it.articles.let { list -> + list.forEach { rssArticle -> + rssArticle.order = order-- + } + App.db.rssArticleDao().insert(*list.toTypedArray()) + if (!rssSource.ruleNextPage.isNullOrEmpty()) { + App.db.rssArticleDao().clearOld(rssSource.sourceUrl, sortName, order) + loadFinally.postValue(true) + } else { + withContext(Dispatchers.Main) { + loadFinally.postValue(false) } - isLoading = false - } - }.onError { - toast(it.localizedMessage) + isLoading = false } - } + }.onError { + toast(it.localizedMessage) + } } - fun loadMore() { + fun loadMore(rssSource: RssSource) { isLoading = true - val source = rssSource val pageUrl = nextPageUrl - if (source != null && !pageUrl.isNullOrEmpty()) { - Rss.getArticles(source, pageUrl) - .onSuccess(IO) { + if (!pageUrl.isNullOrEmpty()) { + Rss.getArticles(sortName, pageUrl, rssSource, pageUrl) + .onSuccess(Dispatchers.IO) { nextPageUrl = it.nextPageUrl - it.articles.let { list -> - if (list.isEmpty()) { - callBack?.loadFinally(false) - return@let - } - callBack?.adapter?.getItems()?.let { adapterItems -> - if (adapterItems.contains(list.first())) { - callBack?.loadFinally(false) - } else { - list.forEach { rssArticle -> - rssArticle.order = order-- - } - App.db.rssArticleDao().insert(*list.toTypedArray()) - } - } - } - isLoading = false + loadMoreSuccess(it.articles) + } + .onError { + loadFinally.postValue(false) } } else { - callBack?.loadFinally(false) + loadFinally.postValue(false) } } - fun read(rssArticle: RssArticle) { - execute { - App.db.rssArticleDao().insertRecord(RssReadRecord(rssArticle.link)) - } - } - - fun clearArticles() { - execute { - url?.let { - App.db.rssArticleDao().delete(it) + private fun loadMoreSuccess(articles: MutableList) { + articles.let { list -> + if (list.isEmpty()) { + loadFinally.postValue(false) + return@let + } + val firstArticle = list.first() + val dbArticle = App.db.rssArticleDao() + .get(firstArticle.origin, firstArticle.link) + if (dbArticle != null) { + loadFinally.postValue(false) + } else { + list.forEach { rssArticle -> + rssArticle.order = order-- + } + App.db.rssArticleDao().insert(*list.toTypedArray()) } - order = System.currentTimeMillis() - }.onSuccess { - loadContent() } + isLoading = false } - interface CallBack { - var adapter: RssArticlesAdapter - fun loadFinally(hasMore: Boolean) - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt new file mode 100644 index 000000000..2d45bcd70 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortActivity.kt @@ -0,0 +1,100 @@ +package io.legado.app.ui.rss.article + +import android.app.Activity +import android.content.Intent +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.fragment.app.Fragment +import androidx.fragment.app.FragmentManager +import androidx.fragment.app.FragmentStatePagerAdapter +import androidx.lifecycle.Observer +import io.legado.app.R +import io.legado.app.base.VMBaseActivity +import io.legado.app.ui.rss.source.edit.RssSourceEditActivity +import io.legado.app.utils.getViewModel +import io.legado.app.utils.gone +import io.legado.app.utils.visible +import kotlinx.android.synthetic.main.activity_rss_artivles.* +import org.jetbrains.anko.startActivityForResult + +class RssSortActivity : VMBaseActivity(R.layout.activity_rss_artivles) { + + override val viewModel: RssSortViewModel + get() = getViewModel(RssSortViewModel::class.java) + private val editSource = 12319 + private val fragments = linkedMapOf() + private lateinit var adapter: TabFragmentPageAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + adapter = TabFragmentPageAdapter(supportFragmentManager) + tab_layout.setupWithViewPager(view_pager) + view_pager.adapter = adapter + viewModel.titleLiveData.observe(this, Observer { + title_bar.title = it + }) + viewModel.initData(intent) { + upFragments() + } + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.rss_articles, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_edit_source -> viewModel.rssSource?.sourceUrl?.let { + startActivityForResult(editSource, Pair("data", it)) + } + R.id.menu_clear -> { + viewModel.url?.let { + viewModel.clearArticles() + } + } + } + return super.onCompatOptionsItemSelected(item) + } + + private fun upFragments() { + fragments.clear() + viewModel.rssSource?.sortUrls()?.forEach { + fragments[it.key] = RssArticlesFragment.create(it.key, it.value) + } + if (fragments.size == 1) { + tab_layout.gone() + } else { + tab_layout.visible() + } + adapter.notifyDataSetChanged() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + editSource -> if (resultCode == Activity.RESULT_OK) { + viewModel.initData(intent) { + upFragments() + } + } + } + } + + private inner class TabFragmentPageAdapter internal constructor(fm: FragmentManager) : + FragmentStatePagerAdapter(fm, BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) { + + override fun getPageTitle(position: Int): CharSequence? { + return fragments.keys.elementAt(position) + } + + override fun getItem(position: Int): Fragment { + return fragments.values.elementAt(position) + } + + override fun getCount(): Int { + return fragments.size + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt new file mode 100644 index 000000000..4b1149f2e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssSortViewModel.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.rss.article + +import android.app.Application +import android.content.Intent +import androidx.lifecycle.MutableLiveData +import io.legado.app.App +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssReadRecord +import io.legado.app.data.entities.RssSource + + +class RssSortViewModel(application: Application) : BaseViewModel(application) { + var url: String? = null + var rssSource: RssSource? = null + val titleLiveData = MutableLiveData() + var order = System.currentTimeMillis() + + fun initData(intent: Intent, finally: () -> Unit) { + execute { + url = intent.getStringExtra("url") + url?.let { url -> + rssSource = App.db.rssSourceDao().getByKey(url) + rssSource?.let { + titleLiveData.postValue(it.sourceName) + } ?: let { + rssSource = RssSource(sourceUrl = url) + } + } + }.onFinally { + finally() + } + } + + fun read(rssArticle: RssArticle) { + execute { + App.db.rssArticleDao().insertRecord(RssReadRecord(rssArticle.link)) + } + } + + fun clearArticles() { + execute { + url?.let { + App.db.rssArticleDao().delete(it) + } + order = System.currentTimeMillis() + }.onSuccess { + + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt index 73c55961b..e11293cc4 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssActivity.kt @@ -24,7 +24,7 @@ import org.jetbrains.anko.toast import org.jsoup.Jsoup -class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_read), +class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_read, false), FileChooserDialog.CallBack, ReadRssViewModel.CallBack { @@ -252,6 +252,7 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r web_view.settings.javaScriptEnabled = true web_view.evaluateJavascript("document.documentElement.outerHTML") { val html = StringEscapeUtils.unescapeJson(it) + .replace("^\"|\"$".toRegex(), "") Jsoup.parse(html).text() viewModel.readAloud(Jsoup.parse(html).textArray()) } diff --git a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt index f5b84642f..72af31aca 100644 --- a/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/read/ReadRssViewModel.kt @@ -138,17 +138,28 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } fun clHtml(content: String): String { - return if (content.contains(" - $content - """.trimIndent() + return when { + !rssSource?.style.isNullOrEmpty() -> { + """ + + $content + """.trimIndent() + } + content.contains(" + $content + """.trimIndent() + } } } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt index 944135b45..bd070639a 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditActivity.kt @@ -135,6 +135,7 @@ class RssSourceEditActivity : add(EditEntity("sourceUrl", rssSource?.sourceUrl, R.string.source_url)) add(EditEntity("sourceIcon", rssSource?.sourceIcon, R.string.source_icon)) add(EditEntity("sourceGroup", rssSource?.sourceGroup, R.string.source_group)) + add(EditEntity("sortUrl", rssSource?.sortUrl, R.string.sort_url)) add(EditEntity("ruleArticles", rssSource?.ruleArticles, R.string.r_articles)) add(EditEntity("ruleNextPage", rssSource?.ruleNextPage, R.string.r_next)) add(EditEntity("ruleTitle", rssSource?.ruleTitle, R.string.r_title)) @@ -143,6 +144,7 @@ class RssSourceEditActivity : add(EditEntity("ruleImage", rssSource?.ruleImage, R.string.r_image)) add(EditEntity("ruleLink", rssSource?.ruleLink, R.string.r_link)) add(EditEntity("ruleContent", rssSource?.ruleContent, R.string.r_content)) + add(EditEntity("style", rssSource?.style, R.string.r_style)) add(EditEntity("header", rssSource?.header, R.string.source_http_header)) } adapter.editEntities = sourceEntities @@ -159,6 +161,7 @@ class RssSourceEditActivity : "sourceUrl" -> source.sourceUrl = it.value ?: "" "sourceIcon" -> source.sourceIcon = it.value ?: "" "sourceGroup" -> source.sourceGroup = it.value + "sortUrl" -> source.sortUrl = it.value "ruleArticles" -> source.ruleArticles = it.value "ruleNextPage" -> source.ruleNextPage = it.value "ruleTitle" -> source.ruleTitle = it.value @@ -167,6 +170,7 @@ class RssSourceEditActivity : "ruleImage" -> source.ruleImage = it.value "ruleLink" -> source.ruleLink = it.value "ruleContent" -> source.ruleContent = it.value + "style" -> source.style = it.value "header" -> source.header = it.value } } @@ -181,7 +185,7 @@ class RssSourceEditActivity : return true } - override fun sendText(text: String) { + private fun insertText(text: String) { if (text.isBlank()) return val view = window.decorView.findFocus() if (view is EditText) { @@ -196,6 +200,14 @@ class RssSourceEditActivity : } } + override fun sendText(text: String) { + if (text == AppConst.keyboardToolChars[0]) { + insertText(AppConst.urlOption) + } else { + insertText(text) + } + } + private fun showKeyboardTopPopupWindow() { mSoftKeyboardTool?.let { if (it.isShowing) return diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt index 300d7468c..65c5bc093 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceViewModel.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.rss.source.manage import android.app.Application +import android.net.Uri import android.text.TextUtils import androidx.documentfile.provider.DocumentFile import com.jayway.jsonpath.JsonPath @@ -134,9 +135,20 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) fun importSourceFromFilePath(path: String, finally: (msg: String) -> Unit) { execute { - val file = File(path) - if (file.exists()) { - GSON.fromJsonArray(file.readText())?.let { + val content = if (path.isContentPath()) { + //在前面被解码了,如果不进行编码,中文会无法识别 + val newPath = Uri.encode(path, ":/.") + DocumentFile.fromSingleUri(context, Uri.parse(newPath))?.readText(context) + } else { + val file = File(path) + if (file.exists()) { + file.readText() + } else { + null + } + } + if (null != content) { + GSON.fromJsonArray(content)?.let { App.db.rssSourceDao().insert(*it.toTypedArray()) } } @@ -191,7 +203,7 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } private fun importSourceUrl(url: String): Int { - HttpHelper.simpleGet(url)?.let { body -> + HttpHelper.simpleGet(url, "UTF-8")?.let { body -> val sources = mutableListOf() val items: List> = jsonPath.parse(body).read("$") for (item in items) { @@ -202,8 +214,8 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } App.db.rssSourceDao().insert(*sources.toTypedArray()) return sources.size - } return 0 } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt index fdf77bc85..63455fc9a 100644 --- a/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt +++ b/app/src/main/java/io/legado/app/ui/welcome/WelcomeActivity.kt @@ -2,7 +2,7 @@ package io.legado.app.ui.welcome import android.content.Intent import android.os.Bundle -import com.github.houbb.opencc4j.util.ZhConverterUtil +import com.hankcs.hanlp.HanLP import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseActivity @@ -37,8 +37,8 @@ open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { .clearExpired(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) //初始化简繁转换引擎 when (AppConfig.chineseConverterType) { - 1 -> ZhConverterUtil.toSimple("初始化") - 2 -> ZhConverterUtil.toTraditional("初始化") + 1 -> HanLP.convertToSimplifiedChinese("初始化") + 2 -> HanLP.convertToTraditionalChinese("初始化") else -> null } } diff --git a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt index f11db64e8..d9725f019 100644 --- a/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/BatteryView.kt @@ -14,6 +14,7 @@ class BatteryView(context: Context, attrs: AttributeSet?) : AppCompatTextView(co private val batteryPaint = Paint() private val outFrame = Rect() private val polar = Rect() + var isBattery = false init { setPadding(4.dp, 0, 6.dp, 0) @@ -35,11 +36,12 @@ class BatteryView(context: Context, attrs: AttributeSet?) : AppCompatTextView(co override fun onDraw(canvas: Canvas) { super.onDraw(canvas) + if (!isBattery) return outFrame.set( 1.dp, - layout.getLineTop(0) + 2.dp, + layout.getLineBaseline(0) + layout.getLineAscent(0) + 2.dp, width - 3.dp, - layout.getLineBottom(0) - 2.dp + layout.getLineBaseline(0) + 2.dp ) val dj = (outFrame.bottom - outFrame.top) / 3 polar.set( diff --git a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt index 9c1c75d5e..dc6ba868b 100644 --- a/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt @@ -23,7 +23,8 @@ class TextDialog : BaseDialogFragment() { fragmentManager: FragmentManager, content: String?, mode: Int = 0, - time: Long = 0 + time: Long = 0, + autoClose: Boolean = false ) { TextDialog().apply { val bundle = Bundle() @@ -32,6 +33,7 @@ class TextDialog : BaseDialogFragment() { bundle.putLong("time", time) arguments = bundle isCancelable = false + this.autoClose = autoClose }.show(fragmentManager, "textDialog") } @@ -39,6 +41,8 @@ class TextDialog : BaseDialogFragment() { private var time = 0L + private var autoClose: Boolean = false + override fun onStart() { super.onStart() val dm = DisplayMetrics() @@ -79,6 +83,7 @@ class TextDialog : BaseDialogFragment() { if (time <= 0) { view.post { dialog?.setCancelable(true) + if (autoClose) dialog?.cancel() } } } @@ -86,6 +91,7 @@ class TextDialog : BaseDialogFragment() { } else { view.post { dialog?.setCancelable(true) + if (autoClose) dialog?.cancel() } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt index 4bd3610b5..9152be216 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/Preference.kt @@ -8,6 +8,7 @@ import android.view.View import android.widget.FrameLayout import android.widget.ImageView import android.widget.TextView +import androidx.core.view.isGone import androidx.core.view.isVisible import androidx.preference.PreferenceViewHolder import io.legado.app.R @@ -15,7 +16,8 @@ import io.legado.app.lib.theme.accentColor import org.jetbrains.anko.layoutInflater import kotlin.math.roundToInt -class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Preference(context, attrs) { +class Preference(context: Context, attrs: AttributeSet) : + androidx.preference.Preference(context, attrs) { init { // isPersistent = true @@ -24,8 +26,17 @@ class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Pr companion object { - fun bindView(context: Context, it: PreferenceViewHolder?, icon: Drawable?, title: CharSequence?, summary: CharSequence?, weightLayoutRes: Int?, viewId: Int?, - weightWidth: Int = 0, weightHeight: Int = 0): T? { + fun bindView( + context: Context, + it: PreferenceViewHolder?, + icon: Drawable?, + title: CharSequence?, + summary: CharSequence?, + weightLayoutRes: Int?, + viewId: Int?, + weightWidth: Int = 0, + weightHeight: Int = 0 + ): T? { if (it == null) return null val view = it.findViewById(R.id.preference_title) if (view is TextView) { @@ -35,7 +46,7 @@ class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Pr val tvSummary = it.findViewById(R.id.preference_desc) if (tvSummary is TextView) { tvSummary.text = summary - tvSummary.isVisible = summary != null && summary.isNotEmpty() + tvSummary.isGone = summary.isNullOrEmpty() } val iconView = it.findViewById(R.id.preference_icon) @@ -69,9 +80,11 @@ class Preference(context: Context, attrs: AttributeSet) : androidx.preference.Pr if (weightWidth > 0 || weightHeight > 0) { val lp = lay.layoutParams if (weightHeight > 0) - lp.height = (context.resources.displayMetrics.density * weightHeight).roundToInt() + lp.height = + (context.resources.displayMetrics.density * weightHeight).roundToInt() if (weightWidth > 0) - lp.width = (context.resources.displayMetrics.density * weightWidth).roundToInt() + lp.width = + (context.resources.displayMetrics.density * weightWidth).roundToInt() lay.layoutParams = lp } else if (needRequestLayout) v.requestLayout() diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt index a7dcd2e6c..9cf6c2bfd 100644 --- a/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt @@ -1,7 +1,6 @@ package io.legado.app.ui.widget.prefs import android.content.Context -import android.graphics.Color import android.util.AttributeSet import android.view.View import android.widget.TextView @@ -9,7 +8,11 @@ import androidx.core.view.isVisible import androidx.preference.PreferenceCategory import androidx.preference.PreferenceViewHolder import io.legado.app.R +import io.legado.app.help.AppConfig +import io.legado.app.lib.theme.ColorUtils import io.legado.app.lib.theme.accentColor +import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.getCompatColor class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) { @@ -25,15 +28,27 @@ class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCate val view = it.findViewById(R.id.preference_title) if (view is TextView) { // && !view.isInEditMode view.text = title - view.setTextColor(context.accentColor) //设置title文本的颜色 + if (view.isInEditMode) { + view.setTextColor(context.getCompatColor(R.color.colorAccent)) + } else { + view.setBackgroundColor(context.backgroundColor) + view.setTextColor(context.accentColor) + } view.isVisible = title != null && title.isNotEmpty() val da = it.findViewById(R.id.preference_divider_above) + val dividerColor = if (AppConfig.isNightTheme) { + ColorUtils.shiftColor(context.backgroundColor, 1.05f) + } else { + ColorUtils.shiftColor(context.backgroundColor, 0.95f) + } if (da is View) { + da.setBackgroundColor(dividerColor) da.isVisible = it.isDividerAllowedAbove } val db = it.findViewById(R.id.preference_divider_below) if (db is View) { + db.setBackgroundColor(dividerColor) db.isVisible = it.isDividerAllowedBelow } } diff --git a/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt b/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt new file mode 100644 index 000000000..b7ef740cc --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/ConstraintUtil.kt @@ -0,0 +1,280 @@ +package io.legado.app.utils + +import androidx.annotation.IdRes +import androidx.constraintlayout.widget.ConstraintLayout +import androidx.constraintlayout.widget.ConstraintSet +import androidx.transition.TransitionManager + + +class ConstraintUtil(private val constraintLayout: ConstraintLayout) { + + private var begin: ConstraintBegin? = null + private val applyConstraintSet = ConstraintSet() + private val resetConstraintSet = ConstraintSet() + + init { + resetConstraintSet.clone(constraintLayout) + } + + /** + * 开始修改 + */ + fun begin(): ConstraintBegin { + synchronized(ConstraintBegin::class.java) { + if (begin == null) { + begin = ConstraintBegin(constraintLayout, applyConstraintSet) + } + } + applyConstraintSet.clone(constraintLayout) + return begin!! + } + + /** + * 带动画的修改 + * @return + */ + fun beginWithAnim(): ConstraintBegin? { + TransitionManager.beginDelayedTransition(constraintLayout) + return begin() + } + + /** + * 重置 + */ + fun reSet() { + resetConstraintSet.applyTo(constraintLayout) + } + + /** + * 带动画的重置 + */ + fun reSetWidthAnim() { + TransitionManager.beginDelayedTransition(constraintLayout) + resetConstraintSet.applyTo(constraintLayout) + } + +} + +class ConstraintBegin( + private val constraintLayout: ConstraintLayout, + private val applyConstraintSet: ConstraintSet +) { + + /** + * 清除关系

+ * 注意:这里不仅仅会清除关系,还会清除对应控件的宽高为 w:0,h:0 + * @param viewIds + * @return + */ + fun clear(@IdRes vararg viewIds: Int): ConstraintBegin { + for (viewId in viewIds) { + applyConstraintSet.clear(viewId) + } + return this + } + + /** + * 清除某个控件的,某个关系 + * @param viewId + * @param anchor + * @return + */ + fun clear(viewId: Int, anchor: Int): ConstraintBegin { + applyConstraintSet.clear(viewId, anchor) + return this + } + + fun setHorizontalWeight(viewId: Int, weight: Float): ConstraintBegin { + applyConstraintSet.setHorizontalWeight(viewId, weight) + return this + } + + fun setVerticalWeight(viewId: Int, weight: Float): ConstraintBegin { + applyConstraintSet.setVerticalWeight(viewId, weight) + return this + } + + /** + * 为某个控件设置 margin + * @param viewId 某个控件ID + * @param left marginLeft + * @param top marginTop + * @param right marginRight + * @param bottom marginBottom + * @return + */ + fun setMargin( + @IdRes viewId: Int, + left: Int, + top: Int, + right: Int, + bottom: Int + ): ConstraintBegin { + setMarginLeft(viewId, left) + setMarginTop(viewId, top) + setMarginRight(viewId, right) + setMarginBottom(viewId, bottom) + return this + } + + /** + * 为某个控件设置 marginLeft + * @param viewId 某个控件ID + * @param left marginLeft + * @return + */ + fun setMarginLeft(@IdRes viewId: Int, left: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.LEFT, left) + return this + } + + /** + * 为某个控件设置 marginRight + * @param viewId 某个控件ID + * @param right marginRight + * @return + */ + fun setMarginRight(@IdRes viewId: Int, right: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.RIGHT, right) + return this + } + + /** + * 为某个控件设置 marginTop + * @param viewId 某个控件ID + * @param top marginTop + * @return + */ + fun setMarginTop(@IdRes viewId: Int, top: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.TOP, top) + return this + } + + /** + * 为某个控件设置marginBottom + * @param viewId 某个控件ID + * @param bottom marginBottom + * @return + */ + fun setMarginBottom(@IdRes viewId: Int, bottom: Int): ConstraintBegin { + applyConstraintSet.setMargin(viewId, ConstraintSet.BOTTOM, bottom) + return this + } + + /** + * 为某个控件设置关联关系 left_to_left_of + * @param startId + * @param endId + * @return + */ + fun leftToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.LEFT) + return this + } + + /** + * 为某个控件设置关联关系 left_to_right_of + * @param startId + * @param endId + * @return + */ + fun leftToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.LEFT, endId, ConstraintSet.RIGHT) + return this + } + + /** + * 为某个控件设置关联关系 top_to_top_of + * @param startId + * @param endId + * @return + */ + fun topToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.TOP) + return this + } + + /** + * 为某个控件设置关联关系 top_to_bottom_of + * @param startId + * @param endId + * @return + */ + fun topToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.TOP, endId, ConstraintSet.BOTTOM) + return this + } + + /** + * 为某个控件设置关联关系 right_to_left_of + * @param startId + * @param endId + * @return + */ + fun rightToLeftOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.LEFT) + return this + } + + /** + * 为某个控件设置关联关系 right_to_right_of + * @param startId + * @param endId + * @return + */ + fun rightToRightOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.RIGHT, endId, ConstraintSet.RIGHT) + return this + } + + /** + * 为某个控件设置关联关系 bottom_to_bottom_of + * @param startId + * @param endId + * @return + */ + fun bottomToBottomOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.BOTTOM) + return this + } + + /** + * 为某个控件设置关联关系 bottom_to_top_of + * @param startId + * @param endId + * @return + */ + fun bottomToTopOf(@IdRes startId: Int, @IdRes endId: Int): ConstraintBegin { + applyConstraintSet.connect(startId, ConstraintSet.BOTTOM, endId, ConstraintSet.TOP) + return this + } + + /** + * 为某个控件设置宽度 + * @param viewId + * @param width + * @return + */ + fun setWidth(@IdRes viewId: Int, width: Int): ConstraintBegin { + applyConstraintSet.constrainWidth(viewId, width) + return this + } + + /** + * 某个控件设置高度 + * @param viewId + * @param height + * @return + */ + fun setHeight(@IdRes viewId: Int, height: Int): ConstraintBegin { + applyConstraintSet.constrainHeight(viewId, height) + return this + } + + /** + * 提交应用生效 + */ + fun commit() { + applyConstraintSet.applyTo(constraintLayout) + } +} diff --git a/app/src/main/java/io/legado/app/utils/GsonExtensions.kt b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt index 13b105618..773cd8030 100644 --- a/app/src/main/java/io/legado/app/utils/GsonExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/GsonExtensions.kt @@ -1,22 +1,26 @@ package io.legado.app.utils -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import com.google.gson.JsonSyntaxException +import com.google.gson.* +import com.google.gson.internal.LinkedTreeMap import com.google.gson.reflect.TypeToken import org.jetbrains.anko.attempt import java.lang.reflect.ParameterizedType import java.lang.reflect.Type +import kotlin.math.ceil + val GSON: Gson by lazy { GsonBuilder() + .registerTypeAdapter( + object : TypeToken?>() {}.type, + MapDeserializerDoubleAsIntFix() + ) .disableHtmlEscaping() .setPrettyPrinting() .create() } -inline fun genericType() = object : TypeToken() {}.type - +inline fun genericType(): Type = object : TypeToken() {}.type @Throws(JsonSyntaxException::class) inline fun Gson.fromJsonObject(json: String?): T? {//可转成任意类型 @@ -41,3 +45,68 @@ class ParameterizedTypeImpl(private val clazz: Class<*>) : ParameterizedType { override fun getActualTypeArguments(): Array = arrayOf(clazz) } + +/** + * 修复Int变为Double的问题 + */ +class MapDeserializerDoubleAsIntFix : + JsonDeserializer?> { + + @Throws(JsonParseException::class) + override fun deserialize( + jsonElement: JsonElement, + type: Type, + jsonDeserializationContext: JsonDeserializationContext + ): Map? { + @Suppress("unchecked_cast") + return read(jsonElement) as? Map + } + + fun read(`in`: JsonElement): Any? { + when { + `in`.isJsonArray -> { + val list: MutableList = ArrayList() + val arr = `in`.asJsonArray + for (anArr in arr) { + list.add(read(anArr)) + } + return list + } + `in`.isJsonObject -> { + val map: MutableMap = + LinkedTreeMap() + val obj = `in`.asJsonObject + val entitySet = + obj.entrySet() + for ((key, value) in entitySet) { + map[key] = read(value) + } + return map + } + `in`.isJsonPrimitive -> { + val prim = `in`.asJsonPrimitive + when { + prim.isBoolean -> { + return prim.asBoolean + } + prim.isString -> { + return prim.asString + } + prim.isNumber -> { + val num: Number = prim.asNumber + // here you can handle double int/long values + // and return any type you want + // this solution will transform 3.0 float to long values + return if (ceil(num.toDouble()) == num.toLong().toDouble()) { + num.toLong() + } else { + num.toDouble() + } + } + } + } + } + return null + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt index 0c59f05a3..b706a8d5e 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -11,9 +11,9 @@ import java.util.regex.Pattern @Suppress("unused") object NetworkUtils { fun getUrl(response: Response<*>): String { - val networkResponse = response.raw().networkResponse - return networkResponse?.request?.url?.toString() - ?: response.raw().request.url.toString() + val networkResponse = response.raw().networkResponse() + return networkResponse?.request()?.url()?.toString() + ?: response.raw().request().url().toString() } private val notNeedEncoding: BitSet by lazy { diff --git a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt index e37539851..7ed6d8cd6 100644 --- a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt @@ -1,5 +1,6 @@ package io.legado.app.utils +import android.annotation.SuppressLint import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas @@ -11,8 +12,11 @@ import android.view.inputmethod.InputMethodManager import android.widget.RadioGroup import android.widget.SeekBar import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.view.menu.MenuPopupHelper +import androidx.appcompat.widget.PopupMenu import androidx.core.view.get import io.legado.app.App +import java.lang.reflect.Field private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? { @@ -107,4 +111,17 @@ fun RadioGroup.getCheckedIndex(): Int { fun RadioGroup.checkByIndex(index: Int) { check(get(index).id) +} + +@SuppressLint("RestrictedApi") +fun PopupMenu.show(x: Int, y: Int) { + try { + val field: Field = this.javaClass.getDeclaredField("mPopup") + field.isAccessible = true + (field.get(this) as MenuPopupHelper).show(x, y) + } catch (e: NoSuchFieldException) { + e.printStackTrace() + } catch (e: IllegalAccessException) { + e.printStackTrace() + } } \ No newline at end of file diff --git a/app/src/main/res/drawable/bg_prefs_color.xml b/app/src/main/res/drawable/bg_prefs_color.xml index ff4a5de4e..beb057eab 100644 --- a/app/src/main/res/drawable/bg_prefs_color.xml +++ b/app/src/main/res/drawable/bg_prefs_color.xml @@ -1,10 +1,4 @@ - - - - - - - - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout-land/activity_book_info.xml b/app/src/main/res/layout-land/activity_book_info.xml index 1dc7d789c..518501721 100644 --- a/app/src/main/res/layout-land/activity_book_info.xml +++ b/app/src/main/res/layout-land/activity_book_info.xml @@ -361,7 +361,8 @@ android:text="@string/book_intro" android:textColor="@color/tv_text_secondary" android:textSize="14sp" - android:visibility="visible" /> + android:visibility="visible" + tools:ignore="RtlHardcoded,RtlSymmetry" /> diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml index ee0d050bb..6d05af889 100644 --- a/app/src/main/res/layout/activity_book_info.xml +++ b/app/src/main/res/layout/activity_book_info.xml @@ -155,7 +155,7 @@ android:text="@string/author" android:textColor="@color/tv_text_summary" android:textSize="13sp" - tools:ignore="NestedWeights" /> + tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" /> @@ -189,7 +189,7 @@ android:text="@string/origin_format" android:textColor="@color/tv_text_summary" android:textSize="13sp" - tools:ignore="NestedWeights" /> + tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" /> + tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" /> @@ -267,7 +267,7 @@ android:text="@string/group_s" android:textColor="@color/tv_text_summary" android:textSize="13sp" - tools:ignore="NestedWeights" /> + tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" /> + tools:ignore="NestedWeights,RtlHardcoded,RtlSymmetry" /> + android:visibility="visible" + tools:ignore="RtlHardcoded,RtlSymmetry" /> @@ -349,7 +350,7 @@ + android:background="@color/background" /> - + android:layout_height="wrap_content" /> - - - + android:layout_height="wrap_content" + app:tabMode="scrollable" /> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_read.xml b/app/src/main/res/layout/activity_rss_read.xml index ea4131764..6eb28b4f8 100644 --- a/app/src/main/res/layout/activity_rss_read.xml +++ b/app/src/main/res/layout/activity_rss_read.xml @@ -2,6 +2,7 @@ + android:layout_height="wrap_content" + app:fitStatusBar="false" /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_rss_articles.xml b/app/src/main/res/layout/fragment_rss_articles.xml new file mode 100644 index 000000000..5ca0c9574 --- /dev/null +++ b/app/src/main/res/layout/fragment_rss_articles.xml @@ -0,0 +1,15 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_book_page.xml b/app/src/main/res/layout/view_book_page.xml index 3f09664a3..95fd4f1f3 100644 --- a/app/src/main/res/layout/view_book_page.xml +++ b/app/src/main/res/layout/view_book_page.xml @@ -1,38 +1,74 @@ - - + + + android:gravity="center_vertical"> - + + + + + android:textSize="12sp" + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - - + - + android:gravity="center_vertical"> - + android:textSize="12sp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/tv_footer_right" + app:layout_constraintTop_toTopOf="parent" /> - + + + android:visibility="gone" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toRightOf="parent" + app:layout_constraintTop_toTopOf="parent" /> - + - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_preference.xml b/app/src/main/res/layout/view_preference.xml index 2b67899f9..acae86b8e 100644 --- a/app/src/main/res/layout/view_preference.xml +++ b/app/src/main/res/layout/view_preference.xml @@ -11,6 +11,7 @@ android:minHeight="42dp" android:clickable="true" android:orientation="horizontal" + android:gravity="center_vertical" android:focusable="true"> + android:layout_height="wrap_content" + android:layout_marginTop="8dp" /> + @@ -12,13 +13,13 @@ + tools:ignore="RtlHardcoded,RtlSymmetry" /> - + - + - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/change_source_item.xml b/app/src/main/res/menu/change_source_item.xml new file mode 100644 index 000000000..49c724fee --- /dev/null +++ b/app/src/main/res/menu/change_source_item.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/rss_main_item.xml b/app/src/main/res/menu/rss_main_item.xml new file mode 100644 index 000000000..19dc2f30a --- /dev/null +++ b/app/src/main/res/menu/rss_main_item.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/arrays.xml b/app/src/main/res/values-zh-rHK/arrays.xml new file mode 100644 index 000000000..ebe6c25dd --- /dev/null +++ b/app/src/main/res/values-zh-rHK/arrays.xml @@ -0,0 +1,52 @@ + + + + + 度小美 + 度小宇 + 度逍遙 + 度丫丫 + 度小嬌 + 度米朵 + 度博文 + 度小童 + 度小萌 + 百度騷男 + 百度評書 + 百度主持 + + + + 跟隨系統 + 亮色主題 + 暗色主題 + + + + 自動 + 黑色 + 白色 + 跟隨背景 + + + + 默認 + 1分鐘 + 2分鐘 + 3分鐘 + 常亮 + + + + 關閉 + 繁體轉簡體 + 簡體轉繁體 + + + + 系統默認字體 + 系統襯線字體 + 系統等寬字體 + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml new file mode 100644 index 000000000..7aa554fdb --- /dev/null +++ b/app/src/main/res/values-zh-rHK/strings.xml @@ -0,0 +1,692 @@ + + + 閲讀 + 閲讀·搜尋 + 閲讀需要訪問存儲卡權限,請前往「設定」—「應用程式權限」—開啟所需要的權限 + + + Home + 還原 + 導入閲讀數據 + 創建子文件夾 + 創建 legado 文件夾作爲備份路徑 + 備份路徑 + 導入舊版數據 + 導入 Github 數據 + 淨化替換 + Send + + 提示 + 取消 + 確認 + 去設定 + 無法轉跳至設定介面 + + 點擊重試 + 正在加載 + 提醒 + 編輯 + 刪除 + 替換 + 替換淨化 + 配置替換淨化規則 + 暫無 + 啟用 + 替換淨化-搜尋 + 書架 + 收藏夾 + 收藏 + 已收藏 + 未收藏 + 訂閲 + 全部 + 最近閲讀 + 最後閲讀 + 更新日誌 + 書架還空着,快去添加吧! + 搜尋 + 下載 + 列表 + 網格三列 + 網格四列 + 網格五列 + 網格六列 + 書架佈局 + 視圖 + 書城 + 添加本地 + 書源 + 書源管理 + 新建/導入/編輯/管理書源 + 設定 + 主題設定 + 同主題/顏色相關的一些設定 + 其它設定 + 與功能相關的一些設定 + 關於 + 捐贈 + 退出 + 尚未保存,是否繼續編輯 + 閲讀樣式設定 + 版本 + 本地 + 搜尋 + 來源: %s + 最近: %s + 書名 + 最新: %s + 是否將《%s》放入書架? + 共 %s 個 Text 文件 + 載入中… + 重試 + Web 服務 + 啟用 Web 服務 + web 編輯書源 + http://%1$s:%2$d + 離線下載 + 離線下載 + 下載已選擇的章節到本地 + 換源 + + \u3000\u3000這是一款使用 Kotlin 全新開發的開源的閲讀應用程式,歡迎你的加入。關注公眾號[开源阅读]! + + + 閲讀3.0下載地址:\nhttps://play.google.com/store/apps/details?id=io.legado.app + + Version %s + 自動刷新 + 打開程式時自動更新書輯 + 自動下載最新章節 + 更新書輯時自動下載最新章節 + 備份與還原 + WebDav 設定 + WebDav 設定/還原舊版本數據 + 備份 + 還原 + 備份請給予存儲權限 + 還原請給予存儲權限 + 確認 + 取消 + 確認備份嗎? + 新備份會覆蓋原有備份。\n備份路徑YueDu + 確認還原嗎? + 還原成功會覆蓋原有書架。 + 備份成功 + 備份失敗 + 正在還原 + 還原成功 + 還原失敗 + 屏幕方向 + 跟隨傳感器 + 橫向 + 豎向 + 跟隨系統 + 免責聲明 + 共%d章 + 介面 + 亮度 + 目錄 + 下一章 + 上一章 + 隱藏狀態欄 + 閲讀介面隱藏狀態欄 + 朗讀 + 正在朗讀 + 點擊打開閲讀介面 + 播放 + 正在播放 + 點擊打開播放介面 + 播放暫停 + 返回 + 刷新 + 開始 + 停止 + 暫停 + 繼續 + 定時 + 朗讀暫停 + 正在朗讀(剩餘 %d 分鐘) + 閲讀介面隱藏導航欄 + 隱藏導航欄 + 導航欄顏色 + GitHub + 評分 + 發送電子郵件 + 無法打開 + 分享失敗 + 無章節 + 添加網址 + 添加書輯網址 + 背景 + 作者 + 作者: %s + 朗讀停止 + 清除緩存 + 成功清除緩存 + 保存 + 編輯源 + 編輯書源 + 禁用書源 + 新建書源 + 新建訂閲源 + 添加書輯 + 掃描 + 拷貝源 + 粘帖源 + 源規則説明 + 檢查更新 + 掃描 QR Code + 掃描本地圖片 + 規則説明 + 分享 + 應用程式分享 + 跟隨系統 + 添加 + 導入書源 + 本地導入 + 網絡導入 + 替換淨化 + 替換規則編輯 + 替換規則 + 替換為 + 封面 + + 音量鍵翻頁 + 點擊翻頁 + 點擊總是翻下一頁 + 翻頁動畫 + 屏幕超時 + 返回 + 菜單 + 調節 + 滾動條 + 清除緩存會刪除所有已保存的章節,確認是否清除? + 書源共享 + 規則替換名稱 + 替換規則為空或不滿足正則表達式要求 + 選擇操作 + 全選 + 全選 (%1$d/%2$d) + 取消 (%1$d/%2$d) + 深色模式 + 啟動頁 + 開始下載 + 取消下載 + 暫無任務 + 已下載 %1$d/%2$d + 導入選擇書輯 + 更新/搜尋線程數,太多會卡頓 + 切換圖標 + 刪除書輯 + 開始閲讀 + 數據載入中… + 載入失敗,點擊重試 + 內容簡介 + 簡介: %s + 打開外部書籍 + 來源: %s + 本地導入 + 導入在線規則 + 檢查更新間隔 + 按閲讀時間 + 按更新時間 + 按書名 + 手動排序 + 閲讀方式 + 排版 + 刪除所選 + 是否確認刪除? + 默認字體 + 發現 + 發現管理 + 沒有內容,去書源裏自定義吧! + 刪除所有 + 搜索歷史 + 清除 + 正文顯示標題 + 書源同步 + 無最新章節信息 + 顯示時間和電量 + 顯示分隔線 + 深色狀態欄圖標 + 內容 + 拷貝內容 + 一鍵緩存 + 這是一段測試文字\n\u3000\u3000只是讓你看看效果的 + 文字顏色和背景(長按自定義) + 沉浸式狀態欄 + 還剩 %d 章未下載 + 沒有選擇 + 長按輸入顏色值 + 加載中… + 追更區 + 養肥區 + 書籤 + 添加書籤 + 刪除 + 加載超時 + 關注: %s + 已拷貝 + 整理書架 + 這將會刪除所有書籍,請謹慎操作。 + 搜索書源 + 搜索訂閲源 + 搜索(共 %d 個書源) + 目錄(%d) + 加粗 + 字體 + 文字 + 軟件主頁 + + + + + 邊距 + 上邊距 + 下邊距 + 左邊距 + 右邊距 + 校驗書源 + 校驗所選 + 進度 %1$d/%2$d + 請安裝並選擇中文 TTS! + TTS 初始化失敗! + 簡繁轉換 + 關閉 + 簡轉繁 + 繁轉簡 + 翻頁模式 + %1$d 項 + 存儲卡: + 加入書架 + 加入書架 (%1$d) + 成功添加 %1$d 本書 + 請將字體文件放到 SD 根目錄 Fonts 文件夾下重新選擇 + 默認字體 + 選擇字體 + 字號 + 行距 + 段距 + 置頂 + 自動展開發現 + 默認展開第一組發現 + 當前線程數 %s + 朗讀語速 + 自動翻頁 + 停止自動翻頁 + 自動翻頁間隔 + 書籍信息 + 書籍信息編輯 + 默認打開書架 + 自動跳轉最近閲讀 + 替換範圍,選填書名或者源名 + 分組 + 內容緩存路徑 + 清理緩存 + 系統文件選擇器 + 新版本 + 下載更新 + 朗讀時音量鍵翻頁 + Tip 邊距跟隨邊距調整 + 允許更新 + 禁止更新 + 反選 + 搜索書名、作者 + 書名、作者、URL + 常見問題 + 顯示所有發現 + 關閉則只顯示勾選源的發現 + 更新目錄 + Txt目錄正則 + 設置編碼 + 倒序-順序 + 排序 + 智能排序 + 手動排序 + 拼音排序 + 滾動到頂部 + 滾動到底部 + 已讀: %s + 追更 + 養肥 + 完結 + 所有書籍 + 追更書籍 + 養肥書籍 + 完結書籍 + 本地書籍 + 狀態欄顏色透明 + 導航欄變色 + 導航欄根據夜間模式變化 + 放入書架 + 繼續閲讀 + 封面地址 + 覆蓋 + 滑動 + 仿真 + 滾動 + 無動畫 + 此書源使用了高級功能,請到捐贈裏點擊支付寶紅包搜索碼領取紅包開啟。 + 後台更新換源最新章節 + 開啟則會在軟件打開 1 分鐘後開始更新 + 書架 ToolBar 自動隱藏 + 滾動書架時 ToolBar 自動隱藏與顯示 + 登錄 + 登錄 %s + 成功 + 當前源沒有配置登陸地址 + + + 源名稱 (sourceName) + 源URL (sourceUrl) + 源分組 (sourceGroup) + 分類 Url + 登錄 URL(loginUrl) + 搜索地址 (url) + 發現地址規則 (url) + 書籍列表規則 (bookList) + 書名規則 (name) + 詳情頁 url 規則 (bookUrl) + 作者規則 (author) + 分類規則 (kind) + 簡介規則 (intro) + 封面規則 (coverUrl) + 最新章節規則 (lastChapter) + 字數規則 (wordCount) + 書籍 URL 正則 (bookUrlPattern) + 預處理規則 (bookInfoInit) + 目錄 URL 規則 (tocUrl) + 目錄下一頁規則 (nextTocUrl) + 目錄列表規則 (chapterList) + 章節名稱規則 (ChapterName) + 章節 URL 規則 (chapterUrl) + VIP 標識 (isVip) + 更新時間 (ChapterInfo) + 正文規則 (content) + 正文下一頁 URL 規則 (nextContentUrl) + webJs + 資源正則 (sourceRegex) + + 圖標 (sourceIcon) + 列表規則 (ruleArticles) + 列表下一頁規則 (ruleArticles) + 標題規則 (ruleTitle) + guid 規則 (ruleGuid) + 時間規則 (rulePubDate) + 類別規則 (ruleCategories) + 描述規則 (ruleDescription) + 圖片 url 規則 (ruleImage) + 內容規則 (ruleContent) + 樣式 (style) + 鏈接規則 (ruleLink) + + + + 沒有書源 + 書籍信息獲取失敗 + 內容獲取失敗 + 目錄獲取失敗 + 訪問網站失敗: %s + 文件讀取失敗 + 加載目錄失敗 + 獲取數據失敗! + 加載失敗\n%s + 沒有網絡 + 網絡連接超時 + 數據解析失敗 + + + 請求頭 (header) + 調試源 + 二維碼導入 + 掃描二維碼 + 選中時點擊可彈出菜單 + 主題 + 主題模式 + 選擇主題模式 + 默認主題 + 恢復主題為默認配色 + 加入QQ羣 + 獲取背景圖片需存儲權限 + 輸入書源網址 + 刪除文件 + 刪除文件成功 + 確定刪除文件嗎? + 手機目錄 + 智能導入 + 發現 + 切換顯示樣式 + 導入本地書籍需存儲權限 + 夜間模式 + E-Ink 模式 + 電子墨水屏模式 + 本軟件需要存儲權限來存儲備份書籍信息 + 再按一次退出程式 + 導入本地書籍需存儲權限 + 網絡連接不可用 + + + 確認 + 是否確認刪除? + 是否刪除全部書籍? + 是否同時刪除已下載的書籍目錄? + 掃描二維碼需相機權限 + 朗讀正在運行,不能自動翻頁 + 輸入編碼 + TXT 目錄規則 + 打開外部書籍需獲取存儲權限 + 未獲取到書名 + 輸入替換規則網址 + 搜索列表獲取成功%d + 書源名稱和 URL 不能為空 + 圖庫 + 領支付寶紅包 + 沒有獲取到更新地址 + 正在打開首頁,成功自動返回主界面 + 登錄成功後請點擊右上角圖標進行首頁訪問測試 + + + 使用正則表達式 + 縮進 + 無縮進 + 一字符縮進 + 二字符縮進 + 三字符縮進 + 四字符縮進 + 選擇文件夾 + 選擇文件 + 沒有發現,可以在書源裏添加。 + 恢復默認 + 自定義緩存路徑需要存儲權限 + 黑色 + 文章內容為空 + 正在換源請等待… + 目錄列表為空 + 字距 + + 基本 + 搜索 + 發現 + 詳情 + 目錄 + 正文 + + E-Ink 模式 + 去除動畫,優化電紙書使用體驗 + Web 服務 + web 端口 + 當前端口 %s + 二維碼分享 + 字符串分享 + wifi 分享 + 請給於存儲權限 + 減速 + 加速 + 上一個 + 下一個 + 音樂 + 音頻 + 啟用 + 啟用 JS + 加載 BaseUrl + 全部書源 + 輸入不能為空 + 清空發現緩存 + 編輯發現 + 切換軟件顯示在桌面的圖標 + 幫助 + 我的 + 閲讀 + %d%% + %d 分鐘 + 自動亮度 %s + 按頁朗讀 + 在線朗讀 + 背景圖片 + 背景顏色 + 文字顏色 + 選擇圖片 + 分組管理 + 分組選擇 + 編輯分組 + 移入分組 + 添加分組 + 新建替換 + 分組 + 分組: %s + 目錄: %s + 啟用發現 + 禁用發現 + 啟用所選 + 禁用所選 + 導出所選 + 導出 + 加載目錄 + TTS + WebDav 密碼 + 輸入你的 WebDav 授權密碼 + 輸入你的服務器地址 + WebDav 服務器地址 + WebDav 賬號 + 輸入你的 WebDav 賬號 + 訂閲源 + 編輯訂閲源 + 篩選 + 篩選發現 + 當前位置: + 精準搜索 + 正在啟動服務 + + 文件選擇 + 文件夾選擇 + 我是有底線的 + Uri 轉 Path 失敗 + 刷新封面 + 封面換源 + 選擇本地圖片 + 類型: + 文本 + 音頻 + 後台 + 正在導入 + 正在導出 + 自定義翻頁按鍵 + 上一頁按鍵 + 下一頁按鍵 + 先將書籍加入書架 + 未分組 + 上一句 + 下一句 + 其它目錄 + 文字太多,生成二維碼失敗 + 分享RSS源 + 分享書源 + 自動切換夜間模式 + 夜間模式跟隨系統 + 上級 + 在線朗讀音色 + (%1$d/%2$d) + 顯示訂閲 + 服務已停止 + 正在啟動服務\n具體信息查看通知欄 + 默認路徑 + 系統文件夾選擇器 + 自帶選擇器\n(Android10 以上因權限限制可能無法使用) + Android10 以上因權限限制可能無法讀寫文件 + 長按文字在操作菜單中顯示閲讀·搜索 + 文字操作顯示搜索 + 記錄日誌 + 中文簡繁體轉換 + 圖標為矢量圖標,Android8.0 以前不支持 + 朗讀設置 + 主界面 + 長按選擇文本 + 頁眉 + 正文 + 頁腳 + 文本選擇結束位置 + 文本選擇開始位置 + 共用佈局 + 瀏覽器 + 導入默認規則 + 名稱 + 正則 + 更多菜單 + + + 系統內置字體樣式 + 刪除源文件 + 預設一 + 預設二 + 預設三 + 標題 + 靠左 + 居中 + 隱藏 + 加入分組 + 保存圖片 + 沒有默認路徑 + 設置分組 + 查看目錄 + 導航欄陰影 + 當前陰影大小 (elevation): %s + 默認 + 主菜單 + 點擊授予權限 + 閲讀需要訪問存儲卡權限,請點擊下方的"授予權限"按鈕,或前往「設定」—「應用程式權限」—打開所需權限。如果授予權限後仍然不正常,請點擊右上角的「選擇文件夾」,使用系統文件夾選擇器。 + 全文朗讀中不能朗讀選中文字 + 擴展到劉海 + 更新目錄中 + 全程響應耳機按鍵 + 即使退出軟件也響應耳機按鍵 + 開發人員 + 聯繫我們 + 開源許可 + 關注公衆號 + WeChat + 你的支持是我更新的動力 + 公众号[开源阅读] + 正在自動換源 + 點擊加入 + + 信息 + + + 主色調 + 強調色 + 背景色 + 導航欄顏色 + 白天 + 白天,主色調 + 白天,強調色 + 白天,背景色 + 白天,導航欄顏色 + 夜間 + 夜間,主色調 + 夜間,強調色 + 夜間,背景色 + 夜間,導航欄顏色 + 隱藏頁眉 + 隱藏頁脚 + 自動換源 + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 39c85cae3..b1046096f 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -96,4 +96,14 @@ 系统等宽字体 + + + 标题 + 时间 + 电量 + 页数 + 进度 + 页数及进度 + + \ No newline at end of file diff --git a/app/src/main/res/values/pref_key_value.xml b/app/src/main/res/values/pref_key_value.xml index 0c535ad67..f1f70b8c5 100644 --- a/app/src/main/res/values/pref_key_value.xml +++ b/app/src/main/res/values/pref_key_value.xml @@ -1,23 +1,23 @@ - - auto_refresh - list_screen_direction - full_screen - threads_num - user_agent - bookshelf_px - read_type - expandGroupFind - defaultToRead - autoDownload - downloadPath - checkUpdate + + auto_refresh + list_screen_direction + full_screen + threads_num + user_agent + bookshelf_px + read_type + expandGroupFind + defaultToRead + autoDownload + checkUpdate + 开源阅读 - https://gitee.com/alanskycn/yuedu/blob/master/Rule/README.md - https://github.com/gedoor/legado - https://github.com/gedoor/legado/graphs/contributors - https://gedoor.github.io/MyBookshelf/disclaimer.html - https://gedoor.github.io/MyBookshelf/ - https://github.com/gedoor/legado/releases/latest - https://api.github.com/repos/gedoor/legado/releases/latest + https://gitee.com/alanskycn/yuedu/blob/master/Rule/README.md + https://github.com/gedoor/legado + https://github.com/gedoor/legado/graphs/contributors + https://gedoor.github.io/MyBookshelf/disclaimer.html + https://gedoor.github.io/MyBookshelf/ + https://github.com/gedoor/legado/releases/latest + https://api.github.com/repos/gedoor/legado/releases/latest diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index bc219ab39..83e6bdc84 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,15 +1,17 @@ + 阅读 - Open navigation drawer - Close navigation drawer - Android Studio - android.studio@android.com - Navigation header - Settings + 阅读·搜索 + 阅读需要访问存储卡权限,请前往“设置”—“应用权限”—打开所需权限 + Home 恢复 导入阅读数据 + 创建子文件夹 + 创建legado文件夹作为备份文件夹 + 备份路径 + 导入旧版数据 导入Github数据 净化替换 Send @@ -18,9 +20,7 @@ 取消 确定 去设置 - 无法跳转至设置界面 - 阅读需要访问存储卡权限,请前往“设置”—“应用权限”—打开所需权限 点击重试 正在加载 @@ -42,9 +42,7 @@ 全部 最近阅读 最后阅读 - 让阅读成为一种习惯。 更新日志 - 阅读·搜索 书架还空着,先去添加吧! 搜索 下载 @@ -90,7 +88,7 @@ 下载选择的章节到本地 换源 - \u3000\u3000这是一款使用Kotlin全新开发的开源的阅读软件,欢迎您的加入。关注公众号[开源阅读软件]! + \u3000\u3000这是一款使用Kotlin全新开发的开源的阅读软件,欢迎您的加入。关注公众号[开源阅读]! 阅读3.0下载地址:\nhttps://play.google.com/store/apps/details?id=io.legado.app @@ -132,8 +130,6 @@ 上一章 隐藏状态栏 阅读界面隐藏状态栏 - 阅读行数调整 - 阅读行数减一行,如阅读界面显示不全可启用 朗读 正在朗读 点击打开阅读界面 @@ -384,6 +380,7 @@ 源名称(sourceName) 源URL(sourceUrl) 源分组(sourceGroup) + 分类Url 登录URL(loginUrl) 搜索地址(url) 发现地址规则(url) @@ -404,7 +401,7 @@ 章节名称规则(ChapterName) 章节URL规则(chapterUrl) VIP标识(isVip) - 章节信息(ChapterInfo) + 更新时间(ChapterInfo) 正文规则(content) 正文下一页URL规则(nextContentUrl) webJs @@ -420,6 +417,7 @@ 描述规则(ruleDescription) 图片url规则(ruleImage) 内容规则(ruleContent) + 样式(style) 链接规则(ruleLink) @@ -565,8 +563,11 @@ 导出 加载目录 TTS + WebDav 密码 输入你的WebDav授权密码 输入你的服务器地址 + WebDav 服务器地址 + WebDav 账号 输入你的WebDav账号 订阅源 编辑订阅源 @@ -654,4 +655,41 @@ 点击授予权限 阅读需要访问存储卡权限,请点击下方的"授予权限"按钮,或前往“设置”—“应用权限”—打开所需权限。如果授予权限后仍然不正常,请点击右上角的“选择文件夹”,使用系统文件夹选择器。 全文朗读中不能朗读选中文字 + 扩展到刘海 + 更新目录中 + 全程响应耳机按键 + 即使退出软件也响应耳机按键 + 开发人员 + 联系我们 + 开源许可 + 其它 + 开源阅读 + 关注公众号 + 微信 + 您的支持是我更新的动力 + 公众号[开源阅读] + 正在自动换源 + 点击加入 + + 信息 + 隐藏页眉 + 隐藏页脚 + + + 主色调 + 强调色 + 背景色 + 底部操作栏颜色 + 白天 + 白天,主色调 + 白天,强调色 + 白天,背景色 + 白天,底栏色 + 夜间 + 夜间,主色调 + 夜间,强调色 + 夜间,背景色 + 夜间,底栏色 + 自动换源 + diff --git a/app/src/main/res/xml/about.xml b/app/src/main/res/xml/about.xml index 2970ab12b..3c607cd9d 100644 --- a/app/src/main/res/xml/about.xml +++ b/app/src/main/res/xml/about.xml @@ -4,8 +4,8 @@ + android:key="gzGzh" + android:title="@string/follow_official_account" + android:summary="@string/official_account" + app:iconSpaceReserved="false" /> + android:summary="@string/click_to_apply" + app:iconSpaceReserved="false" /> + android:key="license" + android:title="@string/license" + app:iconSpaceReserved="false" /> + android:key="webDavCreateDir" + android:defaultValue="true" + android:title="@string/mkdirs" + android:summary="@string/mkdirs_description" + app:allowDividerAbove="false" + app:allowDividerBelow="false" + app:iconSpaceReserved="false" /> @@ -44,24 +44,24 @@ app:iconSpaceReserved="false"> + android:key="backupUri" + android:title="@string/backup_path" + app:iconSpaceReserved="false" /> + android:key="web_dav_backup" + android:title="@string/backup" + app:iconSpaceReserved="false" /> + android:key="web_dav_restore" + android:title="@string/restore" + app:iconSpaceReserved="false" /> + android:key="import_old" + android:title="@string/menu_import_old_version" + app:iconSpaceReserved="false" /> diff --git a/app/src/main/res/xml/pref_config_other.xml b/app/src/main/res/xml/pref_config_other.xml index 560692986..66f5bfd9f 100644 --- a/app/src/main/res/xml/pref_config_other.xml +++ b/app/src/main/res/xml/pref_config_other.xml @@ -45,14 +45,16 @@ android:title="@string/threads_num_title" app:iconSpaceReserved="false" /> - + + + + + android:defaultValue="@color/md_light_blue_600" + android:key="colorPrimary" + android:summary="@string/day_color_primary" + android:title="@string/primary" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_pink_800" + android:key="colorAccent" + android:summary="@string/day_color_accent" + android:title="@string/accent" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_grey_100" + android:key="colorBackground" + android:summary="@string/day_background_color" + android:title="@string/background_color" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_grey_200" + android:key="colorBottomBackground" + android:summary="@string/day_navbar_color" + android:title="@string/navbar_color" + app:cpv_dialogType="preset" + app:allowDividerAbove="false" + app:allowDividerBelow="false" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_grey_900" + android:key="colorPrimaryNight" + android:summary="@string/night_primary" + android:title="@string/primary" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_deep_orange_800" + android:key="colorAccentNight" + android:summary="@string/night_accent" + android:title="@string/accent" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_black_1000" + android:key="colorBackgroundNight" + android:summary="@string/night_background_color" + android:title="@string/background_color" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> + android:defaultValue="@color/md_grey_800" + android:key="colorBottomBackgroundNight" + android:summary="@string/night_navbar_color" + android:title="@string/navbar_color" + app:cpv_dialogType="preset" + app:iconSpaceReserved="false" /> diff --git a/build.gradle b/build.gradle index 839736b35..21c80e3a6 100644 --- a/build.gradle +++ b/build.gradle @@ -1,15 +1,16 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.3.70' + ext.kotlin_version = '1.3.72' repositories { google() - jcenter() + //jcenter() + maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } maven { url 'https://maven.fabric.io/public' } maven { url 'https://plugins.gradle.org/m2/' } } dependencies { - classpath 'com.android.tools.build:gradle:3.6.1' + classpath 'com.android.tools.build:gradle:3.6.3' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath 'de.timfreiheit.resourceplaceholders:placeholders:0.3' classpath 'com.google.gms:google-services:4.3.3' @@ -20,7 +21,8 @@ buildscript { allprojects { repositories { google() - jcenter() + //jcenter() + maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } maven { url "https://jitpack.io" } maven { url 'https://maven.google.com/' } maven { url 'https://github.com/psiegman/mvn-repo/raw/master/releases' }