diff --git a/.github/workflows/legado.yml b/.github/workflows/legado.yml index 57c22b832..787d81e48 100644 --- a/.github/workflows/legado.yml +++ b/.github/workflows/legado.yml @@ -39,11 +39,6 @@ jobs: run: | echo "清空18PlusList.txt" echo "">$GITHUB_WORKSPACE/app/src/main/assets/18PlusList.txt - - name: App Minify - if: ${{ github.event_name == 'release'}} - run: | - sed '/minifyEnabled/i\ shrinkResources true' $GITHUB_WORKSPACE/app/build.gradle -i - sed 's/minifyEnabled false/minifyEnabled true/' $GITHUB_WORKSPACE/app/build.gradle -i - name: Release Apk Sign run: | echo "给apk增加签名" diff --git a/app/build.gradle b/app/build.gradle index 7e1977da9..51a57aef2 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -142,10 +142,10 @@ dependencies { implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" //room - def room_version = '2.2.5' + def room_version = '2.2.6' implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" - testImplementation "androidx.room:room-testing:2.2.5" + testImplementation "androidx.room:room-testing:$room_version" //paging implementation 'androidx.paging:paging-runtime-ktx:2.1.2' @@ -174,8 +174,8 @@ dependencies { //网络 implementation 'com.squareup.okhttp3:okhttp:4.9.0' - implementation 'com.ljx.rxhttp:rxhttp:2.5.1' - kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.1' + implementation 'com.ljx.rxhttp:rxhttp:2.5.3' + kapt 'com.ljx.rxhttp:rxhttp-compiler:2.5.3' //Glide implementation 'com.github.bumptech.glide:glide:4.11.0' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 35207bcbc..4fe52b196 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -150,7 +150,15 @@ + android:launchMode="singleTask"> + + + + + + /*播音人Aiting可改其他https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/speaker.json,详见https://ai.aliyun.com/nls/tts*/;eval(''+java.ajax('https://cdn.jsdelivr.net/gh/Celeter/build/.github/scripts/ttsDemo.js'));ttsDemo(speakText,speakSpeed,'Aiting')" } ] diff --git a/app/src/main/assets/defaultData/rssSources.json b/app/src/main/assets/defaultData/rssSources.json new file mode 100644 index 000000000..b4b2a70e0 --- /dev/null +++ b/app/src/main/assets/defaultData/rssSources.json @@ -0,0 +1,21 @@ +[ + { + "articleStyle": 0, + "customOrder": 1, + "enableJs": true, + "enabled": true, + "loadWithBaseUrl": false, + "ruleArticles": "class.release-main-section||tbody@tr", + "ruleContent": "@js:\nif(baseUrl.match(\/90pan\/)){\nresult=result.replace(\/true\\||<.*?>([^<]+)<\\\/span><\\\/a>\/g,'

$2<\/a><\/h4>

$1<\/p>\\n\\n')}\nelse{\nhtml=String(java.getString(\"@@class.markdown-body@html\",false)+java.getString(\"@@class.flex-justify-between.-1@html\",false));\nhref='https:\/\/github.com'+String(java.getString(\"@@class.flex-justify-between.-1@class.Box-body.0@a@href\",false))\nhtml+'

下载地址:<\/p>'+href+'<\/a>'\n}", + "ruleImage": "@js:if(baseUrl.match(\/legado|704\/g)){\nresult='https:\/\/urlmin.cn\/034'\n}else{result=\"https:\/\/urlmin.cn\/15c\"}", + "ruleLink": "tag.a.0@href\n@js:if(baseUrl.match(\/90pan\/)){\nresult='https:\/\/www.90pan.com\/ajax.php,{\"method\": \"POST\",\"body\": \"action=load_down_addr1&file_id='+result.match(\/b(\\d+)\/)[1]+'\"}'}else{\nresult='https:\/\/github.com'+result\n}", + "ruleNextPage": "text.Next@href", + "rulePubDate": "tag.td!0@text||class.markdown-body@text##\\n##·", + "ruleTitle": "tag.a.0@text", + "sortUrl": "阅读2.0·GitHub::https:\/\/github.com\/gedoor\/MyBookshelf\/releases\n阅读3.0·GitHub::https:\/\/github.com\/gedoor\/legado\/releases\n阅读2.0·90盘::https:\/\/www.90pan.com\/o138703\n阅读3.0·90盘::https:\/\/www.90pan.com\/o138704", + "sourceIcon": "http:\/\/ku.mumuceo.com\/static\/images\/applogo\/yuedu.png", + "sourceName": "阅读下载", + "sourceUrl": "https:\/\/github.com\/gedoor\/legado\/releases", + "style": "" + } +] \ No newline at end of file diff --git a/app/src/main/assets/help/ruleHelp.md b/app/src/main/assets/help/ruleHelp.md index c01ce2b1f..23072a407 100644 --- a/app/src/main/assets/help/ruleHelp.md +++ b/app/src/main/assets/help/ruleHelp.md @@ -78,8 +78,7 @@ variable // 自定义书籍变量信息(用于书源规则检索书籍信息) ``` ### chapter对象的可用属性 - - > 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符. +> 使用方法: 在js中或{{}}中使用chapter.属性的方式即可获取.如在正文内容后加上 ##{{chapter.title+chapter.index}} 可以净化 章节标题+序号(如 第二章 天仙下凡2) 这一类的字符. ``` url // 章节地址 title // 章节标题 @@ -93,3 +92,17 @@ variable // 自定义书籍变量信息(用于书源规则检索书籍信息) variable //变量 ``` +### 字体解析使用 +> 使用方法,在正文替换规则中使用,原理根据f1字体的字形数据到f2中查找字形对应的编码 +``` +@js: +var b64=String(src).match(/ttf;base64,([^\)]+)/); +if (b64) { + var f1 = java.queryBase64TTF(b64[1]) + var f2 = java.queryTTF("/storage/emulated/0/Fonts/Source Han Sans CN Regular.ttf") + java.replaceFont(result, f1, f2) +}else{ + result +} +``` + diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index f3d953e59..9447fa317 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -3,8 +3,25 @@ * 关注合作公众号 **[小说拾遗]** 获取好看的小说。 * 旧版数据导入教程:先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 -**2021/01/04** +**2021/01/23** +* 优化书源校验,从搜索到正文全部校验 + +**2021/01/20** +* 更新在线朗读 + +**2021/01/18** +* 增加三星 S Pen 支持 by [dacer](https://github.com/dacer) +* 订阅添加阅读下载,可以从多个渠道下载 +* 修复一些BUG + +**2021/01/12** * 修复bug +* 朗读时翻页防止重复发送请求 by [litcc](https://github.com/litcc) +* 换源刷新之前删除原搜索记录 +* 优化web调试 + +**2021/01/05** +* 修复崩溃bug **2021/01/03** * 导出书单只保留书名与作者,导入时自动查找可用源 @@ -68,18 +85,7 @@ * 阅读进度从页数改为字数,排版变化时定位更准确 * 修改viewBinding * 修复中文排序 -* 去掉FontJs规则,可以写在替换规则里,例:f2为正确的字体,根据文字数据去正确的字体里面查找,可以把错误的文字替换为正确的文字 -``` -@js: -var b64=String(src).match(/ttf;base64,([^\)]+)/); -if (b64) { - var f1 = java.queryBase64TTF(b64[1]) - var f2 = java.queryTTF("/storage/emulated/0/Fonts/Source Han Sans CN Regular.ttf") - java.replaceFont(result, f1, f2) -}else{ - result -} -``` +* 去掉FontJs规则,可以写在替换规则里,示例可在帮助文档查看 **2020/11/30** * 优化阅读界面设置 diff --git a/app/src/main/assets/web/index.html b/app/src/main/assets/web/index.html index d44d08651..45edf04e6 100644 --- a/app/src/main/assets/web/index.html +++ b/app/src/main/assets/web/index.html @@ -228,29 +228,30 @@

章节信息:
+ placeholder="选择章节信息 (规则结果为String)">
翻页规则:
+ placeholder="选择目录下一页链接 (规则结果为List<String>或String)">

正文
+
+
脚本注入:
+ +
正文规则:
+ placeholder="选择正文内容 (规则结果为String)">
翻页规则:
- -
-
-
脚本注入:
- +
资源正则:
diff --git a/app/src/main/assets/web/index.js b/app/src/main/assets/web/index.js index 9d09d50de..ba67e7801 100644 --- a/app/src/main/assets/web/index.js +++ b/app/src/main/assets/web/index.js @@ -383,15 +383,15 @@ $('.menu').addEventListener('click', e => { ws.send(`{"tag":"${saveRule[0].bookSourceUrl}", "key":"${sKey}"}`); }; ws.onmessage = (msg) => { - DebugPrint(msg.data == 'finish' ? `\n[${Date().split(' ')[4]}] 调试任务已完成!` : msg.data); - if (msg.data == 'finish') setRule(saveRule[0]); + console.log('[调试]', msg); + DebugPrint(msg.data); }; ws.onerror = (err) => { throw `${err.data}`; } ws.onclose = () => { thisNode.setAttribute('class', ''); - DebugPrint(`[${Date().split(' ')[4]}] 调试服务已关闭!`); + DebugPrint(`\n调试服务已关闭!`); } } else throw `${sResult.errorMsg}`; }).catch(err => { 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 f4e902ba0..b8cbedf1c 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -70,10 +70,12 @@ object PreferKey { const val cAccent = "colorAccent" const val cBackground = "colorBackground" const val cBBackground = "colorBottomBackground" + const val bgImage = "backgroundImage" const val cNPrimary = "colorPrimaryNight" const val cNAccent = "colorAccentNight" const val cNBackground = "colorBackgroundNight" const val cNBBackground = "colorBottomBackgroundNight" + const val bgImageN = "backgroundImageNight" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt index 3655e0117..7a40d4cc8 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt @@ -12,10 +12,10 @@ interface BookSourceDao { fun liveDataAll(): LiveData> @Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey or bookSourceComment like :searchKey order by customOrder asc") - fun liveDataSearch(searchKey: String = ""): LiveData> + fun liveDataSearch(searchKey: String): LiveData> @Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc") - fun liveDataGroupSearch(searchKey: String = ""): LiveData> + fun liveDataGroupSearch(searchKey: String): LiveData> @Query("select * from book_sources where enabled = 1 order by customOrder asc") fun liveDataEnabled(): LiveData> @@ -32,18 +32,15 @@ interface BookSourceDao { @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and (bookSourceGroup like :key) order by customOrder asc") fun liveGroupExplore(key: String): LiveData> - @Query("select bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") + @Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") fun liveGroup(): LiveData> - @Query("select bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''") + @Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''") fun liveGroupEnabled(): LiveData> - @Query("select bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''") + @Query("select distinct bookSourceGroup from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' and trim(bookSourceGroup) <> ''") fun liveExploreGroup(): LiveData> - @Query("select distinct enabled from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey") - fun searchIsEnable(searchKey: String = ""): List - @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc") fun observeFind(): DataSource.Factory @@ -68,6 +65,9 @@ interface BookSourceDao { @get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder") val allTextEnabled: List + @get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''") + val allGroup: List + @Query("select * from book_sources where bookSourceUrl = :key") fun getBookSource(key: String): BookSource? diff --git a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt index ad2b25706..deaa45726 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssSourceDao.kt @@ -28,12 +28,21 @@ interface RssSourceDao { @Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder") fun liveGroupSearch(key: String): LiveData> - @Query("SELECT * FROM rssSources where enabled = 1 and (sourceName like '%'||:searchKey||'%' or sourceGroup like '%'||:searchKey||'%' or sourceUrl like '%'||:searchKey||'%') order by customOrder") + @Query("SELECT * FROM rssSources where enabled = 1 order by customOrder") + fun liveEnabled(): LiveData> + + @Query("SELECT * FROM rssSources where enabled = 1 and (sourceName like :searchKey or sourceGroup like :searchKey or sourceUrl like :searchKey) order by customOrder") fun liveEnabled(searchKey: String): LiveData> - @Query("select sourceGroup from rssSources where trim(sourceGroup) <> ''") + @Query("SELECT * FROM rssSources where enabled = 1 and sourceGroup like :searchKey order by customOrder") + fun liveEnabledByGroup(searchKey: String): LiveData> + + @Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") fun liveGroup(): LiveData> + @get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''") + val allGroup: List + @get:Query("select min(customOrder) from rssSources") val minOrder: Int diff --git a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt index 74d33dfeb..234e24e0d 100644 --- a/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/SearchBookDao.kt @@ -63,6 +63,10 @@ interface SearchBookDao { @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg searchBook: SearchBook): List + @Query("delete from searchBooks where name = :name and author = :author") + fun clear(name: String, author: String) + @Query("delete from searchBooks where time < :time") fun clearExpired(time: Long) + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/DefaultData.kt b/app/src/main/java/io/legado/app/help/DefaultData.kt index d2bab07f1..ba06316b9 100644 --- a/app/src/main/java/io/legado/app/help/DefaultData.kt +++ b/app/src/main/java/io/legado/app/help/DefaultData.kt @@ -2,6 +2,7 @@ package io.legado.app.help import io.legado.app.App import io.legado.app.data.entities.HttpTTS +import io.legado.app.data.entities.RssSource import io.legado.app.data.entities.TxtTocRule import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonArray @@ -45,17 +46,25 @@ object DefaultData { GSON.fromJsonArray(json)!! } + val rssSources by lazy { + val json = String( + App.INSTANCE.assets.open("defaultData${File.separator}rssSources.json") + .readBytes() + ) + GSON.fromJsonArray(json)!! + } + fun importDefaultHttpTTS() { App.db.httpTTSDao.deleteDefault() - httpTTS.let { - App.db.httpTTSDao.insert(*it.toTypedArray()) - } + App.db.httpTTSDao.insert(*httpTTS.toTypedArray()) } fun importDefaultTocRules() { App.db.txtTocRule.deleteDefault() - txtTocRules.let { - App.db.txtTocRule.insert(*it.toTypedArray()) - } + App.db.txtTocRule.insert(*txtTocRules.toTypedArray()) + } + + fun importDefaultRssSources() { + App.db.rssSourceDao.insert(*rssSources.toTypedArray()) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/LocalConfig.kt b/app/src/main/java/io/legado/app/help/LocalConfig.kt index ac0d3034d..6278c0c55 100644 --- a/app/src/main/java/io/legado/app/help/LocalConfig.kt +++ b/app/src/main/java/io/legado/app/help/LocalConfig.kt @@ -65,8 +65,11 @@ object LocalConfig { get() = isLastVersion(1, "ruleHelpVersion") val hasUpHttpTTS: Boolean - get() = isLastVersion(1, "httpTtsVersion") + get() = !isLastVersion(2, "httpTtsVersion") val hasUpTxtTocRule: Boolean - get() = isLastVersion(1, "txtTocRuleVersion") + get() = !isLastVersion(1, "txtTocRuleVersion") + + val hasUpRssSources: Boolean + get() = !isLastVersion(1, "rssSourceVersion") } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ThemeConfig.kt b/app/src/main/java/io/legado/app/help/ThemeConfig.kt index 9de5ceade..0b0298e91 100644 --- a/app/src/main/java/io/legado/app/help/ThemeConfig.kt +++ b/app/src/main/java/io/legado/app/help/ThemeConfig.kt @@ -204,6 +204,7 @@ object ThemeConfig { var primaryColor: String, var accentColor: String, var backgroundColor: String, + var backgroundImage: String? = null, var bottomBackground: String ) 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 1686df19e..a26f5174f 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 @@ -29,7 +29,7 @@ object Backup { "bookmark.json", "bookGroup.json", "bookSource.json", - "rssSource.json", + "rssSources.json", "rssStar.json", "replaceRule.json", "readRecord.json", @@ -61,7 +61,7 @@ object Backup { writeListToJson(App.db.bookmarkDao.all, "bookmark.json", backupPath) writeListToJson(App.db.bookGroupDao.all, "bookGroup.json", backupPath) writeListToJson(App.db.bookSourceDao.all, "bookSource.json", backupPath) - writeListToJson(App.db.rssSourceDao.all, "rssSource.json", backupPath) + writeListToJson(App.db.rssSourceDao.all, "rssSources.json", backupPath) writeListToJson(App.db.rssStarDao.all, "rssStar.json", backupPath) writeListToJson(App.db.replaceRuleDao.all, "replaceRule.json", backupPath) writeListToJson(App.db.readRecordDao.all, "readRecord.json", backupPath) diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt index b0d833b0c..212152849 100644 --- a/app/src/main/java/io/legado/app/help/storage/Restore.kt +++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt @@ -122,7 +122,7 @@ object Restore { fileToListT(path, "bookSource.json")?.let { App.db.bookSourceDao.insert(*it.toTypedArray()) } - fileToListT(path, "rssSource.json")?.let { + fileToListT(path, "rssSources.json")?.let { App.db.rssSourceDao.insert(*it.toTypedArray()) } fileToListT(path, "rssStar.json")?.let { 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 ec3d31097..c74a36b5e 100644 --- a/app/src/main/java/io/legado/app/model/Debug.kt +++ b/app/src/main/java/io/legado/app/model/Debug.kt @@ -16,8 +16,8 @@ import java.text.SimpleDateFormat import java.util.* object Debug { - private var debugSource: String? = null var callback: Callback? = null + private var debugSource: String? = null private val tasks: CompositeCoroutine = CompositeCoroutine() @SuppressLint("ConstantLocale") @@ -59,12 +59,12 @@ object Debug { } } - fun startDebug(rssSource: RssSource) { + fun startDebug(scope: CoroutineScope, rssSource: RssSource) { cancelDebug() debugSource = rssSource.sourceUrl log(debugSource, "︾开始解析") val sort = rssSource.sortUrls().entries.first() - Rss.getArticles(sort.key, sort.value, rssSource, 1) + Rss.getArticles(scope, sort.key, sort.value, rssSource, 1) .onSuccess { if (it.articles.isEmpty()) { log(debugSource, "⇒列表页解析成功,为空") @@ -77,7 +77,7 @@ object Debug { if (ruleContent.isNullOrEmpty()) { log(debugSource, "⇒内容规则为空,默认获取整个网页", state = 1000) } else { - rssContentDebug(it.articles[0], ruleContent, rssSource) + rssContentDebug(scope, it.articles[0], ruleContent, rssSource) } } else { log(debugSource, "⇒存在描述规则,不解析内容页") @@ -90,9 +90,14 @@ object Debug { } } - private fun rssContentDebug(rssArticle: RssArticle, ruleContent: String, rssSource: RssSource) { + private fun rssContentDebug( + scope: CoroutineScope, + rssArticle: RssArticle, + ruleContent: String, + rssSource: RssSource + ) { log(debugSource, "︾开始解析内容页") - Rss.getContent(rssArticle, ruleContent, rssSource) + Rss.getContent(scope, rssArticle, ruleContent, rssSource) .onSuccess { log(debugSource, it) log(debugSource, "︽内容页解析完成", state = 1000) diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt index 5445884bf..98e590cf9 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeRule.kt @@ -646,7 +646,9 @@ class AnalyzeRule(val ruleData: RuleDataInterface) : JsExtensions { bindings["chapter"] = chapter bindings["title"] = chapter?.title bindings["src"] = content - return SCRIPT_ENGINE.eval(jsStr, bindings) + return runBlocking { + SCRIPT_ENGINE.eval(jsStr, bindings) + } } /** 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 0a0146874..318c1dcd3 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 @@ -309,7 +309,7 @@ class AnalyzeUrl( } return when (method) { RequestMethod.POST -> { - if (fieldMap.isNotEmpty()) { + if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { RxHttp.postForm(url) .setAssemblyEnabled(false) .setOkClient(HttpHelper.getProxyClient(proxy)) @@ -338,7 +338,7 @@ class AnalyzeUrl( setCookie(tag) return when (method) { RequestMethod.POST -> { - if (fieldMap.isNotEmpty()) { + if (fieldMap.isNotEmpty() || body.isNullOrBlank()) { RxHttp.postForm(url) .setAssemblyEnabled(false) .setOkClient(HttpHelper.getProxyClient(proxy)) diff --git a/app/src/main/java/io/legado/app/model/rss/Rss.kt b/app/src/main/java/io/legado/app/model/rss/Rss.kt index afa6c06fd..71aa5b2b3 100644 --- a/app/src/main/java/io/legado/app/model/rss/Rss.kt +++ b/app/src/main/java/io/legado/app/model/rss/Rss.kt @@ -14,11 +14,11 @@ import kotlin.coroutines.CoroutineContext object Rss { fun getArticles( + scope: CoroutineScope, sortName: String, sortUrl: String, rssSource: RssSource, page: Int, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO ): Coroutine { return Coroutine.async(scope, context) { @@ -35,10 +35,10 @@ object Rss { } fun getContent( + scope: CoroutineScope, rssArticle: RssArticle, ruleContent: String, rssSource: RssSource?, - scope: CoroutineScope = Coroutine.DEFAULT, context: CoroutineContext = Dispatchers.IO ): Coroutine { return Coroutine.async(scope, context) { 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 1bba73ec1..1bdf1ca05 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 @@ -93,15 +93,18 @@ object BookChapterList { for (item in chapterDataList) { downloadToc( scope, item, book, bookSource, - tocRule, listRule, chapterList, chapterDataList + tocRule, listRule, chapterList, chapterDataList, + { + block.resume(finish(book, chapterList, reverse)) + } ) { - block.resume(finish(book, chapterList, reverse)) + block.cancel(it) } } } } }.onFailure { - block.resumeWithException(it) + block.cancel(it) } } @@ -114,7 +117,8 @@ object BookChapterList { listRule: String, chapterList: ArrayList, chapterDataList: ArrayList>, - onFinish: () -> Unit + onFinish: () -> Unit, + onError: (error: Throwable) -> Unit ) { Coroutine.async(scope = scope) { val nextBody = AnalyzeUrl( @@ -142,7 +146,7 @@ object BookChapterList { } } }.onError { - throw it + onError.invoke(it) } } 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 aa3bd6799..6bd72e7bf 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -8,9 +8,11 @@ import io.legado.app.base.BaseService import io.legado.app.constant.AppConst import io.legado.app.constant.EventBus 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.CompositeCoroutine +import io.legado.app.model.webBook.WebBook import io.legado.app.service.help.CheckSource import io.legado.app.ui.book.source.manage.BookSourceActivity import io.legado.app.utils.postEvent @@ -21,7 +23,7 @@ import kotlin.math.min class CheckSourceService : BaseService() { private var threadCount = AppConfig.threadCount - private var searchPool = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() + private var searchCoroutine = Executors.newFixedThreadPool(threadCount).asCoroutineDispatcher() private var tasks = CompositeCoroutine() private val allIds = ArrayList() private val checkedIds = ArrayList() @@ -62,7 +64,7 @@ class CheckSourceService : BaseService() { override fun onDestroy() { super.onDestroy() tasks.clear() - searchPool.close() + searchCoroutine.close() postEvent(EventBus.CHECK_SOURCE_DONE, 0) } @@ -96,18 +98,36 @@ class CheckSourceService : BaseService() { if (index < allIds.size) { val sourceUrl = allIds[index] App.db.bookSourceDao.getBookSource(sourceUrl)?.let { source -> - if (source.searchUrl.isNullOrEmpty()) { - onNext(sourceUrl, source.bookSourceName) - } else { - CheckSource(source).check(this, searchPool) { - onNext(it, source.bookSourceName) - } - } + check(source) } ?: onNext(sourceUrl, "") } } } + fun check(source: BookSource) { + execute(context = searchCoroutine) { + val webBook = WebBook(source) + val books = webBook.searchBookAwait(this, CheckSource.keyword) + val book = webBook.getBookInfoAwait(this, books.first().toBook()) + val toc = webBook.getChapterListAwait(this, book) + val content = webBook.getContentAwait(this, book, toc.first()) + if (content.isBlank()) { + throw Exception("正文内容为空") + } + }.timeout(60000L) + .onError { + source.addGroup("失效") + source.bookSourceComment = + "error:${it.localizedMessage}\n${source.bookSourceComment}" + App.db.bookSourceDao.update(source) + }.onSuccess { + source.removeGroup("失效") + App.db.bookSourceDao.update(source) + }.onFinally { + onNext(source.bookSourceUrl, source.bookSourceName) + } + } + private fun onNext(sourceUrl: String, sourceName: String) { synchronized(this) { check() diff --git a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt index 2851173ed..a91771e66 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -12,11 +12,17 @@ import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook import io.legado.app.utils.FileUtils import io.legado.app.utils.LogUtils +import io.legado.app.utils.MD5Utils import io.legado.app.utils.postEvent +import kotlinx.coroutines.ensureActive import kotlinx.coroutines.isActive +import org.jetbrains.anko.collections.forEachWithIndex import java.io.File import java.io.FileDescriptor import java.io.FileInputStream +import java.net.ConnectException +import java.net.SocketTimeoutException +import java.util.* class HttpReadAloudService : BaseReadAloudService(), MediaPlayer.OnPreparedListener, @@ -55,12 +61,17 @@ class HttpReadAloudService : BaseReadAloudService(), override fun play() { if (contentList.isEmpty()) return - if (nowSpeak == 0) { - downloadAudio() - } else { - val file = getSpeakFile(nowSpeak) - if (file.exists()) { - playAudio(FileInputStream(file).fd) + ReadAloud.httpTTS?.let { + val fileName = md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), contentList[nowSpeak]) + if (nowSpeak == 0) { + downloadAudio() + } else { + val file = getSpeakFileAsMd5(fileName) + if (file.exists()) { + playAudio(FileInputStream(file).fd) + } else { + downloadAudio() + } } } } @@ -68,28 +79,53 @@ class HttpReadAloudService : BaseReadAloudService(), private fun downloadAudio() { task?.cancel() task = execute { - FileUtils.deleteFile(ttsFolder) - for (index in 0 until contentList.size) { - if (isActive) { - ReadAloud.httpTTS?.let { - AnalyzeUrl( - it.url, - speakText = contentList[index], - speakSpeed = AppConfig.ttsSpeechRate - ).getByteArray().let { bytes -> - if (isActive) { - val file = getSpeakFile(index) - file.writeBytes(bytes) - if (index == nowSpeak) { - @Suppress("BlockingMethodInNonBlockingContext") - val fis = FileInputStream(file) - playAudio(fis.fd) + removeCacheFile() + ReadAloud.httpTTS?.let { + contentList.forEachWithIndex { index, item -> + if (isActive) { + val fileName = + md5SpeakFileName(it.url, AppConfig.ttsSpeechRate.toString(), item) + + if (hasSpeakFile(fileName)) { //已经下载好的语音缓存 + if (index == nowSpeak) { + val file = getSpeakFileAsMd5(fileName) + + @Suppress("BlockingMethodInNonBlockingContext") + val fis = FileInputStream(file) + playAudio(fis.fd) + } + } else if (hasSpeakCacheFile(fileName)) { //缓存文件还在,可能还没下载完 + return@let + } else { //没有下载并且没有缓存文件 + try { + createSpeakCacheFile(fileName) + AnalyzeUrl( + it.url, + speakText = item, + speakSpeed = AppConfig.ttsSpeechRate + ).getByteArray().let { bytes -> + ensureActive() + val file = getSpeakFileAsMd5(fileName) + //val file = getSpeakFile(index) + file.writeBytes(bytes) + removeSpeakCacheFile(fileName) + if (index == nowSpeak) { + @Suppress("BlockingMethodInNonBlockingContext") + val fis = FileInputStream(file) + playAudio(fis.fd) + } } + } catch (e: SocketTimeoutException) { + removeSpeakCacheFile(fileName) + // delay(2000) + // downloadAudio() + } catch (e: ConnectException) { + removeSpeakCacheFile(fileName) + } catch (e: Exception) { + removeSpeakCacheFile(fileName) } } } - } else { - break } } } @@ -110,10 +146,47 @@ class HttpReadAloudService : BaseReadAloudService(), } } - private fun getSpeakFile(index: Int = nowSpeak): File { - return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") + private fun speakFilePath() = ttsFolder + File.separator + private fun md5SpeakFileName(url: String, ttsConfig: String, content: String): String { + return MD5Utils.md5Encode16(textChapter!!.title) + "_" + MD5Utils.md5Encode16("$url-|-$ttsConfig-|-$content") } + private fun hasSpeakFile(name: String) = + FileUtils.exist("${speakFilePath()}$name.mp3") + + private fun hasSpeakCacheFile(name: String) = + FileUtils.exist("${speakFilePath()}$name.mp3.cache") + + private fun createSpeakCacheFile(name: String): File = + FileUtils.createFileWithReplace("${speakFilePath()}$name.mp3.cache") + + private fun removeSpeakCacheFile(name: String) { + FileUtils.delete("${speakFilePath()}$name.mp3.cache") + } + + private fun getSpeakFileAsMd5(name: String): File = + FileUtils.createFileIfNotExist("${speakFilePath()}$name.mp3") + + private fun removeCacheFile() { + FileUtils.listDirsAndFiles(speakFilePath())?.forEach { + if (it == null) { + return@forEach + } + if (Regex(""".+\.mp3$""").matches(it.name)) { //mp3缓存文件 + val reg = + """^${MD5Utils.md5Encode16(textChapter!!.title)}_[a-z0-9]{16}\.mp3$""".toRegex() + if (!reg.matches(it.name)) { + FileUtils.deleteFile(it.absolutePath) + } + } else { + if (Date().time - it.lastModified() > 30000) { + FileUtils.deleteFile(it.absolutePath) + } + } + } + } + + override fun pauseReadAloud(pause: Boolean) { super.pauseReadAloud(pause) mediaPlayer.pause() diff --git a/app/src/main/java/io/legado/app/service/help/CheckSource.kt b/app/src/main/java/io/legado/app/service/help/CheckSource.kt index 596662429..d38e6c225 100644 --- a/app/src/main/java/io/legado/app/service/help/CheckSource.kt +++ b/app/src/main/java/io/legado/app/service/help/CheckSource.kt @@ -2,64 +2,35 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent -import io.legado.app.App import io.legado.app.R import io.legado.app.constant.IntentAction import io.legado.app.data.entities.BookSource -import io.legado.app.help.coroutine.Coroutine -import io.legado.app.model.webBook.WebBook import io.legado.app.service.CheckSourceService -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers import org.jetbrains.anko.toast -import kotlin.coroutines.CoroutineContext -class CheckSource(val source: BookSource) { +object CheckSource { + var keyword = "我的" - companion object { - var keyword = "我的" - - fun start(context: Context, sources: List) { - if (sources.isEmpty()) { - context.toast(R.string.non_select) - return - } - val selectedIds: ArrayList = arrayListOf() - sources.map { - selectedIds.add(it.bookSourceUrl) - } - Intent(context, CheckSourceService::class.java).let { - it.action = IntentAction.start - it.putExtra("selectIds", selectedIds) - context.startService(it) - } + fun start(context: Context, sources: List) { + if (sources.isEmpty()) { + context.toast(R.string.non_select) + return } - - fun stop(context: Context) { - Intent(context, CheckSourceService::class.java).let { - it.action = IntentAction.stop - context.startService(it) - } + val selectedIds: ArrayList = arrayListOf() + sources.map { + selectedIds.add(it.bookSourceUrl) + } + Intent(context, CheckSourceService::class.java).let { + it.action = IntentAction.start + it.putExtra("selectIds", selectedIds) + context.startService(it) } } - fun check( - scope: CoroutineScope, - context: CoroutineContext, - onNext: (sourceUrl: String) -> Unit - ): Coroutine<*> { - val webBook = WebBook(source) - return webBook - .searchBook(scope, keyword, context = context) - .timeout(60000L) - .onError(Dispatchers.IO) { - source.addGroup("失效") - App.db.bookSourceDao.update(source) - }.onSuccess(Dispatchers.IO) { - source.removeGroup("失效") - App.db.bookSourceDao.update(source) - }.onFinally(Dispatchers.IO) { - onNext(source.bookSourceUrl) - } + fun stop(context: Context) { + Intent(context, CheckSourceService::class.java).let { + it.action = IntentAction.stop + context.startService(it) + } } } \ No newline at end of file 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 c76386bd6..1a2d7b340 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 @@ -183,9 +183,11 @@ object ReadBook { } } - fun skipToPage(index: Int) { + fun skipToPage(index: Int, success: (() -> Unit)? = null) { durChapterPos = curTextChapter?.getReadLength(index) ?: index - callBack?.upContent() + callBack?.upContent() { + success?.invoke() + } curPageChanged() saveRead() } @@ -240,19 +242,28 @@ object ReadBook { /** * 加载章节内容 */ - fun loadContent(resetPageOffset: Boolean) { - loadContent(durChapterIndex, resetPageOffset = resetPageOffset) + fun loadContent(resetPageOffset: Boolean, success: (() -> Unit)? = null) { + loadContent(durChapterIndex, resetPageOffset = resetPageOffset) { + success?.invoke() + } loadContent(durChapterIndex + 1, resetPageOffset = resetPageOffset) loadContent(durChapterIndex - 1, resetPageOffset = resetPageOffset) } - fun loadContent(index: Int, upContent: Boolean = true, resetPageOffset: Boolean) { + fun loadContent( + index: Int, + upContent: Boolean = true, + resetPageOffset: Boolean, + success: (() -> Unit)? = null + ) { book?.let { book -> if (addLoading(index)) { Coroutine.async { App.db.bookChapterDao.getChapter(book.bookUrl, index)?.let { chapter -> BookHelp.getContent(book, chapter)?.let { - contentLoadFinish(book, chapter, it, upContent, resetPageOffset) + contentLoadFinish(book, chapter, it, upContent, resetPageOffset) { + success?.invoke() + } removeLoading(chapter.index) } ?: download(chapter, resetPageOffset = resetPageOffset) } ?: removeLoading(index) @@ -282,7 +293,11 @@ object ReadBook { } } - private fun download(chapter: BookChapter, resetPageOffset: Boolean) { + private fun download( + chapter: BookChapter, + resetPageOffset: Boolean, + success: (() -> Unit)? = null + ) { val book = book val webBook = webBook if (book != null && webBook != null) { @@ -290,7 +305,9 @@ object ReadBook { } else if (book != null) { contentLoadFinish( book, chapter, "没有书源", resetPageOffset = resetPageOffset - ) + ) { + success?.invoke() + } removeLoading(chapter.index) } else { removeLoading(chapter.index) @@ -379,7 +396,8 @@ object ReadBook { chapter: BookChapter, content: String, upContent: Boolean = true, - resetPageOffset: Boolean + resetPageOffset: Boolean, + success: (() -> Unit)? = null ) { Coroutine.async { ImageProvider.clearOut(durChapterIndex) @@ -420,6 +438,8 @@ object ReadBook { }.onError { it.printStackTrace() App.INSTANCE.toast("ChapterProvider ERROR:\n${it.getStackTraceString()}") + }.onSuccess { + success?.invoke() } } @@ -460,10 +480,19 @@ object ReadBook { interface CallBack { fun loadChapterList(book: Book) - fun upContent(relativePosition: Int = 0, resetPageOffset: Boolean = true) + + fun upContent( + relativePosition: Int = 0, + resetPageOffset: Boolean = true, + success: (() -> Unit)? = null + ) + fun upView() + fun pageChanged() + fun contentLoadFinish() + fun upPageAnim() } 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 ca805475b..992bf6931 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 @@ -9,6 +9,7 @@ import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.App import io.legado.app.R +import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.openUrl @@ -24,19 +25,24 @@ class AboutFragment : PreferenceFragmentCompat() { Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"), Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"), Pair("(QQ群VIP3)680280282", "_N0i7yZObjKSeZQvzoe2ej7j02kLnOOK"), + Pair("(QQ群VIP4)680280282", "VF2UwvUCuaqlo6pddWTe_kw__a1_Fr8O"), Pair("(QQ群1)805192012", "6GlFKjLeIk5RhQnR3PNVDaKB6j10royo"), Pair("(QQ群2)773736122", "5Bm5w6OgLupXnICbYvbgzpPUgf0UlsJF"), Pair("(QQ群3)981838750", "g_Sgmp2nQPKqcZQ5qPcKLHziwX_mpps9"), Pair("(QQ群4)256929088", "czEJPLDnT4Pd9SKQ6RoRVzKhDxLchZrO"), Pair("(QQ群5)811843556", "zKZ2UYGZ7o5CzcA6ylxzlqi21si_iqaX"), Pair("(QQ群6)870270970", "FeCF8iSxfQbe90HPvGsvcqs5P5oSeY5n"), - Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC") + Pair("(QQ群7)15987187", "S2g2TMD0LGd3sefUADd1AbyPEW2o2XfC"), + Pair("(QQ群8)1079926194", "gg2qFH8q9IPFaCHV3H7CqCN-YljvazE1"), ) override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.about) - findPreference("check_update")?.summary = + findPreference("update_log")?.summary = "${getString(R.string.version)} ${App.versionName}" + if (AppConfig.isGooglePlay) { + preferenceScreen.removePreferenceRecursively("check_update") + } } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -58,6 +64,7 @@ class AboutFragment : PreferenceFragmentCompat() { "qq" -> showQqGroups() "gzGzh" -> requireContext().sendToClip(getString(R.string.legado_gzh)) "tg" -> openUrl(R.string.tg_url) + "discord" -> openUrl(R.string.discord_url) } return super.onPreferenceTreeClick(preference) } diff --git a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt index 6aa236744..0f3e6008f 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportBookSourceDialog.kt @@ -10,10 +10,12 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey import io.legado.app.data.entities.BookSource import io.legado.app.databinding.DialogEditTextBinding @@ -21,10 +23,8 @@ import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.* import io.legado.app.utils.viewbindingdelegate.viewBinding -import io.legado.app.utils.visible import org.jetbrains.anko.sdk27.listeners.onClick /** @@ -85,7 +85,14 @@ class ImportBookSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickList when (item.itemId) { R.id.menu_new_group -> { alert(R.string.diy_edit_source_group) { - val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + val groups = linkedSetOf() + App.db.bookSourceDao.allGroup.forEach { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp + } customView = alertBinding.root okButton { alertBinding.editView.text?.toString()?.let { group -> diff --git a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt index 41bda67fd..5ad9aeee7 100644 --- a/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/association/ImportRssSourceDialog.kt @@ -10,10 +10,12 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseDialogFragment import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.RecyclerAdapter +import io.legado.app.constant.AppPattern import io.legado.app.constant.PreferKey import io.legado.app.data.entities.RssSource import io.legado.app.databinding.DialogEditTextBinding @@ -21,10 +23,8 @@ import io.legado.app.databinding.DialogRecyclerViewBinding import io.legado.app.databinding.ItemSourceImportBinding import io.legado.app.help.AppConfig import io.legado.app.lib.dialogs.alert -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.putPrefBoolean +import io.legado.app.utils.* import io.legado.app.utils.viewbindingdelegate.viewBinding -import io.legado.app.utils.visible import org.jetbrains.anko.sdk27.listeners.onClick /** @@ -85,7 +85,14 @@ class ImportRssSourceDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListe when (item.itemId) { R.id.menu_new_group -> { alert(R.string.diy_edit_source_group) { - val alertBinding = DialogEditTextBinding.inflate(layoutInflater) + val alertBinding = DialogEditTextBinding.inflate(layoutInflater).apply { + val groups = linkedSetOf() + App.db.rssSourceDao.allGroup.forEach { group -> + groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) + } + editView.setFilterValues(groups.toList()) + editView.dropDownHeight = 180.dp + } customView = alertBinding.root okButton { alertBinding.editView.text?.toString()?.let { group -> 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 1c5071173..fe26451fb 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 @@ -84,10 +84,10 @@ class ChangeSourceDialog : BaseDialogFragment(), binding.toolBar.inflateMenu(R.menu.change_source) binding.toolBar.menu.applyTint(requireContext()) binding.toolBar.setOnMenuItemClickListener(this) - binding.toolBar.menu.findItem(R.id.menu_load_info)?.isChecked = - AppConfig.changeSourceLoadInfo - binding.toolBar.menu.findItem(R.id.menu_load_toc)?.isChecked = AppConfig.changeSourceLoadToc - + binding.toolBar.menu.findItem(R.id.menu_load_info) + ?.isChecked = AppConfig.changeSourceLoadInfo + binding.toolBar.menu.findItem(R.id.menu_load_toc) + ?.isChecked = AppConfig.changeSourceLoadToc } private fun initRecyclerView() { 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 279cf7d79..0c738d93e 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 @@ -88,6 +88,7 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } private fun searchFinish(searchBook: SearchBook) { + if (searchBooks.contains(searchBook)) return App.db.searchBookDao.insert(searchBook) if (screenKey.isEmpty()) { searchBooks.add(searchBook) @@ -99,6 +100,9 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio private fun startSearch() { execute { + App.db.searchBookDao.clear(name, author) + searchBooks.clear() + upAdapter() bookSourceList.clear() if (searchGroup.isBlank()) { bookSourceList.addAll(App.db.bookSourceDao.allEnabled) @@ -137,7 +141,6 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } else { searchFinish(searchBook) } - return@onSuccess } } } 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 02040e26d..14482296a 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 @@ -51,10 +51,8 @@ import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.delay import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.toast @@ -171,8 +169,6 @@ class ReadBookActivity : ReadBookBaseActivity(), R.id.menu_group_on_line_ns -> item.isVisible = onLine R.id.menu_group_local -> item.isVisible = !onLine R.id.menu_group_text -> item.isVisible = book.isLocalTxt() - R.id.menu_group_login -> - item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() else -> when (item.itemId) { R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule() R.id.menu_re_segment -> item.isChecked = book.getReSegment() @@ -245,12 +241,6 @@ class ReadBookActivity : ReadBookBaseActivity(), supportFragmentManager, ReadBook.book?.tocUrl ) - R.id.menu_login -> ReadBook.webBook?.bookSource?.let { - startActivity( - Pair("sourceUrl", it.bookSourceUrl), - Pair("loginUrl", it.loginUrl) - ) - } R.id.menu_set_charset -> showCharsetConfig() R.id.menu_get_progress -> ReadBook.book?.let { viewModel.syncBookProgress(it) { progress -> @@ -310,6 +300,14 @@ class ReadBookActivity : ReadBookBaseActivity(), return true } } + keyCode == KeyEvent.KEYCODE_PAGE_UP -> { + binding.readView.pageDelegate?.keyTurnPage(PageDirection.PREV) + return true + } + keyCode == KeyEvent.KEYCODE_PAGE_DOWN -> { + binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) + return true + } keyCode == KeyEvent.KEYCODE_SPACE -> { binding.readView.pageDelegate?.keyTurnPage(PageDirection.NEXT) return true @@ -531,13 +529,18 @@ class ReadBookActivity : ReadBookBaseActivity(), /** * 更新内容 */ - override fun upContent(relativePosition: Int, resetPageOffset: Boolean) { - autoPageProgress = 0 + override fun upContent( + relativePosition: Int, + resetPageOffset: Boolean, + success: (() -> Unit)? + ) { launch { + autoPageProgress = 0 binding.readView.upContent(relativePosition, resetPageOffset) binding.readMenu.setSeekPage(ReadBook.durPageIndex()) + loadStates = false + success?.invoke() } - loadStates = false } /** @@ -715,6 +718,15 @@ class ReadBookActivity : ReadBookBaseActivity(), upNavigationBarColor() } + override fun showLogin() { + ReadBook.webBook?.bookSource?.let { + startActivity( + Pair("sourceUrl", it.bookSourceUrl), + Pair("loginUrl", it.loginUrl) + ) + } + } + /** * 朗读按钮 */ @@ -775,7 +787,9 @@ class ReadBookActivity : ReadBookBaseActivity(), super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { when (requestCode) { - requestCodeEditSource -> viewModel.upBookSource() + requestCodeEditSource -> viewModel.upBookSource { + upView() + } requestCodeChapterList -> data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> if (index != ReadBook.durChapterIndex) { @@ -795,43 +809,35 @@ class ReadBookActivity : ReadBookBaseActivity(), } private fun skipToSearch(index: Int, indexWithinChapter: Int) { - launch(IO) { - viewModel.openChapter(index) - // block until load correct chapter and pages - var pages = ReadBook.curTextChapter?.pages - while (ReadBook.durChapterIndex != index || pages == null) { - delay(100L) - pages = ReadBook.curTextChapter?.pages - } + viewModel.openChapter(index) { + val pages = ReadBook.curTextChapter?.pages ?: return@openChapter val positions = ReadBook.searchResultPositions( pages, indexWithinChapter, viewModel.searchContentQuery ) - while (ReadBook.durPageIndex() != positions[0]) { - delay(100L) - ReadBook.skipToPage(positions[0]) - } - withContext(Main) { - binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) - delay(20L) - when (positions[3]) { - 0 -> binding.readView.curPage.selectEndMoveIndex( - 0, - positions[1], - positions[2] + viewModel.searchContentQuery.length - 1 - ) - 1 -> binding.readView.curPage.selectEndMoveIndex( - 0, - positions[1] + 1, - positions[4] - ) - //consider change page, jump to scroll position - -1 -> binding.readView.curPage - .selectEndMoveIndex(1, 0, positions[4]) + ReadBook.skipToPage(positions[0]) { + launch { + binding.readView.curPage.selectStartMoveIndex(0, positions[1], positions[2]) + delay(20L) + when (positions[3]) { + 0 -> binding.readView.curPage.selectEndMoveIndex( + 0, + positions[1], + positions[2] + viewModel.searchContentQuery.length - 1 + ) + 1 -> binding.readView.curPage.selectEndMoveIndex( + 0, + positions[1] + 1, + positions[4] + ) + //consider change page, jump to scroll position + -1 -> binding.readView.curPage + .selectEndMoveIndex(1, 0, positions[4]) + } + binding.readView.isTextSelected = true + delay(100L) } - binding.readView.isTextSelected = true - delay(100L) } } } 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 de9c080d7..d73f96658 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 @@ -250,7 +250,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } - fun openChapter(index: Int, durChapterPos: Int = 0) { + fun openChapter(index: Int, durChapterPos: Int = 0, success: (() -> Unit)? = null) { ReadBook.prevTextChapter = null ReadBook.curTextChapter = null ReadBook.nextTextChapter = null @@ -260,7 +260,9 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { ReadBook.durChapterPos = durChapterPos } ReadBook.saveRead() - ReadBook.loadContent(resetPageOffset = true) + ReadBook.loadContent(resetPageOffset = true) { + success?.invoke() + } } fun removeFromBookshelf(success: (() -> Unit)?) { @@ -271,13 +273,15 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } - fun upBookSource() { + fun upBookSource(success: (() -> Unit)?) { execute { ReadBook.book?.let { book -> App.db.bookSourceDao.getBookSource(book.origin)?.let { ReadBook.webBook = WebBook(it) } } + }.onSuccess { + success?.invoke() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt index a73f3d4a2..63fa5c0f1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/ReadMenu.kt @@ -9,6 +9,7 @@ import android.view.WindowManager import android.view.animation.Animation import android.widget.FrameLayout import android.widget.SeekBar +import androidx.core.view.isGone import androidx.core.view.isVisible import io.legado.app.App import io.legado.app.R @@ -140,6 +141,9 @@ class ReadMenu @JvmOverloads constructor( tvChapterUrl.onClick { context.openUrl(binding.tvChapterUrl.text.toString()) } + tvLogin.onClick { + callBack.showLogin() + } ivBrightnessAuto.onClick { context.putPrefBoolean("brightnessAuto", !brightnessAuto()) upBrightnessState() @@ -277,6 +281,7 @@ class ReadMenu @JvmOverloads constructor( } fun upBookView() { + binding.tvLogin.isGone = ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() ReadBook.curTextChapter?.let { binding.tvChapterName.text = it.title binding.tvChapterName.visible() @@ -323,6 +328,7 @@ class ReadMenu @JvmOverloads constructor( fun upSystemUiVisibility() fun onClickReadAloud() fun showReadMenuHelp() + fun showLogin() } } 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 4d431f8a5..10a454109 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 @@ -347,8 +347,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at selectStart[0] = relativePage selectStart[1] = lineIndex selectStart[2] = charIndex - val textLine = relativePage(relativePage).textLines[lineIndex] - val textChar = textLine.textChars[charIndex] + val textLine = relativePage(relativePage).getLine(lineIndex) + val textChar = textLine.getTextChar(charIndex) upSelectedStart( textChar.start, textLine.lineBottom + relativeOffset(relativePage), @@ -364,8 +364,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at selectEnd[0] = relativePage selectEnd[1] = lineIndex selectEnd[2] = charIndex - val textLine = relativePage(relativePage).textLines[lineIndex] - val textChar = textLine.textChars[charIndex] + val textLine = relativePage(relativePage).getLine(lineIndex) + val textChar = textLine.getTextChar(charIndex) upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset(relativePage)) upSelectChars() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt index 8bc72438b..2ded3399f 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt @@ -338,17 +338,8 @@ class ReadView(context: Context, attrs: AttributeSet) : } end -= 1 } - kotlin.runCatching { - curPage.selectStartMoveIndex(firstRelativePage, lineStart, start) - curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end) - }.onFailure { - print( - """ - curPage.selectStartMoveIndex($firstRelativePage, $lineStart, $start) - curPage.selectEndMoveIndex($firstRelativePage, $lineEnd, $end) - """.trimIndent() - ) - } + curPage.selectStartMoveIndex(firstRelativePage, lineStart, start) + curPage.selectEndMoveIndex(firstRelativePage, lineEnd, end) } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt index d921a217e..9d73757bb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -28,8 +28,10 @@ data class TextLine( textChars.add(TextChar(charData, start = start, end = end)) } - fun getTextCharAt(index: Int): TextChar { - return textChars[index] + fun getTextChar(index: Int): TextChar { + return textChars.getOrElse(index) { + textChars.last() + } } fun getTextCharReverseAt(index: Int): TextChar { 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 e2d291141..ba57dbc48 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 @@ -25,6 +25,12 @@ data class TextPage( val lineSize get() = textLines.size val charSize get() = text.length + fun getLine(index: Int): TextLine { + return textLines.getOrElse(index) { + textLines.last() + } + } + fun upLinesPosition() = ChapterProvider.apply { if (!ReadBookConfig.textBottomJustify) return@apply if (textLines.size <= 1) return@apply diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt index 0aeceded0..aa78e4a5b 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceActivity.kt @@ -58,7 +58,7 @@ class BookSourceActivity : VMBaseActivity>? = null - private var groups = linkedSetOf() + private val groups = linkedSetOf() private var groupMenu: SubMenu? = null private var sort = Sort.Default private var sortAscending = true @@ -130,6 +130,11 @@ class BookSourceActivity : VMBaseActivity { + item.isChecked = true + sortCheck(Sort.Enable) + initLiveDataBookSource(searchView.query?.toString()) + } R.id.menu_enabled_group -> { searchView.setQuery(getString(R.string.enabled), true) } @@ -187,29 +192,44 @@ class BookSourceActivity : VMBaseActivity { App.db.bookSourceDao.liveDataSearch("%$searchKey%") } - } - bookSourceLiveDate?.observe(this, { data -> - val sourceList = - if (sortAscending) when (sort) { - Sort.Weight -> data.sortedBy { it.weight } - Sort.Name -> data.sortedWith { o1, o2 -> - o1.bookSourceName.cnCompare(o2.bookSourceName) + }.apply { + observe(this@BookSourceActivity, { data -> + val sourceList = + if (sortAscending) when (sort) { + Sort.Weight -> data.sortedBy { it.weight } + Sort.Name -> data.sortedWith { o1, o2 -> + o1.bookSourceName.cnCompare(o2.bookSourceName) + } + Sort.Url -> data.sortedBy { it.bookSourceUrl } + Sort.Update -> data.sortedByDescending { it.lastUpdateTime } + Sort.Enable -> data.sortedWith { o1, o2 -> + var sort = -o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + else -> data } - Sort.Url -> data.sortedBy { it.bookSourceUrl } - Sort.Update -> data.sortedByDescending { it.lastUpdateTime } - else -> data - } - else when (sort) { - Sort.Weight -> data.sortedByDescending { it.weight } - Sort.Name -> data.sortedWith { o1, o2 -> - o2.bookSourceName.cnCompare(o1.bookSourceName) + else when (sort) { + Sort.Weight -> data.sortedByDescending { it.weight } + Sort.Name -> data.sortedWith { o1, o2 -> + o2.bookSourceName.cnCompare(o1.bookSourceName) + } + Sort.Url -> data.sortedByDescending { it.bookSourceUrl } + Sort.Update -> data.sortedBy { it.lastUpdateTime } + Sort.Enable -> data.sortedWith { o1, o2 -> + var sort = o1.enabled.compareTo(o2.enabled) + if (sort == 0) { + sort = o1.bookSourceName.cnCompare(o2.bookSourceName) + } + sort + } + else -> data.reversed() } - Sort.Url -> data.sortedByDescending { it.bookSourceUrl } - Sort.Update -> data.sortedBy { it.lastUpdateTime } - else -> data.reversed() - } - adapter.setItems(sourceList, adapter.diffItemCallback) - }) + adapter.setItems(sourceList, adapter.diffItemCallback) + }) + } } private fun showHelp() { @@ -229,7 +249,7 @@ class BookSourceActivity : VMBaseActivity + it.forEach { group -> groups.addAll(group.splitNotBlank(AppPattern.splitGroupRegex)) } upGroupMenu() @@ -483,6 +503,6 @@ class BookSourceActivity : VMBaseActivity clearCache() PreferKey.defaultCover -> if (getPrefString(PreferKey.defaultCover).isNullOrEmpty()) { - selectDefaultCover() + selectImage(requestCodeCover) } else { selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> if (i == 0) { removePref(PreferKey.defaultCover) } else { - selectDefaultCover() + selectImage(requestCodeCover) } } } @@ -183,11 +183,12 @@ class OtherConfigFragment : BasePreferenceFragment(), }.show() } - private fun selectDefaultCover() { + @Suppress("SameParameterValue") + private fun selectImage(requestCode: Int) { val intent = Intent(Intent.ACTION_GET_CONTENT) intent.addCategory(Intent.CATEGORY_OPENABLE) intent.type = "image/*" - startActivityForResult(intent, requestCodeCover) + startActivityForResult(intent, requestCode) } private fun isProcessTextEnabled(): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt index b7a29434e..8248a840a 100644 --- a/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt +++ b/app/src/main/java/io/legado/app/ui/config/ThemeConfigFragment.kt @@ -1,13 +1,17 @@ package io.legado.app.ui.config import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent import android.content.SharedPreferences +import android.net.Uri import android.os.Build import android.os.Bundle import android.view.Menu import android.view.MenuInflater import android.view.MenuItem import android.view.View +import androidx.documentfile.provider.DocumentFile import androidx.preference.Preference import io.legado.app.App import io.legado.app.R @@ -19,24 +23,38 @@ import io.legado.app.databinding.DialogEditTextBinding import io.legado.app.help.AppConfig import io.legado.app.help.LauncherIconHelp import io.legado.app.help.ThemeConfig +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.selector import io.legado.app.lib.theme.ATH import io.legado.app.ui.widget.number.NumberPickerDialog import io.legado.app.ui.widget.prefs.ColorPreference -import io.legado.app.ui.widget.prefs.IconListPreference +import io.legado.app.ui.widget.prefs.PreferenceCategory import io.legado.app.utils.* +import java.io.File @Suppress("SameParameterValue") class ThemeConfigFragment : BasePreferenceFragment(), SharedPreferences.OnSharedPreferenceChangeListener { + private val requestCodeBgImage = 234 + private val requestCodeBgImageN = 342 + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_theme) if (Build.VERSION.SDK_INT < 26) { - findPreference(PreferKey.launcherIcon)?.let { - preferenceScreen.removePreference(it) - } + preferenceScreen.removePreferenceRecursively(PreferKey.launcherIcon) + } + if (AppConfig.isGooglePlay) { + upPreferenceSummary(PreferKey.bgImage, getPrefString(PreferKey.bgImage)) + upPreferenceSummary(PreferKey.bgImageN, getPrefString(PreferKey.bgImageN)) + } else { + findPreference("dayThemeCategory") + ?.removePreferenceRecursively(PreferKey.bgImage) + findPreference("nightThemeCategory") + ?.removePreferenceRecursively(PreferKey.bgImageN) } upPreferenceSummary(PreferKey.barElevation, AppConfig.elevation.toString()) findPreference(PreferKey.cBackground)?.let { @@ -168,10 +186,39 @@ class ThemeConfigFragment : BasePreferenceFragment(), } "themeList" -> ThemeListDialog().show(childFragmentManager, "themeList") "saveDayTheme", "saveNightTheme" -> saveThemeAlert(key) + PreferKey.bgImage -> if (getPrefString(PreferKey.bgImage).isNullOrEmpty()) { + selectImage(requestCodeBgImage) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.bgImage) + } else { + selectImage(requestCodeBgImage) + } + } + } + PreferKey.bgImageN -> if (getPrefString(PreferKey.bgImageN).isNullOrEmpty()) { + selectImage(requestCodeBgImageN) + } else { + selector(items = arrayListOf("删除图片", "选择图片")) { _, i -> + if (i == 0) { + removePref(PreferKey.bgImageN) + } else { + selectImage(requestCodeBgImageN) + } + } + } } return super.onPreferenceTreeClick(preference) } + private fun selectImage(requestCode: Int) { + val intent = Intent(Intent.ACTION_GET_CONTENT) + intent.addCategory(Intent.CATEGORY_OPENABLE) + intent.type = "image/*" + startActivityForResult(intent, requestCode) + } + @SuppressLint("InflateParams") private fun saveThemeAlert(key: String) { alert(R.string.theme_name) { @@ -211,6 +258,61 @@ class ThemeConfigFragment : BasePreferenceFragment(), when (preferenceKey) { PreferKey.barElevation -> preference.summary = getString(R.string.bar_elevation_s, value) + else -> preference.summary = value + } + } + + private fun setBgFromUri(uri: Uri, preferenceKey: String) { + if (uri.isContentScheme()) { + val doc = DocumentFile.fromSingleUri(requireContext(), uri) + doc?.name?.let { + var file = requireContext().externalFilesDir + file = FileUtils.createFileIfNotExist(file, preferenceKey, it) + kotlin.runCatching { + DocumentUtils.readBytes(requireContext(), doc.uri) + }.getOrNull()?.let { byteArray -> + file.writeBytes(byteArray) + putPrefString(preferenceKey, file.absolutePath) + + } ?: toast("获取文件出错") + } + } else { + PermissionsCompat.Builder(this) + .addPermissions( + Permissions.READ_EXTERNAL_STORAGE, + Permissions.WRITE_EXTERNAL_STORAGE + ) + .rationale(R.string.bg_image_per) + .onGranted { + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + val imgFile = File(path) + if (imgFile.exists()) { + var file = requireContext().externalFilesDir + file = FileUtils.createFileIfNotExist(file, preferenceKey, imgFile.name) + file.writeBytes(imgFile.readBytes()) + putPrefString(preferenceKey, file.absolutePath) + + } + } + } + .request() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + requestCodeBgImage -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + setBgFromUri(uri, PreferKey.bgImage) + } + } + requestCodeBgImageN -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + setBgFromUri(uri, PreferKey.bgImageN) + } + } } } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt index 88969fe5d..c115e661a 100644 --- a/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/MainViewModel.kt @@ -157,6 +157,9 @@ class MainViewModel(application: Application) : BaseViewModel(application) { if (LocalConfig.hasUpTxtTocRule) { DefaultData.importDefaultTocRules() } + if (LocalConfig.hasUpRssSources) { + DefaultData.importDefaultRssSources() + } } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt index 1fb43003b..ce6065e27 100644 --- a/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/my/MyFragment.kt @@ -96,7 +96,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config), FilePickerDialog.C } if (AppConfig.isGooglePlay) { findPreference("aboutCategory") - ?.removePreference(findPreference("donate")) + ?.removePreferenceRecursively("donate") } } 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 60db06649..daf1dde0d 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 @@ -92,7 +92,7 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), } override fun onQueryTextChange(newText: String?): Boolean { - initData(newText ?: "") + initData(newText) return false } }) @@ -105,7 +105,7 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), adapter.addHeaderView { ItemRssBinding.inflate(layoutInflater, it, false).apply { tvName.setText(R.string.rule_subscription) - ivIcon.setImageResource(R.mipmap.ic_launcher) + ivIcon.setImageResource(R.drawable.image_legado) root.onClick { startActivity() } @@ -113,9 +113,16 @@ class RssFragment : VMBaseFragment(R.layout.fragment_rss), } } - private fun initData(searchKey: String = "") { + private fun initData(searchKey: String? = null) { liveRssData?.removeObservers(this) - liveRssData = App.db.rssSourceDao.liveEnabled(searchKey).apply { + liveRssData = when { + searchKey.isNullOrEmpty() -> App.db.rssSourceDao.liveEnabled() + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + App.db.rssSourceDao.liveEnabledByGroup("%$key%") + } + else -> App.db.rssSourceDao.liveEnabled("%$searchKey%") + }.apply { observe(viewLifecycleOwner, { adapter.setItems(it) }) diff --git a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt index 548d9a481..af44e0c32 100644 --- a/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/replace/ReplaceRuleActivity.kt @@ -154,14 +154,15 @@ class ReplaceRuleActivity : VMBaseActivity { App.db.replaceRuleDao.liveDataSearch("%$searchKey%") } + }.apply { + observe(this@ReplaceRuleActivity, { + if (dataInit) { + setResult(Activity.RESULT_OK) + } + adapter.setItems(it, adapter.diffItemCallBack) + dataInit = true + }) } - replaceRuleLiveData?.observe(this, { - if (dataInit) { - setResult(Activity.RESULT_OK) - } - adapter.setItems(it, adapter.diffItemCallBack) - dataInit = true - }) } private fun observeGroupData() { @@ -239,7 +240,7 @@ class ReplaceRuleActivity : VMBaseActivity @@ -60,7 +60,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application page++ val pageUrl = nextPageUrl if (!pageUrl.isNullOrEmpty()) { - Rss.getArticles(sortName, pageUrl, rssSource, page) + Rss.getArticles(this, sortName, pageUrl, rssSource, page) .onSuccess(Dispatchers.IO) { nextPageUrl = it.nextPageUrl loadMoreSuccess(it.articles) 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 0c09f71e6..452f8c87f 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 @@ -94,7 +94,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } private fun loadContent(rssArticle: RssArticle, ruleContent: String) { - Rss.getContent(rssArticle, ruleContent, rssSource, this) + Rss.getContent(this, rssArticle, ruleContent, rssSource) .onSuccess(IO) { body -> rssArticle.description = body App.db.rssArticleDao.insert(rssArticle) diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt index 2d069deab..d903da070 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugModel.kt @@ -31,7 +31,7 @@ class RssSourceDebugModel(application: Application) : BaseViewModel(application) rssSource?.let { start?.invoke() Debug.callback = this - Debug.startDebug(it) + Debug.startDebug(this, it) } ?: error?.invoke() } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt index 0f7d10e8d..1e6156391 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceActivity.kt @@ -92,6 +92,7 @@ class RssSourceActivity : VMBaseActivity viewModel.shareSelection(adapter.getSelection()) { startActivity(Intent.createChooser(it, getString(R.string.share_selected_source))) } + R.id.menu_import_default -> viewModel.importDefault() R.id.menu_help -> showHelp() else -> if (item.groupId == R.id.source_group) { binding.titleBar.findViewById(R.id.search_view) @@ -199,22 +200,22 @@ class RssSourceActivity : VMBaseActivity { - App.db.rssSourceDao.liveAll() - } - searchKey.startsWith("group:") -> { - val key = searchKey.substringAfter("group:") - App.db.rssSourceDao.liveGroupSearch("%$key%") - } - else -> { - App.db.rssSourceDao.liveSearch("%$searchKey%") - } + sourceLiveData = when { + searchKey.isNullOrBlank() -> { + App.db.rssSourceDao.liveAll() } - sourceLiveData?.observe(this, { - adapter.setItems(it, adapter.diffItemCallback) - }) + searchKey.startsWith("group:") -> { + val key = searchKey.substringAfter("group:") + App.db.rssSourceDao.liveGroupSearch("%$key%") + } + else -> { + App.db.rssSourceDao.liveSearch("%$searchKey%") + } + }.apply { + observe(this@RssSourceActivity, { + adapter.setItems(it, adapter.diffItemCallback) + }) + } } private fun showHelp() { 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 9a90932df..7ff23307c 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 @@ -9,6 +9,7 @@ import io.legado.app.App import io.legado.app.BuildConfig import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.RssSource +import io.legado.app.help.DefaultData import io.legado.app.utils.* import org.jetbrains.anko.toast import java.io.File @@ -166,4 +167,10 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } } + fun importDefault() { + execute { + DefaultData.importDefaultRssSources() + } + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/StringExtensions.kt b/app/src/main/java/io/legado/app/utils/StringExtensions.kt index 4263fc896..b6a6d23e2 100644 --- a/app/src/main/java/io/legado/app/utils/StringExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt @@ -10,7 +10,7 @@ import java.util.* val removeHtmlRegex = "]*>".toRegex() val imgRegex = "]*>".toRegex() -val notImgHtmlRegex = "]*>".toRegex() +val notImgHtmlRegex = "])[^<>]*>".toRegex() fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() diff --git a/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt b/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt index 8b18d1179..747a741f5 100644 --- a/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt +++ b/app/src/main/java/io/legado/app/web/SourceDebugWebSocket.kt @@ -1,6 +1,7 @@ package io.legado.app.web +import android.os.Looper import fi.iki.elonen.NanoHTTPD import fi.iki.elonen.NanoWSD import io.legado.app.App @@ -22,12 +23,12 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : override fun onOpen() { launch(IO) { - do { - delay(30000) - runCatching { + kotlin.runCatching { + while (isOpen) { ping("ping".toByteArray()) + delay(30000) } - } while (isOpen) + } } } @@ -42,21 +43,21 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : override fun onMessage(message: NanoWSD.WebSocketFrame) { if (!message.textPayload.isJson()) return - kotlin.runCatching { - val debugBean = GSON.fromJsonObject>(message.textPayload) - if (debugBean != null) { - val tag = debugBean["tag"] - val key = debugBean["key"] - if (tag.isNullOrBlank() || key.isNullOrBlank()) { - kotlin.runCatching { + launch(IO) { + kotlin.runCatching { + val debugBean = GSON.fromJsonObject>(message.textPayload) + if (debugBean != null) { + val tag = debugBean["tag"] + val key = debugBean["key"] + if (tag.isNullOrBlank() || key.isNullOrBlank()) { send(App.INSTANCE.getString(R.string.cannot_empty)) close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + return@launch + } + App.db.bookSourceDao.getBookSource(tag)?.let { + Debug.callback = this@SourceDebugWebSocket + Debug.startDebug(this, WebBook(it), key) } - return - } - App.db.bookSourceDao.getBookSource(tag)?.let { - Debug.callback = this - Debug.startDebug(this, WebBook(it), key) } } } @@ -71,10 +72,27 @@ class SourceDebugWebSocket(handshakeRequest: NanoHTTPD.IHTTPSession) : } override fun printLog(state: Int, msg: String) { - kotlin.runCatching { - send(msg) - if (state == -1 || state == 1000) { - Debug.cancelDebug(true) + if (Looper.getMainLooper() == Looper.myLooper()) { + launch(IO) { + runCatching { + send(msg) + if (state == -1 || state == 1000) { + Debug.cancelDebug(true) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + } + }.onFailure { + it.printStackTrace() + } + } + } else { + runCatching { + send(msg) + if (state == -1 || state == 1000) { + Debug.cancelDebug(true) + close(NanoWSD.WebSocketFrame.CloseCode.NormalClosure, "调试结束", false) + } + }.onFailure { + it.printStackTrace() } } } diff --git a/app/src/main/res/drawable/image_legado.png b/app/src/main/res/drawable/image_legado.png new file mode 100644 index 000000000..6d101d806 Binary files /dev/null and b/app/src/main/res/drawable/image_legado.png differ diff --git a/app/src/main/res/layout/dialog_bookmark.xml b/app/src/main/res/layout/dialog_bookmark.xml index 83bc712bb..168324bfd 100644 --- a/app/src/main/res/layout/dialog_bookmark.xml +++ b/app/src/main/res/layout/dialog_bookmark.xml @@ -22,6 +22,7 @@ android:id="@+id/edit_book_text" android:layout_width="match_parent" android:layout_height="wrap_content" + android:maxLines="6" android:hint="@string/content" /> @@ -37,6 +38,7 @@ android:id="@+id/edit_view" android:layout_width="match_parent" android:layout_height="wrap_content" + android:maxLines="6" android:hint="@string/note_content" /> diff --git a/app/src/main/res/layout/item_bookshelf_list.xml b/app/src/main/res/layout/item_bookshelf_list.xml index f6e0a694d..5a9fefbd2 100644 --- a/app/src/main/res/layout/item_bookshelf_list.xml +++ b/app/src/main/res/layout/item_bookshelf_list.xml @@ -11,8 +11,8 @@ @@ -126,7 +126,7 @@ android:textSize="13sp" app:layout_constraintBottom_toTopOf="@id/tv_last" app:layout_constraintLeft_toRightOf="@+id/iv_read" - app:layout_constraintRight_toLeftOf="@id/fl_has_new" + app:layout_constraintRight_toRightOf="@+id/fl_has_new" app:layout_constraintTop_toBottomOf="@+id/tv_author" /> - - - + android:layout_height="wrap_content"> + + + + + + + + @@ -48,19 +72,19 @@ android:layout_width="wrap_content" android:layout_height="0dp" android:layout_margin="8dp" - android:orientation="vertical" android:gravity="center_horizontal" - app:layout_constraintTop_toBottomOf="@+id/title_bar" + android:orientation="vertical" + app:layout_constraintBottom_toTopOf="@+id/bottom_menu" app:layout_constraintLeft_toLeftOf="parent" - app:layout_constraintBottom_toTopOf="@+id/bottom_menu"> + app:layout_constraintTop_toBottomOf="@+id/title_bar"> + android:contentDescription="@string/brightness_auto" + android:src="@drawable/ic_brightness_auto" /> @@ -187,8 +211,8 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:background="@color/background_menu" - android:orientation="vertical" - android:importantForAccessibility="no"> + android:importantForAccessibility="no" + android:orientation="vertical"> + android:importantForAccessibility="no" + android:orientation="horizontal"> + android:importantForAccessibility="no" + android:orientation="horizontal"> @@ -322,8 +346,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" - android:singleLine="true" android:ellipsize="middle" + android:singleLine="true" android:text="@string/read_aloud" android:textColor="@color/primaryText" android:textSize="12sp" /> @@ -363,8 +387,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" - android:singleLine="true" android:ellipsize="middle" + android:singleLine="true" android:text="@string/interface_setting" android:textColor="@color/primaryText" android:textSize="12sp" /> @@ -404,8 +428,8 @@ android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:layout_marginTop="3dp" - android:singleLine="true" android:ellipsize="middle" + android:singleLine="true" android:text="@string/setting" android:textColor="@color/primaryText" android:textSize="12sp" /> diff --git a/app/src/main/res/menu/book_read.xml b/app/src/main/res/menu/book_read.xml index f35b5b901..b34861c5a 100644 --- a/app/src/main/res/menu/book_read.xml +++ b/app/src/main/res/menu/book_read.xml @@ -49,14 +49,6 @@ android:title="@string/bookmark_add" app:showAsAction="never" /> - - - - + + + diff --git a/app/src/main/res/menu/rss_source.xml b/app/src/main/res/menu/rss_source.xml index b8915e094..739b68d58 100644 --- a/app/src/main/res/menu/rss_source.xml +++ b/app/src/main/res/menu/rss_source.xml @@ -48,6 +48,12 @@ android:title="@string/import_by_qr_code" app:showAsAction="never" /> + + 导入书单 预下载 预先下载10章正文 + 启用排序 + 背景图片 diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml index af90b7f86..73f4ea493 100644 --- a/app/src/main/res/values-zh-rTW/strings.xml +++ b/app/src/main/res/values-zh-rTW/strings.xml @@ -13,6 +13,7 @@ 離線快取書籍備份 匯出本機同時備份到legado資料夾下exports目錄 備份路徑 + 請選擇備份路徑 匯入舊版資料 匯入Github資料 淨化取代 @@ -29,6 +30,7 @@ 提醒 編輯 刪除 + 刪除所有 取代 取代淨化 配置取代淨化規則 @@ -388,11 +390,14 @@ 源名稱(sourceName) 源URL(sourceUrl) 源分組(sourceGroup) + 自訂源分組 + 輸入自訂源分組名稱 + 【%s】 分類Url 登入URL(loginUrl) + 源注釋(sourceComment) 搜尋地址(url) 發現地址規則(url) - 源注釋(sourceComment) 書籍列表規則(bookList) 書名規則(name) 詳情頁url規則(bookUrl) @@ -415,6 +420,8 @@ 正文規則(content) 正文下一頁URL規則(nextContentUrl) WebViewJs(webJs) + 圖片樣式(imageStyle) + 取代規則(replaceRegex) 資源正則(sourceRegex) 圖示(sourceIcon) @@ -449,6 +456,7 @@ 請求頭(header) 除錯源 二維碼匯入 + 分享選取源 掃描二維碼 選中時點擊可彈出選單 主題 @@ -684,6 +692,7 @@ 訊息 切換布局 + 文章字重轉換 主色調 @@ -702,16 +711,14 @@ 夜間,底欄色 自動換源 文字兩端對齊 + 文字底部對齊 自動翻頁速度 地址排序 - 文章字體轉換 - 請選擇備份路徑 本機和 WebDav 一起備份 優先從 WebDav 復原,長按從本機復原 選擇舊版備份資料夾 已啟用 已禁用 - 文字底部對齊 正在啟動下載 該書已在下載列表 點擊打開 @@ -729,8 +736,6 @@ 復原忽略列表 復原時忽略一些內容不復原,方便不同手機配置不同 閱讀介面設定 - 圖片樣式(imageStyle) - 取代規則(replaceRegex) 分組名稱 備註內容 預設啟用取代淨化 @@ -755,7 +760,6 @@ 執行緒數 總閱讀時間 全不選 - 刪除所有 匯入 匯出 儲存主題配置 @@ -764,7 +768,6 @@ 主題列表 使用儲存主題,匯入,分享主題 切換預設主題 - 分享選中源 時間排序 全文搜尋 關注公眾號[开源阅读]獲取訂閱源! @@ -800,5 +803,7 @@ 导入书单 预下载 预先下载10章正文 + 启用排序 + 背景图片 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 48eb53cb6..6ff4b0be6 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -803,5 +803,7 @@ 导入书单 预下载 预先下载10章正文 + 启用排序 + 背景图片 diff --git a/app/src/main/res/values/pref_key_value.xml b/app/src/main/res/values/pref_key_value.xml index 78e9f018d..ebaac231b 100644 --- a/app/src/main/res/values/pref_key_value.xml +++ b/app/src/main/res/values/pref_key_value.xml @@ -13,4 +13,5 @@ https://github.com/gedoor/legado/releases/latest https://api.github.com/repos/gedoor/legado/releases/latest https://t.me/yueduguanfang + https://discord.gg/qDE52P5xGW diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d2c078fec..2472dbe3c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -806,5 +806,7 @@ 导入书单 预下载 预先下载10章正文 + 启用排序 + 背景图片 diff --git a/app/src/main/res/xml/about.xml b/app/src/main/res/xml/about.xml index e8881231a..69e339435 100644 --- a/app/src/main/res/xml/about.xml +++ b/app/src/main/res/xml/about.xml @@ -9,13 +9,15 @@ app:iconSpaceReserved="false" /> @@ -51,6 +53,12 @@ android:summary="@string/this_github_url" app:iconSpaceReserved="false" /> + + + + + + + + + + + + + + \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4e4d97e17..5de0edc73 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ buildscript { maven { url "https://plugins.gradle.org/m2/" } } dependencies { - classpath "com.android.tools.build:gradle:4.1.1" + classpath 'com.android.tools.build:gradle:4.1.2' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" classpath "de.timfreiheit.resourceplaceholders:placeholders:0.3" classpath "com.google.gms:google-services:4.3.4"