diff --git a/.gitignore b/.gitignore index 5f0e23e18..2543443bb 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,6 @@ /captures .externalNativeBuild /release -/tmp \ No newline at end of file +/tmp +node_modules/ +package-lock.json diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..ace01cdff --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +# 1.0.0 (2020-02-09) + + + diff --git a/README.md b/README.md index d65e3dbdc..ac01e9389 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,9 @@ # legado + +[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) + ## 阅读3.0 +书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg + +## 免责声明 +https://gedoor.github.io/MyBookshelf/disclaimer.html diff --git a/app/build.gradle b/app/build.gradle index 356d95bb4..585099975 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -20,13 +20,15 @@ def gitCommits = Integer.parseInt('git rev-list --count HEAD'.execute([], projec android { compileSdkVersion 29 signingConfigs { - myConfig { - storeFile file(RELEASE_STORE_FILE) - storePassword RELEASE_STORE_PASSWORD - keyAlias RELEASE_KEY_ALIAS - keyPassword RELEASE_KEY_PASSWORD - v1SigningEnabled true - v2SigningEnabled true + if (project.hasProperty("RELEASE_STORE_FILE")) { + myConfig { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + v1SigningEnabled true + v2SigningEnabled true + } } } defaultConfig { @@ -49,12 +51,16 @@ android { } buildTypes { release { - signingConfig signingConfigs.myConfig + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.myConfig + } minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { - signingConfig signingConfigs.myConfig + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.myConfig + } applicationIdSuffix '.debug' versionNameSuffix 'debug' minifyEnabled false @@ -96,22 +102,23 @@ dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" //fireBase - implementation 'com.google.firebase:firebase-core:17.2.1' + implementation 'com.google.firebase:firebase-core:17.2.2' implementation 'com.crashlytics.sdk.android:crashlytics:2.10.1' //androidX - implementation 'androidx.core:core-ktx:1.2.0-rc01' + 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.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.2.0-alpha03' + 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' //lifecycle - def lifecycle_version = '2.1.0' + def lifecycle_version = '2.2.0' implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" @@ -137,13 +144,12 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" //规则相关 - implementation 'com.google.code.gson:gson:2.8.5' implementation 'org.jsoup:jsoup:1.12.1' implementation 'cn.wanghaomiao:JsoupXpath:2.3.2' implementation 'com.jayway.jsonpath:json-path:2.4.0' - //JS - implementation 'com.github.gedoor:rhino-android:1.3' + //JS rhino + implementation 'com.github.gedoor:rhino-android:1.4' //Retrofit implementation 'com.squareup.okhttp3:logging-interceptor:4.1.0' @@ -169,6 +175,9 @@ dependencies { //MarkDown implementation 'ru.noties.markwon:core:3.0.2' + // 转换繁体 + implementation 'com.github.houbb:opencc4j:1.4.0' + } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 23f5edd36..a8ebd70da 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -22,14 +22,53 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:networkSecurityConfig="@xml/network_security_config" - android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme.Light" - android:requestLegacyExternalStorage="false" tools:ignore="AllowBackup,GoogleAppIndexingWarning,UnusedAttribute"> + + + + + + + + + + + + + + + + + + + + + + + + + + android:name=".ui.welcome.Launcher3" + android:icon="@mipmap/launcher3" + android:enabled="false"> @@ -39,22 +78,43 @@ android:resource="@xml/shortcuts" android:launchMode="singleTask" /> + + + + + + + + + + + + + - - - + - @@ -95,14 +150,12 @@ - + + diff --git a/app/src/main/assets/readConfig.json b/app/src/main/assets/readConfig.json index ee546dc37..c26c81554 100644 --- a/app/src/main/assets/readConfig.json +++ b/app/src/main/assets/readConfig.json @@ -1,72 +1,62 @@ [ { - "bgStr": "羊皮纸2.jpg", - "bgType": 1, + "bgStr": "#EBD9BB", + "bgStrNight": "#1E2021", + "textColor": "#63543C", + "textColorNight": "#DCDFE1", + "bgType": 0, + "bgTypeNight": 0, "darkStatusIcon": true, - "textColor": "#5E432E", "textSize": 24, "letterSpacing": 0, - "lineSpacingExtra": 10, - "lineSpacingMultiplier": 1.2, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 0, - "paddingBottom": 0 + "lineSpacingExtra": 10 }, { - "bgStr": "#C6BAA1", + "bgStr": "#DDC090", + "bgStrNight": "#3C3F43", + "textColor": "#3E3422", + "textColorNight": "#DCDFE1", "bgType": 0, + "bgTypeNight": 0, "darkStatusIcon": true, - "textColor": "#5E432E", "textSize": 24, "letterSpacing": 0, - "lineSpacingExtra": 10, - "lineSpacingMultiplier": 1.2, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 0, - "paddingBottom": 0 + "lineSpacingExtra": 10 }, { - "bgStr": "#015A86", + "bgStr": "#C2D8AA", + "bgStrNight": "#3C3F43", + "textColor": "#596C44", + "textColorNight": "#88C16F", "bgType": 0, + "bgTypeNight": 0, "darkStatusIcon": false, - "textColor": "#FFFFFF", "textSize": 24, "letterSpacing": 0, - "lineSpacingExtra": 10, - "lineSpacingMultiplier": 1.2, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 0, - "paddingBottom": 0 + "lineSpacingExtra": 10 }, { - "bgStr": "宁静夜色", - "bgType": 1, + "bgStr": "#DBB8E2", + "bgStrNight": "#3C3F43", + "textColor": "#68516C", + "textColorNight": "#F6AEAE", + "bgType": 0, + "bgTypeNight": 0, "darkStatusIcon": false, - "textColor": "#adadad", "textSize": 24, "letterSpacing": 0, - "lineSpacingExtra": 10, - "lineSpacingMultiplier": 1.2, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 0, - "paddingBottom": 0 + "lineSpacingExtra": 10 }, { - "bgStr": "#000000", + "bgStr": "#ABCEE0", + "bgStrNight": "#3C3F43", + "textColor": "#3D4C54", + "textColorNight": "#90BFF5", "bgType": 0, + "bgTypeNight": 0, "darkStatusIcon": false, - "textColor": "#adadad", "textSize": 24, "letterSpacing": 0, - "lineSpacingExtra": 10, - "lineSpacingMultiplier": 1.2, - "paddingLeft": 16, - "paddingRight": 16, - "paddingTop": 0, - "paddingBottom": 0 + "lineSpacingExtra": 10 } ] \ No newline at end of file diff --git a/app/src/main/assets/txtChapterRule.json b/app/src/main/assets/txtChapterRule.json deleted file mode 100644 index b035fccb5..000000000 --- a/app/src/main/assets/txtChapterRule.json +++ /dev/null @@ -1,38 +0,0 @@ -[ - { - "enable": true, - "name": "默认正则1", - "rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$", - "serialNumber": 0 - }, - { - "enable": true, - "name": "默认正则2", - "rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$", - "serialNumber": 1 - }, - { - "enable": true, - "name": "默认正则3", - "rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$", - "serialNumber": 2 - }, - { - "enable": true, - "name": "默认正则4", - "rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$", - "serialNumber": 3 - }, - { - "enable": true, - "name": "默认正则5", - "rule": "^(\\s{0,4})(正文)(.{0,20})$", - "serialNumber": 4 - }, - { - "enable": true, - "name": "默认正则6", - "rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$", - "serialNumber": 5 - } -] \ No newline at end of file diff --git a/app/src/main/assets/txtTocRule.json b/app/src/main/assets/txtTocRule.json new file mode 100644 index 000000000..4e93e04d7 --- /dev/null +++ b/app/src/main/assets/txtTocRule.json @@ -0,0 +1,62 @@ +[ + { + "enable": true, + "name": "数字 分隔符 标题名称", + "rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", + "serialNumber": 0 + }, + { + "enable": true, + "name": "目录", + "rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", + "serialNumber": 1 + }, + { + "enable": false, + "name": "目录(不匹配行前空白)", + "rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", + "serialNumber": 2 + }, + { + "enable": false, + "name": "目录(去简介)", + "rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", + "serialNumber": 3 + }, + { + "enable": false, + "name": "目录(古典小说备用)", + "rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$", + "serialNumber": 4 + }, + { + "enable": true, + "name": "Chapter/Section/Part 序号 标题", + "rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$", + "serialNumber": 5 + }, + { + "enable": true, + "name": "正文 标题/序号", + "rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$", + "serialNumber": 6 + }, + { + "enable": true, + "name": "特殊符号 序号 标题", + "rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$", + "serialNumber": 7 + }, + { + "enable": true, + "name": "特殊符号 标题", + "rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", + "serialNumber": 8 + }, + { + "enable":false, + "name": "特殊符号 标题(不匹配空白字符)", + "rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", + "serialNumber": 9 + } +] diff --git a/app/src/main/assets/updateLog.md b/app/src/main/assets/updateLog.md index 171c3d744..8b1acd8d9 100644 --- a/app/src/main/assets/updateLog.md +++ b/app/src/main/assets/updateLog.md @@ -1,14 +1,105 @@ ## 更新日志 - * 旧版数据导入教程: * 先在旧版阅读(2.x)中进行备份,然后在新版阅读(3.x)【我的】->【备份与恢复】,选择【导入旧版本数据】。 -**2020/01/11 +**2020/02/23** +* 修复BUG +* 本地目录正则自定义完成 +* 选择文本修复框选不全的问题,增加操作按钮 + +**2020/02/22** +* 长按选择完成 + +**2020/02/21** +* 重写了阅读界面,实现了段距调整,两端对齐,页眉页脚调整 +* 选择文本暂不可用,滚动暂不可用,仿真翻页还有问题 + +**2020/02/19** +* 导出功能完成 +* 其它一些优化,仿真翻页有点问题,还没找到问题所在 + +**2020/02/15** +* 修复bug +* 添加一个图标 +* 阅读界面文本选择开关 +* 书源管理发现开启关闭标志 + +**2020/02/14** +* 书籍分组支持一本书籍在多个分组,既可以在追更,又可以在玄幻 +* 搜索界面限制刷新频率,每秒刷新一次 +* 添加一种图标,2.0的老图标 + +**2020/02/13** +* 修复BUG +* 优化已下载检测,解决目录卡顿 +* 添加切换图标 + +**2020/02/12** +* 修复bug +* 优化,网页编码优先使用书源配置的编码 +* 其它一些优化 +* 添加简繁转换 + +**2020/02/10** +* 多页目录并行获取解析 +* 优化详情页 +* 优化换源页面,添加换源是否加载目录配置 +* 换源顺序按书源顺序排列 + +**2020/02/09** +* 优化书源管理,备份恢复 +* 主题色修改,底部操作栏更明显 + +**2020/02/08** +* 书架分组调整顺序后,书架及时变动 + +**2020/02/07** +* 优化 +* 书源校验 +* 书架整理 + +**2020/02/05** +* 修复bug +* Rss收藏功能完成 +* Rss已读标记不会再丢失 + +**2020/02/04** +* 主界面切换时自动隐藏键盘 +* 添加本地书籍完成,解析txt文件完成,本地txt可以看了 +* 封面换源,书籍信息界面点击封面弹出封面换源界面 +* 默认封面绘制书名和作者 +* 修复在线朗读遇到单独标点,停止朗读的问题 + +**2020/02/02** +* merged commit e584606, rss修复BaseURL模式下部分图片无法加载, 修复可能出现的乱码 +* 菜单添加网址功能完成 + +**2020/01/31** +* 修复搜索闪退,因为默认线程为0了 + +**2020/01/30** +* 优化缓存文件夹选择,不再需要存储权限 +* 修复替换净化导入报错的bug + +**2020/01/27** +* 添加根据系统主题切换夜间模式 +* 合并Modificator提交的代码 + +**2020/01/26** +* 修复bug +* 未加入书架可查看目录 + +**2020/01/24** +* 添加线程数配置 +* 记住退出时的书架 +* 添加屏幕超时配置 + +**2020/01/11** * RSS阅读界面添加朗读功能 * 其它一些优化 * 合并KKL369提交的代码,重写LinearLayoutManager,修复书籍目录模糊搜索后scrollToPosition在可见范围不置顶 -**2020/01/10 +**2020/01/10** * 合并KKL369提交的代码 **2020/01/08** @@ -74,13 +165,13 @@ * 最近感冒了,发热咳嗽还没好,继续咸鱼 **2019/12/12** -* [fix]web服务停止问题 +* web服务停止问题 * 默认显示沉浸式状态栏 **2019/12/09** -* [add]其他设置->清理缓存 -* [mod]调整深色模式配色,预适配Android10 -* [mod]启用web服务 +* 其他设置->清理缓存 +* 调整深色模式配色,预适配Android10 +* 启用web服务 **2019/12/03** * from Celeter: diff --git a/app/src/main/java/io/legado/app/App.kt b/app/src/main/java/io/legado/app/App.kt index ee3e4f5f6..7310dcef8 100644 --- a/app/src/main/java/io/legado/app/App.kt +++ b/app/src/main/java/io/legado/app/App.kt @@ -15,13 +15,12 @@ import io.legado.app.constant.AppConst.channelIdReadAloud import io.legado.app.constant.AppConst.channelIdWeb import io.legado.app.data.AppDatabase import io.legado.app.help.ActivityHelp +import io.legado.app.help.AppConfig import io.legado.app.help.CrashHandler import io.legado.app.help.ReadBookConfig import io.legado.app.lib.theme.ThemeStore -import io.legado.app.ui.book.read.page.ChapterProvider import io.legado.app.utils.getCompatColor import io.legado.app.utils.getPrefInt -import io.legado.app.utils.isNightTheme @Suppress("DEPRECATION") class App : Application() { @@ -50,7 +49,7 @@ class App : Application() { } if (!ThemeStore.isConfigured(this, versionCode)) applyTheme() - initNightTheme() + initNightMode() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) createChannelId() @@ -67,13 +66,13 @@ class App : Application() { * 更新主题 */ fun applyTheme() { - if (isNightTheme) { + if (AppConfig.isNightTheme) { ThemeStore.editTheme(this) .primaryColor( - getPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) + getPrefInt("colorPrimaryNight", getCompatColor(R.color.md_blue_grey_600)) ) .accentColor( - getPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color)) + getPrefInt("colorAccentNight", getCompatColor(R.color.md_brown_800)) ) .backgroundColor( getPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) @@ -82,31 +81,32 @@ class App : Application() { } else { ThemeStore.editTheme(this) .primaryColor( - getPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100)) + getPrefInt("colorPrimary", getCompatColor(R.color.md_indigo_800)) ) .accentColor( - getPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color)) + getPrefInt("colorAccent", getCompatColor(R.color.md_red_600)) ) .backgroundColor( getPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) ) .apply() } - ChapterProvider.upReadAloudSpan() +// ChapterProvider.upReadAloudSpan() } fun applyDayNight() { ReadBookConfig.upBg() applyTheme() - initNightTheme() + initNightMode() } - private fun initNightTheme() { - val targetMode = if (isNightTheme) { - AppCompatDelegate.MODE_NIGHT_YES - } else { - AppCompatDelegate.MODE_NIGHT_NO - } + private fun initNightMode() { + val targetMode = + if (AppConfig.isNightTheme) { + AppCompatDelegate.MODE_NIGHT_YES + } else { + AppCompatDelegate.MODE_NIGHT_NO + } AppCompatDelegate.setDefaultNightMode(targetMode) } diff --git a/app/src/main/java/io/legado/app/README.md b/app/src/main/java/io/legado/app/README.md new file mode 100644 index 000000000..b23d4196d --- /dev/null +++ b/app/src/main/java/io/legado/app/README.md @@ -0,0 +1,12 @@ +## 文件结构介绍 + +* base 基类 +* constant 常量 +* data 数据 +* help 帮助 +* lib 库 +* model 解析 +* receiver 广播侦听 +* service 服务 +* ui 界面 +* web web服务 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseActivity.kt b/app/src/main/java/io/legado/app/base/BaseActivity.kt index 2517cc616..e0d4713d7 100644 --- a/app/src/main/java/io/legado/app/base/BaseActivity.kt +++ b/app/src/main/java/io/legado/app/base/BaseActivity.kt @@ -53,8 +53,11 @@ abstract class BaseActivity( } override fun onMenuOpened(featureId: Int, menu: Menu?): Boolean { - menu?.applyOpenTint(this) - return super.onMenuOpened(featureId, menu) + menu?.let { + menu.applyOpenTint(this) + return super.onMenuOpened(featureId, menu) + } + return true } open fun onCompatCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt new file mode 100644 index 000000000..8bb119625 --- /dev/null +++ b/app/src/main/java/io/legado/app/base/BaseDialogFragment.kt @@ -0,0 +1,24 @@ +package io.legado.app.base + +import android.os.Bundle +import androidx.fragment.app.DialogFragment +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlin.coroutines.CoroutineContext + +abstract class BaseDialogFragment : DialogFragment(), CoroutineScope { + override val coroutineContext: CoroutineContext + get() = job + Dispatchers.Main + private lateinit var job: Job + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + job = Job() + } + + override fun onDestroy() { + super.onDestroy() + job.cancel() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/BaseFragment.kt b/app/src/main/java/io/legado/app/base/BaseFragment.kt index d870939d0..c4b712349 100644 --- a/app/src/main/java/io/legado/app/base/BaseFragment.kt +++ b/app/src/main/java/io/legado/app/base/BaseFragment.kt @@ -32,6 +32,14 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), return super.onCreateView(inflater, container, savedInstanceState) } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + onFragmentCreated(view, savedInstanceState) + observeLiveBus() + } + + abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?) + override fun onDestroy() { super.onDestroy() job.cancel() @@ -52,6 +60,8 @@ abstract class BaseFragment(layoutID: Int) : Fragment(layoutID), } } + open fun observeLiveBus() { + } open fun onCompatCreateOptionsMenu(menu: Menu) { } diff --git a/app/src/main/java/io/legado/app/base/BaseService.kt b/app/src/main/java/io/legado/app/base/BaseService.kt index c67acb95c..163ac09ec 100644 --- a/app/src/main/java/io/legado/app/base/BaseService.kt +++ b/app/src/main/java/io/legado/app/base/BaseService.kt @@ -3,17 +3,27 @@ package io.legado.app.base import android.app.Service import android.content.Intent import android.os.IBinder +import io.legado.app.help.coroutine.Coroutine import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.MainScope import kotlinx.coroutines.cancel +import kotlin.coroutines.CoroutineContext abstract class BaseService : Service(), CoroutineScope by MainScope() { + fun execute( + scope: CoroutineScope = this, + context: CoroutineContext = Dispatchers.IO, + block: suspend CoroutineScope.() -> T + ): Coroutine { + return Coroutine.async(scope, context) { block() } + } + override fun onBind(intent: Intent?): IBinder? { return null } - override fun onDestroy() { super.onDestroy() cancel() diff --git a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt index 679ba6f89..f864aa4e8 100644 --- a/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/CommonRecyclerAdapter.kt @@ -5,6 +5,7 @@ import android.util.SparseArray import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.RecyclerView import java.util.* @@ -15,13 +16,17 @@ import java.util.* * * 通用的adapter 可添加header,footer,以及不同类型item */ -abstract class CommonRecyclerAdapter(protected val context: Context) : RecyclerView.Adapter() { +abstract class CommonRecyclerAdapter(protected val context: Context) : + RecyclerView.Adapter() { - constructor(context: Context, vararg delegates: Pair>) : this(context) { + constructor(context: Context, vararg delegates: ItemViewDelegate) : this(context) { addItemViewDelegates(*delegates) } - constructor(context: Context, vararg delegates: ItemViewDelegate) : this(context) { + constructor( + context: Context, + vararg delegates: Pair> + ) : this(context) { addItemViewDelegates(*delegates) } @@ -122,7 +127,7 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec } } - fun setItems(items: List?, notify: Boolean = true) { + fun setItems(items: List?) { synchronized(lock) { if (this.items.isNotEmpty()) { this.items.clear() @@ -130,9 +135,19 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec if (items != null) { this.items.addAll(items) } - if (notify) { - notifyDataSetChanged() + notifyDataSetChanged() + } + } + + fun setItems(items: List?, diffResult: DiffUtil.DiffResult) { + synchronized(lock) { + if (this.items.isNotEmpty()) { + this.items.clear() } + if (items != null) { + this.items.addAll(items) + } + diffResult.dispatchUpdatesTo(this) } } @@ -236,7 +251,11 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec synchronized(lock) { val size = getActualItemCount() if (fromPosition in 0 until size && toPosition in 0 until size) { - notifyItemRangeChanged(fromPosition + getHeaderCount(), toPosition - fromPosition + 1, payloads) + notifyItemRangeChanged( + fromPosition + getHeaderCount(), + toPosition - fromPosition + 1, + payloads + ) } } } @@ -271,7 +290,13 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return footerItems?.size() ?: 0 } - fun getItem(position: Int): ITEM? = if (position in 0 until items.size) items[position] else null + fun getItem(position: Int): ITEM? = + if (position in 0 until items.size) items[position] else null + + fun getItemByLayoutPosition(position: Int): ITEM? { + val pos = position - getHeaderCount() + return if (pos in 0 until items.size) items[pos] else null + } fun getItems(): List = items @@ -294,7 +319,9 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec return when { isHeader(position) -> TYPE_HEADER_VIEW + position isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount() - else -> getItem(getActualPosition(position))?.let { getItemViewType(it, getActualPosition(position)) } ?: 0 + else -> getItem(getActualPosition(position))?.let { + getItemViewType(it, getActualPosition(position)) + } ?: 0 } } @@ -309,7 +336,16 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec } else -> { - val holder = ItemViewHolder(inflater.inflate(itemDelegates.getValue(viewType).layoutId, parent, false)) + val holder = ItemViewHolder( + inflater.inflate( + itemDelegates.getValue(viewType).layoutId, + parent, + false + ) + ) + + itemDelegates.getValue(viewType) + .registerListener(holder) if (itemClickListener != null) { holder.itemView.setOnClickListener { @@ -336,7 +372,11 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) { } - final override fun onBindViewHolder(holder: ItemViewHolder, position: Int, payloads: MutableList) { + final override fun onBindViewHolder( + holder: ItemViewHolder, + position: Int, + payloads: MutableList + ) { if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) { getItem(holder.layoutPosition - getHeaderCount())?.let { itemDelegates.getValue(getItemViewType(holder.layoutPosition)) @@ -385,10 +425,6 @@ abstract class CommonRecyclerAdapter(protected val context: Context) : Rec } private fun addAnimation(holder: ItemViewHolder) { - if (itemAnimation == null) { - itemAnimation = ItemAnimation.create().enabled(true) - } - itemAnimation?.let { if (it.itemAnimEnabled) { if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) { diff --git a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt index 2ac1090f2..866a53b6f 100644 --- a/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt +++ b/app/src/main/java/io/legado/app/base/adapter/ItemViewDelegate.kt @@ -9,6 +9,17 @@ import android.content.Context */ abstract class ItemViewDelegate(protected val context: Context, val layoutId: Int) { + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item, + * 或者使用registerListener(holder: ItemViewHolder, position: Int) + */ abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder) + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt index c65cfabf4..81c831ff4 100644 --- a/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt +++ b/app/src/main/java/io/legado/app/base/adapter/SimpleRecyclerAdapter.kt @@ -15,8 +15,20 @@ abstract class SimpleRecyclerAdapter(context: Context, private val layoutI this@SimpleRecyclerAdapter.convert(holder, item, payloads) } + override fun registerListener(holder: ItemViewHolder) { + this@SimpleRecyclerAdapter.registerListener(holder) + } }) } + /** + * 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题, + * 使用getItem(holder.layoutPosition)来获取item + */ abstract fun convert(holder: ItemViewHolder, item: ITEM, payloads: MutableList) + + /** + * 注册事件 + */ + abstract fun registerListener(holder: ItemViewHolder) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/Bus.kt b/app/src/main/java/io/legado/app/constant/EventBus.kt similarity index 88% rename from app/src/main/java/io/legado/app/constant/Bus.kt rename to app/src/main/java/io/legado/app/constant/EventBus.kt index eb1dd14dd..402827cb4 100644 --- a/app/src/main/java/io/legado/app/constant/Bus.kt +++ b/app/src/main/java/io/legado/app/constant/EventBus.kt @@ -1,11 +1,11 @@ package io.legado.app.constant -object Bus { +object EventBus { const val MEDIA_BUTTON = "mediaButton" const val RECREATE = "RECREATE" const val UP_BOOK = "sourceDebugLog" const val ALOUD_STATE = "aloud_state" - const val TTS_START = "ttsStart" + const val TTS_PROGRESS = "ttsStart" const val TTS_DS = "ttsDs" const val BATTERY_CHANGED = "batteryChanged" const val TIME_CHANGED = "timeChanged" @@ -21,4 +21,5 @@ object Bus { const val WEB_SERVICE_STOP = "webServiceStop" const val UP_DOWNLOAD = "upDownload" const val UP_TABS = "upTabs" + const val SAVE_CONTENT = "saveContent" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/Action.kt b/app/src/main/java/io/legado/app/constant/IntentAction.kt similarity index 91% rename from app/src/main/java/io/legado/app/constant/Action.kt rename to app/src/main/java/io/legado/app/constant/IntentAction.kt index a13a8cca3..f55e486ac 100644 --- a/app/src/main/java/io/legado/app/constant/Action.kt +++ b/app/src/main/java/io/legado/app/constant/IntentAction.kt @@ -1,6 +1,6 @@ package io.legado.app.constant -object Action { +object IntentAction { const val start = "start" const val play = "play" const val stop = "stop" @@ -17,4 +17,5 @@ object Action { const val next = "next" const val moveTo = "moveTo" const val init = "init" + const val remove = "remove" } \ 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 b869927e6..ce49d6023 100644 --- a/app/src/main/java/io/legado/app/constant/PreferKey.kt +++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt @@ -1,12 +1,17 @@ package io.legado.app.constant object PreferKey { - + const val versionCode = "versionCode" + const val themeMode = "themeMode" const val downloadPath = "downloadPath" const val hideStatusBar = "hideStatusBar" const val clickAllNext = "clickAllNext" const val hideNavigationBar = "hideNavigationBar" const val precisionSearch = "precisionSearch" + const val readAloudOnLine = "readAloudOnLine" + const val readAloudByPage = "readAloudByPage" + const val ttsSpeechRate = "ttsSpeechRate" + const val ttsSpeechPer = "ttsSpeechPer" const val prevKey = "prevKeyCode" const val nextKey = "nextKeyCode" const val showRss = "showRss" @@ -14,10 +19,22 @@ object PreferKey { const val recordLog = "recordLog" const val processText = "process_text" const val cleanCache = "cleanCache" - const val lastGroup = "lastGroup" const val saveTabPosition = "saveTabPosition" const val pageAnim = "pageAnim" const val readBookFont = "readBookFont" const val fontFolder = "fontFolder" const val backupPath = "backupUri" + const val threadCount = "threadCount" + const val keepLight = "keep_light" + const val webService = "webService" + const val webDavUrl = "web_dav_url" + const val webDavAccount = "web_dav_account" + const val webDavPassword = "web_dav_password" + const val changeSourceLoadToc = "changeSourceLoadToc" + const val chineseConverterType = "chineseConverterType" + const val launcherIcon = "launcherIcon" + const val textSelectAble = "selectText" + const val lastBackup = "lastBackup" + const val bodyIndent = "textIndent" + const val shareLayout = "shareLayout" } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/constant/Theme.kt b/app/src/main/java/io/legado/app/constant/Theme.kt index 40467a688..b8587997b 100644 --- a/app/src/main/java/io/legado/app/constant/Theme.kt +++ b/app/src/main/java/io/legado/app/constant/Theme.kt @@ -1,14 +1,13 @@ package io.legado.app.constant -import io.legado.app.App -import io.legado.app.utils.isNightTheme +import io.legado.app.help.AppConfig enum class Theme { Dark, Light, Auto; companion object { fun getTheme(): Theme { - return if (App.INSTANCE.isNightTheme) { + return if (AppConfig.isNightTheme) { Dark } else Light } 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 d65b43ada..e548f4584 100644 --- a/app/src/main/java/io/legado/app/data/AppDatabase.kt +++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt @@ -16,8 +16,9 @@ import kotlinx.coroutines.launch @Database( entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class, ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class, - RssSource::class, Bookmark::class, RssArticle::class, RssStar::class], - version = 5, + RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class, + RssStar::class, TxtTocRule::class], + version = 8, exportSchema = true ) abstract class AppDatabase : RoomDatabase() { @@ -50,4 +51,5 @@ abstract class AppDatabase : RoomDatabase() { abstract fun rssArticleDao(): RssArticleDao abstract fun rssStarDao(): RssStarDao abstract fun cookieDao(): CookieDao + abstract fun txtTocRule(): TxtTocRuleDao } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/README.md b/app/src/main/java/io/legado/app/data/README.md index 18061b12d..9b5f490b7 100644 --- a/app/src/main/java/io/legado/app/data/README.md +++ b/app/src/main/java/io/legado/app/data/README.md @@ -1 +1,17 @@ -## 存储数据用 \ No newline at end of file +## 存储数据用 +* dao 数据操作 +* entities 数据模型 +* \Book 书籍信息 +* \BookChapter 目录信息 +* \BookGroup 书籍分组 +* \Bookmark 书签 +* \BookSource 书源 +* \Cookie http cookie +* \ReplaceRule 替换规则 +* \RssArticle rss条目 +* \RssReadRecord rss阅读记录 +* \RssSource rss源 +* \RssStar rss收藏 +* \SearchBook 搜索结果 +* \SearchKeyword 搜索关键字 +* \TxtTocRule txt文件目录规则 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 9dda87c0e..38cc452f2 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 @@ -19,6 +19,9 @@ interface BookChapterDao { @Query("select * from chapters where bookUrl = :bookUrl") fun getChapterList(bookUrl: String): List + @Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end") + fun getChapterList(bookUrl: String, start: Int, end: Int): List + @Query("select * from chapters where bookUrl = :bookUrl and `index` = :index") fun getChapter(bookUrl: String, index: Int): BookChapter? 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 68de17a66..b0c07339c 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 @@ -17,15 +17,15 @@ interface BookDao { @Query("SELECT * FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") fun observeLocal(): LiveData> + @Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}' order by durChapterTime desc") + fun observeLocalUri(): LiveData> + @Query("SELECT * FROM books WHERE origin <> '${BookType.local}' and type = 0 order by durChapterTime desc") fun observeDownload(): LiveData> - @Query("SELECT * FROM books WHERE `group` = :group") + @Query("SELECT * FROM books WHERE (`group` & :group) > 0") fun observeByGroup(group: Int): LiveData> - @Query("SELECT bookUrl FROM books WHERE `group` = :group") - fun observeUrlsByGroup(group: Int): LiveData> - @Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'") fun liveDataSearch(key: String): LiveData> @@ -42,7 +42,7 @@ interface BookDao { val hasUpdateBooks: List @get:Query("SELECT * FROM books") - val allBooks: List + val all: List @get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1") val lastReadBook: Book? @@ -57,11 +57,14 @@ interface BookDao { fun insert(vararg book: Book) @Update - fun update(vararg books: Book) + fun update(vararg book: Book) - @Query("delete from books where bookUrl = :bookUrl") - fun delete(bookUrl: String) + @Delete + fun delete(vararg book: Book) @Query("update books set durChapterPos = :pos where bookUrl = :bookUrl") fun upProgress(bookUrl: String, pos: Int) + + @Query("update books set `group` = :newGroupId where `group` = :oldGroupId") + fun upGroup(oldGroupId: Int, newGroupId: Int) } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt index c48bd6e6b..c262d60e1 100644 --- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt @@ -13,11 +13,14 @@ interface BookGroupDao { @Query("SELECT * FROM book_groups ORDER BY `order`") fun liveDataAll(): LiveData> - @get:Query("SELECT MAX(groupId) FROM book_groups") - val maxId: Int + @get:Query("SELECT sum(groupId) FROM book_groups") + val idsSum: Int - @Query("SELECT * FROM book_groups ORDER BY `order`") - fun all(): List + @get:Query("SELECT MAX(`order`) FROM book_groups") + val maxOrder: Int + + @get:Query("SELECT * FROM book_groups ORDER BY `order`") + val all: List @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg bookGroup: BookGroup) 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 f76ed3d13..0981a94ea 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 @@ -14,10 +14,10 @@ interface BookSourceDao { @Query("select * from book_sources where bookSourceName like :searchKey or bookSourceGroup like :searchKey or bookSourceUrl like :searchKey order by customOrder asc") fun liveDataSearch(searchKey: String = ""): LiveData> - @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc") + @Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' order by customOrder asc") fun liveExplore(): LiveData> - @Query("select * from book_sources where enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc") + @Query("select * from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and (bookSourceGroup like :key or bookSourceName like :key) order by customOrder asc") fun liveExplore(key: String): LiveData> @Query("select bookSourceGroup from book_sources where bookSourceGroup is not null and bookSourceGroup <> ''") @@ -26,24 +26,12 @@ interface BookSourceDao { @Query("select bookSourceGroup from book_sources where enabled = 1 and bookSourceGroup is not null and bookSourceGroup <> ''") fun liveGroupEnabled(): LiveData> + @Query("select bookSourceGroup from book_sources where enabled = 1 and enabledExplore = 1 and exploreUrl is not null and exploreUrl <> '' and bookSourceGroup is not null and bookSourceGroup <> ''") + fun liveGroupExplore(): 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("update book_sources set enabled = 1 where bookSourceUrl = :sourceUrl") - fun enableSection(sourceUrl: String) - - @Query("update book_sources set enabled = 0 where bookSourceUrl = :sourceUrl") - fun disableSection(sourceUrl: String) - - @Query("update book_sources set enabledExplore = 1 where bookSourceUrl = :sourceUrl") - fun enableSectionExplore(sourceUrl: String) - - @Query("update book_sources set enabledExplore = 0 where bookSourceUrl = :sourceUrl") - fun disableSectionExplore(sourceUrl: String) - - @Query("delete from book_sources where bookSourceUrl = :sourceUrl") - fun delSection(sourceUrl: String) - @Query("select * from book_sources where enabledExplore = 1 order by customOrder asc") fun observeFind(): DataSource.Factory @@ -53,6 +41,9 @@ interface BookSourceDao { @Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'") fun getEnabledByGroup(group: String): List + @get:Query("select * from book_sources where bookUrlPattern is not null || bookUrlPattern <> ''") + val hasBookUrlPattern: List + @get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''") val noGroup: List @@ -75,7 +66,7 @@ interface BookSourceDao { fun update(vararg bookSource: BookSource) @Delete - fun delete(bookSource: BookSource) + fun delete(vararg bookSource: BookSource) @Query("delete from book_sources where bookSourceUrl = :key") fun delete(key: String) diff --git a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt index 5940272af..0abc76dc8 100644 --- a/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/ReplaceRuleDao.kt @@ -32,24 +32,15 @@ interface ReplaceRuleDao { @Query("SELECT * FROM replace_rules WHERE id in (:ids)") fun findByIds(vararg ids: Long): List - @Query("update replace_rules set isEnabled = 1 where id in (:ids)") - fun enableSection(vararg ids: Long) - - @Query("update replace_rules set isEnabled = 0 where id in (:ids)") - fun disableSection(vararg ids: Long) - - @Query("delete from replace_rules where id in (:ids)") - fun delSection(vararg ids: Long) - @Query( """SELECT * FROM replace_rules WHERE isEnabled = 1 - AND (scope LIKE '%' || :scope || '%' or scope = null or scope = '')""" + AND (scope LIKE '%' || :scope || '%' or scope is null or scope = '')""" ) fun findEnabledByScope(scope: String): List @Query( """SELECT * FROM replace_rules WHERE isEnabled = 1 - AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope = null or scope = '')""" + AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '')""" ) fun findEnabledByScope(name: String, origin: String): 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 247a5be6e..fbf3c3611 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 @@ -3,6 +3,7 @@ package io.legado.app.data.dao import androidx.lifecycle.LiveData import androidx.room.* import io.legado.app.data.entities.RssArticle +import io.legado.app.data.entities.RssReadRecord @Dao interface RssArticleDao { @@ -10,7 +11,11 @@ interface RssArticleDao { @Query("select * from rssArticles where origin = :origin and link = :link") fun get(origin: String, link: String): RssArticle? - @Query("select * from rssArticles where origin = :origin order by `order` desc") + @Query( + """select t1.link, 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""" + ) fun liveByOrigin(origin: String): LiveData> @Insert(onConflict = OnConflictStrategy.REPLACE) @@ -24,4 +29,9 @@ interface RssArticleDao { @Query("delete from rssArticles where origin = :origin") fun delete(origin: String) + + @Insert(onConflict = OnConflictStrategy.IGNORE) + fun insertRecord(vararg rssReadRecord: RssReadRecord) + + } \ No newline at end of file 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 df0bf605c..c1c1e85d9 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 @@ -31,21 +31,12 @@ interface RssSourceDao { @Query("select sourceGroup from rssSources where sourceGroup is not null and sourceGroup <> ''") fun liveGroup(): LiveData> - @Query("update rssSources set enabled = 1 where sourceUrl in (:sourceUrls)") - fun enableSection(vararg sourceUrls: String) - - @Query("update rssSources set enabled = 0 where sourceUrl in (:sourceUrls)") - fun disableSection(vararg sourceUrls: String) - @get:Query("select min(customOrder) from rssSources") val minOrder: Int @get:Query("select max(customOrder) from rssSources") val maxOrder: Int - @Query("delete from rssSources where sourceUrl in (:sourceUrls)") - fun delSection(vararg sourceUrls: String) - @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg rssSource: RssSource) diff --git a/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt index 87e527f75..faaf85b9f 100644 --- a/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt +++ b/app/src/main/java/io/legado/app/data/dao/RssStarDao.kt @@ -7,6 +7,9 @@ import io.legado.app.data.entities.RssStar @Dao interface RssStarDao { + @get:Query("select * from rssStars order by starTime desc") + val all: List + @Query("select * from rssStars where origin = :origin and link = :link") fun get(origin: String, link: String): RssStar? 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 7e623bfcc..779cfd1e2 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 @@ -19,16 +19,45 @@ interface SearchBookDao { @Query("select * from searchBooks where bookUrl = :bookUrl") fun getSearchBook(bookUrl: String): SearchBook? - @Query("select * from searchBooks where name = :name and author = :author order by originOrder limit 1") + @Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder limit 1") fun getFirstByNameAuthor(name: String, author: String): SearchBook? - @Query("select * from searchBooks where name = :name and author = :author order by originOrder") - fun getByNameAuthor(name: String, author: String): List - - @Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources where enabled = 1) order by originOrder") + @Query( + """ + select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author = :author + order by t2.customOrder + """ + ) fun getByNameAuthorEnable(name: String, author: String): List + @Query( + """ + select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author = :author and originName like '%'||:key||'%' + order by t2.customOrder + """ + ) + fun getChangeSourceSearch(name: String, author: String, key: String): List + + @Query( + """ + select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder + from searchBooks as t1 inner join book_sources as t2 + on t1.origin = t2.bookSourceUrl + where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> '' + order by t2.customOrder + """ + ) + fun getEnableHasCover(name: String, author: String): List + @Insert(onConflict = OnConflictStrategy.REPLACE) fun insert(vararg searchBook: SearchBook): List + @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/data/dao/TxtTocRuleDao.kt b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt new file mode 100644 index 000000000..5812b419c --- /dev/null +++ b/app/src/main/java/io/legado/app/data/dao/TxtTocRuleDao.kt @@ -0,0 +1,28 @@ +package io.legado.app.data.dao + +import androidx.lifecycle.LiveData +import androidx.room.* +import io.legado.app.data.entities.TxtTocRule + +@Dao +interface TxtTocRuleDao { + + @Query("select * from txtTocRules order by serialNumber") + fun observeAll(): LiveData> + + @get:Query("select * from txtTocRules order by serialNumber") + val all: List + + @get:Query("select * from txtTocRules where enable = 1 order by serialNumber") + val enabled: List + + @Insert(onConflict = OnConflictStrategy.REPLACE) + fun insert(vararg rule: TxtTocRule) + + @Update(onConflict = OnConflictStrategy.REPLACE) + fun update(vararg rule: TxtTocRule) + + @Delete + fun delete(vararg rule: TxtTocRule) + +} \ 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 d98f79603..d01919a94 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 @@ -10,6 +10,7 @@ import io.legado.app.utils.GSON import io.legado.app.utils.fromJsonObject import kotlinx.android.parcel.IgnoredOnParcel import kotlinx.android.parcel.Parcelize +import java.nio.charset.Charset import kotlin.math.max @Parcelize @@ -19,15 +20,15 @@ data class Book( var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径) var tocUrl: String = "", // 目录页Url (toc=table of Contents) var origin: String = BookType.local, // 书源URL(默认BookType.local) - var originName: String = "", //书源名称 - var name: String = "", // 书籍名称(书源获取) - var author: String = "", // 作者名称(书源获取) - override var kind: String? = null, // 分类信息(书源获取) + var originName: String = "", //书源名称 or 本地书籍文件名 + var name: String = "", // 书籍名称(书源获取) + var author: String = "", // 作者名称(书源获取) + override var kind: String? = null, // 分类信息(书源获取) var customTag: String? = null, // 分类信息(用户修改) var coverUrl: String? = null, // 封面Url(书源获取) var customCoverUrl: String? = null, // 封面Url(用户修改) - var intro: String? = null, // 简介内容(书源获取) - var customIntro: String? = null, // 简介内容(用户修改) + var intro: String? = null, // 简介内容(书源获取) + var customIntro: String? = null, // 简介内容(用户修改) var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍) var type: Int = 0, // @BookType var group: Int = 0, // 自定义分组索引号 @@ -48,6 +49,25 @@ data class Book( var variable: String? = null // 自定义书籍变量信息(用于书源规则检索书籍信息) ) : Parcelable, BaseBook { + fun isLocalBook(): Boolean { + return origin == BookType.local + } + + fun isTxt(): Boolean { + return isLocalBook() && originName.endsWith(".txt", true) + } + + override fun equals(other: Any?): Boolean { + if (other is Book) { + return other.bookUrl == bookUrl + } + return false + } + + override fun hashCode(): Int { + return bookUrl.hashCode() + } + @Ignore @IgnoredOnParcel override var variableMap: HashMap? = null @@ -66,6 +86,8 @@ data class Book( @IgnoredOnParcel override var tocHtml: String? = null + fun getRealAuthor() = author.replace("作者:", "") + fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0) fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl @@ -77,6 +99,10 @@ data class Book( variable = GSON.toJson(variableMap) } + fun fileCharset(): Charset { + return charset(charset ?: "UTF-8") + } + fun toSearchBook(): SearchBook { return SearchBook( name = name, diff --git a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt index ba5575cf3..f7a008870 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookChapter.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookChapter.kt @@ -33,7 +33,7 @@ data class BookChapter( var tag: String? = null, // var start: Long? = null, // 章节起始位置 var end: Long? = null, // 章节终止位置 - var variable: String? = null + var variable: String? = null //变量 ) : Parcelable { @Ignore @@ -52,5 +52,16 @@ data class BookChapter( variable = GSON.toJson(variableMap) } + override fun hashCode(): Int { + return url.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is BookChapter) { + return other.url == url + } + return false + } + } diff --git a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt index 5b3bb0992..9cd74b331 100644 --- a/app/src/main/java/io/legado/app/data/entities/BookGroup.kt +++ b/app/src/main/java/io/legado/app/data/entities/BookGroup.kt @@ -8,8 +8,8 @@ import kotlinx.android.parcel.Parcelize @Parcelize @Entity(tableName = "book_groups") data class BookGroup( - @PrimaryKey - var groupId: Int = 0, - var groupName: String, - var order: Int = 0 + @PrimaryKey + val groupId: Int = 0b1, + var groupName: String, + var order: Int = 0 ) : Parcelable \ No newline at end of file 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 39b569565..78852a4ec 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 @@ -47,6 +47,18 @@ data class BookSource( var ruleToc: String? = null, // 目录页规则 var ruleContent: String? = null // 正文页规则 ) : Parcelable, JsExtensions { + + override fun hashCode(): Int { + return bookSourceUrl.hashCode() + } + + override fun equals(other: Any?): Boolean { + if (other is BookSource) { + return other.bookSourceUrl == bookSourceUrl + } + return false + } + @Ignore @IgnoredOnParcel private var searchRuleV: SearchRule? = null @@ -126,6 +138,16 @@ data class BookSource( return contentRuleV!! } + fun addGroup(group: String) { + bookSourceGroup?.let { + if (!it.contains(group)) { + bookSourceGroup = "$it;$group" + } + } ?: let { + bookSourceGroup = group + } + } + fun getExploreKinds(): ArrayList? { val exploreKinds = arrayListOf() exploreUrl?.let { diff --git a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt index 269325e5d..8744ce2cb 100644 --- a/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt +++ b/app/src/main/java/io/legado/app/data/entities/ReplaceRule.kt @@ -24,4 +24,17 @@ data class ReplaceRule( var isRegex: Boolean = true, @ColumnInfo(name = "sortOrder") var order: Int = 0 -) : Parcelable \ No newline at end of file +) : Parcelable { + + + override fun equals(other: Any?): Boolean { + if (other is ReplaceRule) { + return other.id == id + } + return super.equals(other) + } + + override fun hashCode(): Int { + return id.hashCode() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt new file mode 100644 index 000000000..edbb4c15d --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/RssReadRecord.kt @@ -0,0 +1,7 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + +@Entity(tableName = "rssReadRecords") +data class RssReadRecord(@PrimaryKey val record: String, val read: Boolean = true) \ No newline at end of file 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 261611caf..999b12523 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 @@ -39,6 +39,17 @@ data class RssSource( var customOrder: Int = 0 ) : Parcelable, JsExtensions { + override fun equals(other: Any?): Boolean { + if (other is RssSource) { + return other.sourceUrl == sourceUrl + } + return false + } + + override fun hashCode(): Int { + return sourceUrl.hashCode() + } + @Throws(Exception::class) fun getHeaderMap(): Map { val headerMap = HashMap() diff --git a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt index de3466580..2baf259e4 100644 --- a/app/src/main/java/io/legado/app/data/entities/SearchBook.kt +++ b/app/src/main/java/io/legado/app/data/entities/SearchBook.kt @@ -90,6 +90,15 @@ data class SearchBook( origins?.add(origin) } + fun getDisplayLastChapterTitle(): String { + latestChapterTitle?.let { + if (it.isNotEmpty()) { + return it + } + } + return "无最新章节" + } + fun toBook(): Book { return Book( name = name, 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 new file mode 100644 index 000000000..cdfca7567 --- /dev/null +++ b/app/src/main/java/io/legado/app/data/entities/TxtTocRule.kt @@ -0,0 +1,14 @@ +package io.legado.app.data.entities + +import androidx.room.Entity +import androidx.room.PrimaryKey + + +@Entity(tableName = "txtTocRules") +data class TxtTocRule( + @PrimaryKey + var name: String = "", + var rule: String = "", + var serialNumber: Int = -1, + var enable: Boolean = true +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ActivityHelp.kt b/app/src/main/java/io/legado/app/help/ActivityHelp.kt index ca7c20f5f..6138c0c7e 100644 --- a/app/src/main/java/io/legado/app/help/ActivityHelp.kt +++ b/app/src/main/java/io/legado/app/help/ActivityHelp.kt @@ -19,7 +19,7 @@ object ActivityHelp { * 判断指定Activity是否存在 */ fun isExist(activityClass: Class<*>): Boolean { - for (item in activities) { + activities.forEach { item -> if (item.get()?.javaClass == activityClass) { return true } @@ -63,7 +63,7 @@ object ActivityHelp { * 关闭指定 activity */ fun finishActivity(vararg activities: Activity) { - for (activity in activities) { + activities.forEach { activity -> activity.finish() } } @@ -81,8 +81,8 @@ object ActivityHelp { } } } - for (activityWeakReference in waitFinish) { - activityWeakReference.get()?.finish() + waitFinish.forEach { + it.get()?.finish() } } diff --git a/app/src/main/java/io/legado/app/help/AdapterDataObserverProxy.kt b/app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt similarity index 86% rename from app/src/main/java/io/legado/app/help/AdapterDataObserverProxy.kt rename to app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt index 9ef120e15..24f0c0f2b 100644 --- a/app/src/main/java/io/legado/app/help/AdapterDataObserverProxy.kt +++ b/app/src/main/java/io/legado/app/help/AdapterDataObserverHeader.kt @@ -2,7 +2,10 @@ package io.legado.app.help import androidx.recyclerview.widget.RecyclerView -internal class AdapterDataObserverProxy(var adapterDataObserver: RecyclerView.AdapterDataObserver, var headerCount: Int) : RecyclerView.AdapterDataObserver() { +internal class AdapterDataObserverHeader( + var adapterDataObserver: RecyclerView.AdapterDataObserver, + var headerCount: Int +) : RecyclerView.AdapterDataObserver() { override fun onChanged() { adapterDataObserver.onChanged() } diff --git a/app/src/main/java/io/legado/app/help/AppConfig.kt b/app/src/main/java/io/legado/app/help/AppConfig.kt new file mode 100644 index 000000000..1f4302fdc --- /dev/null +++ b/app/src/main/java/io/legado/app/help/AppConfig.kt @@ -0,0 +1,89 @@ +package io.legado.app.help + +import android.content.Context +import io.legado.app.App +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.utils.* + +object AppConfig { + + fun isNightTheme(context: Context): Boolean { + return when (context.getPrefString(PreferKey.themeMode, "0")) { + "1" -> false + "2" -> true + else -> context.sysIsDarkMode() + } + } + + var isNightTheme: Boolean + get() = isNightTheme(App.INSTANCE) + set(value) { + if (value) { + App.INSTANCE.putPrefString(PreferKey.themeMode, "2") + } else { + App.INSTANCE.putPrefString(PreferKey.themeMode, "1") + } + } + + var isTransparentStatusBar: Boolean + get() = App.INSTANCE.getPrefBoolean("transparentStatusBar") + set(value) { + App.INSTANCE.putPrefBoolean("transparentStatusBar", value) + } + + var backupPath: String? + get() = App.INSTANCE.getPrefString(PreferKey.backupPath) + set(value) { + if (value.isNullOrEmpty()) { + App.INSTANCE.removePref(PreferKey.backupPath) + } else { + App.INSTANCE.putPrefString(PreferKey.backupPath, value) + } + } + + var isShowRSS: Boolean + get() = App.INSTANCE.getPrefBoolean(PreferKey.showRss, true) + set(value) { + App.INSTANCE.putPrefBoolean(PreferKey.showRss, value) + } + + val autoRefreshBook: Boolean + get() = App.INSTANCE.getPrefBoolean(App.INSTANCE.getString(R.string.pk_auto_refresh)) + + var threadCount: Int + get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.threadCount, value) + } + + var importBookPath: String? + get() = App.INSTANCE.getPrefString("importBookPath") + set(value) { + if (value == null) { + App.INSTANCE.removePref("importBookPath") + } else { + App.INSTANCE.putPrefString("importBookPath", value) + } + } + + var ttsSpeechRate: Int + get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value) + } + + val ttsSpeechPer: String + get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0" + + val isEInkMode: Boolean + get() = App.INSTANCE.getPrefBoolean("isEInkMode") + + val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) + + var chineseConverterType: Int + get() = App.INSTANCE.getPrefInt(PreferKey.chineseConverterType) + set(value) { + App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value) + } +} 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 ff5ee0b7d..a4227619b 100644 --- a/app/src/main/java/io/legado/app/help/BookHelp.kt +++ b/app/src/main/java/io/legado/app/help/BookHelp.kt @@ -1,101 +1,176 @@ package io.legado.app.help +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import com.github.houbb.opencc4j.core.impl.ZhConvertBootstrap import io.legado.app.App +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey 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.utils.MD5Utils -import io.legado.app.utils.getPrefInt -import io.legado.app.utils.getPrefString +import io.legado.app.model.localBook.AnalyzeTxtFile +import io.legado.app.utils.* import org.apache.commons.text.similarity.JaccardSimilarity import java.io.File import kotlin.math.min object BookHelp { - - private var downloadPath: String = - App.INSTANCE.getPrefString(PreferKey.downloadPath) + private const val cacheFolderName = "book_cache" + val downloadPath: String + get() = App.INSTANCE.getPrefString(PreferKey.downloadPath) ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath ?: App.INSTANCE.cacheDir.absolutePath - fun upDownloadPath() { - downloadPath = - App.INSTANCE.getPrefString(PreferKey.downloadPath) - ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath - ?: App.INSTANCE.cacheDir.absolutePath + private val downloadUri get() = Uri.parse(downloadPath) + + private fun bookFolderName(book: Book): String { + return formatFolderName(book.name) + MD5Utils.md5Encode16(book.bookUrl) } - private fun getBookCachePath(): String { - return "$downloadPath${File.separator}book_cache" + fun formatChapterName(bookChapter: BookChapter): String { + return String.format( + "%05d-%s.nb", + bookChapter.index, + MD5Utils.md5Encode16(bookChapter.title) + ) } fun clearCache() { - FileHelp.deleteFile(getBookCachePath()) - FileHelp.getFolder(getBookCachePath()) + if (downloadPath.isContentPath()) { + DocumentFile.fromTreeUri(App.INSTANCE, downloadUri) + ?.findFile(cacheFolderName) + ?.delete() + } else { + FileUtils.deleteFile( + FileUtils.getPath( + File(downloadPath), + subDirs = *arrayOf(cacheFolderName) + ) + ) + } } @Synchronized fun saveContent(book: Book, bookChapter: BookChapter, content: String) { if (content.isEmpty()) return - FileHelp.getFolder(getBookFolder(book)).listFiles()?.forEach { - if (it.name.startsWith(String.format("%05d", bookChapter.index))) { - it.delete() - return@forEach + 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) } - val filePath = getChapterPath(book, bookChapter) - val file = FileHelp.getFile(filePath) - file.writeText(content) + postEvent(EventBus.SAVE_CONTENT, bookChapter) } - fun getChapterCount(book: Book): Int { - return FileHelp.getFolder(getBookFolder(book)).list()?.size ?: 0 + 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) + } + } + return fileNameList } fun hasContent(book: Book, bookChapter: BookChapter): Boolean { - val filePath = getChapterPath(book, bookChapter) - runCatching { - val file = File(filePath) - if (file.exists()) { + 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 false } fun getContent(book: Book, bookChapter: BookChapter): String? { - val filePath = getChapterPath(book, bookChapter) - runCatching { - val file = File(filePath) - if (file.exists()) { - return file.readText() + 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() + } } } return null } fun delContent(book: Book, bookChapter: BookChapter) { - val filePath = getChapterPath(book, bookChapter) - kotlin.runCatching { - val file = File(filePath) - if (file.exists()) { - file.delete() + 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() } } } - private fun getBookFolder(book: Book): String { - val bookFolder = formatFolderName(book.name + MD5Utils.md5Encode16(book.bookUrl)) - return "${getBookCachePath()}${File.separator}$bookFolder" - } - - private fun getChapterPath(book: Book, bookChapter: BookChapter): String { - val chapterFile = - String.format("%05d-%s", bookChapter.index, MD5Utils.md5Encode(bookChapter.title)) - return "${getBookFolder(book)}${File.separator}$chapterFile.nb" - } - private fun formatFolderName(folderName: String): String { return folderName.replace("[\\\\/:*?\"<>|.]".toRegex(), "") } @@ -124,16 +199,16 @@ object BookHelp { } var newIndex = 0 - val jaccardSimilarity = JaccardSimilarity() + val jSimilarity = JaccardSimilarity() var similarity = if (chapters.size > index) { - jaccardSimilarity.apply(title, chapters[index].title) + jSimilarity.apply(title, chapters[index].title) } else 0.0 if (similarity == 1.0) { return index } else { for (i in 1..50) { if (index - i in chapters.indices) { - jaccardSimilarity.apply(title, chapters[index - i].title).let { + jSimilarity.apply(title, chapters[index - i].title).let { if (it > similarity) { similarity = it newIndex = index - i @@ -144,7 +219,7 @@ object BookHelp { } } if (index + i in chapters.indices) { - jaccardSimilarity.apply(title, chapters[index + i].title).let { + jSimilarity.apply(title, chapters[index + i].title).let { if (it > similarity) { similarity = it newIndex = index + i @@ -159,9 +234,11 @@ object BookHelp { return newIndex } - var bookName: String? = null - var bookOrigin: String? = null - var replaceRules: List = arrayListOf() + private var bookName: String? = null + private var bookOrigin: String? = null + private var replaceRules: List = arrayListOf() + val bodyIndent + get() = " ".repeat(App.INSTANCE.getPrefInt(PreferKey.bodyIndent, 2)) fun disposeContent( title: String, @@ -170,7 +247,6 @@ object BookHelp { content: String, enableReplace: Boolean ): String { - var c = content synchronized(this) { if (enableReplace && (bookName != name || bookOrigin != origin)) { replaceRules = if (origin.isNullOrEmpty()) { @@ -180,9 +256,7 @@ object BookHelp { } } } - if (!content.substringBefore("\n").contains(title)) { - c = title + "\n" + c - } + var c = content for (item in replaceRules) { item.pattern.let { if (it.isNotEmpty()) { @@ -194,7 +268,11 @@ object BookHelp { } } } - val indent = App.INSTANCE.getPrefInt("textIndent", 2) - return c.replace("\\s*\\n+\\s*".toRegex(), "\n" + " ".repeat(indent)) + c = "$title\n$c" + when (AppConfig.chineseConverterType) { + 1 -> c = ZhConvertBootstrap.newInstance().toSimple(c) + 2 -> c = ZhConvertBootstrap.newInstance().toTraditional(c) + } + return c.replace("\\s*\\n+\\s*".toRegex(), "\n$bodyIndent") } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/CrashHandler.kt b/app/src/main/java/io/legado/app/help/CrashHandler.kt index 9a5c7131a..b59609e18 100644 --- a/app/src/main/java/io/legado/app/help/CrashHandler.kt +++ b/app/src/main/java/io/legado/app/help/CrashHandler.kt @@ -9,12 +9,12 @@ import android.os.Looper import android.util.Log import android.widget.Toast import io.legado.app.service.TTSReadAloudService -import java.io.File -import java.io.FileOutputStream +import io.legado.app.utils.FileUtils import java.io.PrintWriter import java.io.StringWriter import java.text.SimpleDateFormat import java.util.* +import java.util.concurrent.TimeUnit /** * 异常管理类 @@ -141,14 +141,15 @@ class CrashHandler : Thread.UncaughtExceptionHandler { val timestamp = System.currentTimeMillis() val time = format.format(Date()) val fileName = "crash-$time-$timestamp.log" - val path = mContext?.externalCacheDir?.toString() + "/crash/" - val dir = File(path) - if (!dir.exists()) { - dir.mkdirs() + mContext?.externalCacheDir?.let { rootFile -> + FileUtils.getDirFile(rootFile, "crash").listFiles()?.forEach { + if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) { + it.delete() + } + } + FileUtils.createFileIfNotExist(rootFile, fileName, "crash") + .writeText(sb.toString()) } - val fos = FileOutputStream(path + fileName) - fos.write(sb.toString().toByteArray()) - fos.close() } } diff --git a/app/src/main/java/io/legado/app/help/FileHelp.kt b/app/src/main/java/io/legado/app/help/FileHelp.kt deleted file mode 100644 index 5a7678563..000000000 --- a/app/src/main/java/io/legado/app/help/FileHelp.kt +++ /dev/null @@ -1,58 +0,0 @@ -package io.legado.app.help - -import io.legado.app.App -import java.io.File -import java.io.IOException - -object FileHelp { - - - //获取文件夹 - fun getFolder(filePath: String): File { - val file = File(filePath) - //如果文件夹不存在,就创建它 - if (!file.exists()) { - file.mkdirs() - } - return file - } - - //获取文件 - @Synchronized - fun getFile(filePath: String): File { - val file = File(filePath) - try { - if (!file.exists()) { - //创建父类文件夹 - getFolder(file.parent) - //创建文件 - file.createNewFile() - } - } catch (e: IOException) { - e.printStackTrace() - } - return file - } - - fun getCachePath(): String { - return App.INSTANCE.externalCacheDir?.absolutePath - ?: App.INSTANCE.cacheDir.absolutePath - } - - //递归删除文件夹下的数据 - @Synchronized - fun deleteFile(filePath: String) { - val file = File(filePath) - if (!file.exists()) return - - if (file.isDirectory) { - val files = file.listFiles() - files?.forEach { subFile -> - val path = subFile.path - deleteFile(path) - } - } - //删除文件 - file.delete() - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt b/app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt new file mode 100644 index 000000000..a35f0cd8c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/FirstTopListUpCallback.kt @@ -0,0 +1,34 @@ +package io.legado.app.help + +import androidx.recyclerview.widget.ListUpdateCallback +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.base.adapter.ItemViewHolder + +class FirstTopListUpCallback : ListUpdateCallback { + var firstInsert = -1 + lateinit var adapter: RecyclerView.Adapter + + override fun onChanged(position: Int, count: Int, payload: Any?) { + adapter.notifyItemRangeChanged(position, count, payload) + } + + override fun onMoved(fromPosition: Int, toPosition: Int) { + if (toPosition == 0) { + firstInsert = 0 + } + adapter.notifyItemMoved(fromPosition, toPosition) + } + + override fun onInserted(position: Int, count: Int) { + if (firstInsert == -1 || firstInsert > position) { + firstInsert = position + } + adapter.notifyItemRangeInserted(position, count) + } + + override fun onRemoved(position: Int, count: Int) { + adapter.notifyItemRangeRemoved(position, count) + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/ImageLoader.kt b/app/src/main/java/io/legado/app/help/ImageLoader.kt index 26a9063d6..3d4272e9e 100644 --- a/app/src/main/java/io/legado/app/help/ImageLoader.kt +++ b/app/src/main/java/io/legado/app/help/ImageLoader.kt @@ -12,13 +12,15 @@ import java.io.File object ImageLoader { fun load(context: Context, path: String?): RequestBuilder { - if (path?.startsWith("http", true) == true) { - return Glide.with(context).load(path) + return when { + path.isNullOrEmpty() -> Glide.with(context).load(path) + path.startsWith("http", true) -> Glide.with(context).load(path) + else -> try { + Glide.with(context).load(File(path)) + } catch (e: Exception) { + Glide.with(context).load(path) + } } - kotlin.runCatching { - return Glide.with(context).load(File(path)) - } - return Glide.with(context).load(path) } fun load(context: Context, @DrawableRes resId: Int?): RequestBuilder { diff --git a/app/src/main/java/io/legado/app/help/ItemTouchCallback.kt b/app/src/main/java/io/legado/app/help/ItemTouchCallback.kt index 9cf1fccf5..99ba74ef1 100644 --- a/app/src/main/java/io/legado/app/help/ItemTouchCallback.kt +++ b/app/src/main/java/io/legado/app/help/ItemTouchCallback.kt @@ -88,16 +88,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { srcViewHolder: RecyclerView.ViewHolder, targetViewHolder: RecyclerView.ViewHolder ): Boolean { - onItemTouchCallbackListener?.let { - return it.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition) - } - return false + return onItemTouchCallbackListener + ?.onMove(srcViewHolder.adapterPosition, targetViewHolder.adapterPosition) + ?: false } override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - onItemTouchCallbackListener?.let { - return it.onSwiped(viewHolder.adapterPosition) - } + onItemTouchCallbackListener?.onSwiped(viewHolder.adapterPosition) } override fun onSelectedChanged(viewHolder: RecyclerView.ViewHolder?, actionState: Int) { @@ -107,13 +104,21 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { viewPager?.requestDisallowInterceptTouchEvent(swiping) } + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + onItemTouchCallbackListener?.clearView(recyclerView, viewHolder) + } + interface OnItemTouchCallbackListener { + /** * 当某个Item被滑动删除的时候 * * @param adapterPosition item的position */ - fun onSwiped(adapterPosition: Int) + fun onSwiped(adapterPosition: Int) { + + } /** * 当两个Item位置互换的时候被回调 @@ -122,6 +127,13 @@ class ItemTouchCallback : ItemTouchHelper.Callback() { * @param targetPosition 目的地的Item的position * @return 开发者处理了操作应该返回true,开发者没有处理就返回false */ - fun onMove(srcPosition: Int, targetPosition: Int): Boolean + fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + return true + } + + fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + + } + } } diff --git a/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt new file mode 100644 index 000000000..76bcd6116 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/LauncherIconHelp.kt @@ -0,0 +1,65 @@ +package io.legado.app.help + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.os.Build +import io.legado.app.App +import io.legado.app.R +import io.legado.app.ui.welcome.Launcher1 +import io.legado.app.ui.welcome.Launcher2 +import io.legado.app.ui.welcome.Launcher3 +import io.legado.app.ui.welcome.WelcomeActivity +import org.jetbrains.anko.toast + +/** + * Created by GKF on 2018/2/27. + * 更换图标 + */ +object LauncherIconHelp { + private val packageManager: PackageManager = App.INSTANCE.packageManager + private val componentNames = arrayListOf( + ComponentName(App.INSTANCE, Launcher1::class.java.name), + ComponentName(App.INSTANCE, Launcher2::class.java.name), + ComponentName(App.INSTANCE, Launcher3::class.java.name) + ) + + fun changeIcon(icon: String?) { + if (icon.isNullOrEmpty()) return + if (Build.VERSION.SDK_INT < 26) { + App.INSTANCE.toast(R.string.chage_icon_error) + return + } + var hasEnabled = false + componentNames.forEach { + if (icon.equals(it.className.substringAfterLast("."), true)) { + hasEnabled = true + //启用 + packageManager.setComponentEnabledSetting( + it, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } else { + //禁用 + packageManager.setComponentEnabledSetting( + it, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } + } + if (hasEnabled) { + packageManager.setComponentEnabledSetting( + ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, + PackageManager.DONT_KILL_APP + ) + } else { + packageManager.setComponentEnabledSetting( + ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, + PackageManager.DONT_KILL_APP + ) + } + } +} \ 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 328433c3c..75777d51b 100644 --- a/app/src/main/java/io/legado/app/help/ReadBookConfig.kt +++ b/app/src/main/java/io/legado/app/help/ReadBookConfig.kt @@ -6,14 +6,10 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import io.legado.app.App import io.legado.app.R +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.ui.book.read.page.ChapterProvider import io.legado.app.utils.* -import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch -import java.io.BufferedWriter import java.io.File -import java.io.FileWriter -import java.io.IOException /** * 阅读界面配置 @@ -23,6 +19,12 @@ object ReadBookConfig { private val configFilePath = App.INSTANCE.filesDir.absolutePath + File.separator + readConfigFileName val configList: ArrayList = arrayListOf() + private val defaultConfigs by lazy { + val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes()) + GSON.fromJsonArray(json)!! + } + val durConfig + get() = getConfig(styleSelect) var styleSelect get() = App.INSTANCE.getPrefInt("readStyleSelect") @@ -34,28 +36,30 @@ object ReadBookConfig { } @Synchronized - fun getConfig(index: Int = styleSelect): Config { + fun getConfig(index: Int): Config { if (configList.size < 5) { - reset() + resetAll() } return configList[index] } fun upConfig() { - val configFile = File(configFilePath) - val json = if (configFile.exists()) { - configFile.readText() - } else { - String(App.INSTANCE.assets.open(readConfigFileName).readBytes()) + (getConfigs() ?: defaultConfigs).let { + configList.clear() + configList.addAll(it) } - try { - GSON.fromJsonArray(json)?.let { - configList.clear() - configList.addAll(it) - } ?: reset() - } catch (e: Exception) { - reset() + } + + private fun getConfigs(): List? { + val configFile = File(configFilePath) + if (configFile.exists()) { + try { + val json = configFile.readText() + return GSON.fromJsonArray(json) + } catch (e: Exception) { + } } + return null } fun upBg() { @@ -63,55 +67,63 @@ object ReadBookConfig { val dm = resources.displayMetrics val width = dm.widthPixels val height = dm.heightPixels - bg = getConfig().bgDrawable(width, height) + bg = durConfig.bgDrawable(width, height) } fun save() { - GlobalScope.launch(IO) { + Coroutine.async { val json = GSON.toJson(configList) - val configFile = File(configFilePath) - //获取流并存储 - try { - BufferedWriter(FileWriter(configFile)).use { writer -> - writer.write(json) - writer.flush() - } - } catch (e: IOException) { - e.printStackTrace() - } + FileUtils.createFileIfNotExist(configFilePath).writeText(json) } } - private fun reset() { - val json = String(App.INSTANCE.assets.open(readConfigFileName).readBytes()) - GSON.fromJsonArray(json)?.let { + fun resetDur() { + defaultConfigs[styleSelect].let { + durConfig.setBg(it.bgType(), it.bgStr()) + durConfig.setTextColor(it.textColor()) + upBg() + save() + } + } + + private fun resetAll() { + defaultConfigs.let { configList.clear() configList.addAll(it) + save() } - save() } data class Config( - var bgStr: String = "#EEEEEE", - var bgStrNight: String = "#000000", - var bgType: Int = 0, - var bgTypeNight: Int = 0, - var darkStatusIcon: Boolean = true, - var darkStatusIconNight: Boolean = false, - var letterSpacing: Float = 1f, - var lineSpacingExtra: Int = 12, - var lineSpacingMultiplier: Float = 1.2f, - var paddingBottom: Int = 0, + var bgStr: String = "#EEEEEE",//白天背景 + var bgStrNight: String = "#000000",//夜间背景 + var bgType: Int = 0,//白天背景类型 + var bgTypeNight: Int = 0,//夜间背景类型 + var darkStatusIcon: Boolean = true,//白天是否暗色状态栏 + var darkStatusIconNight: Boolean = false,//晚上是否暗色状态栏 + var textColor: String = "#3E3D3B",//白天文字颜色 + var textColorNight: String = "#adadad",//夜间文字颜色 + var textBold: Boolean = false,//是否粗体字 + var textSize: Int = 15,//文字大小 + var letterSpacing: Float = 1f,//字间距 + var lineSpacingExtra: Int = 12,//行间距 + var paragraphSpacing: Int = 12,//段距 + var titleCenter: Boolean = true,//标题居中 + var paddingBottom: Int = 6, var paddingLeft: Int = 16, var paddingRight: Int = 16, - var paddingTop: Int = 0, - var textBold: Boolean = false, - var textColor: String = "#3E3D3B", - var textColorNight: String = "#adadad", - var textSize: Int = 15 + var paddingTop: Int = 6, + var headerPaddingBottom: Int = 0, + var headerPaddingLeft: Int = 16, + var headerPaddingRight: Int = 16, + var headerPaddingTop: Int = 0, + var footerPaddingBottom: Int = 6, + var footerPaddingLeft: Int = 16, + var footerPaddingRight: Int = 16, + var footerPaddingTop: Int = 6 ) { fun setBg(bgType: Int, bg: String) { - if (App.INSTANCE.isNightTheme) { + if (AppConfig.isNightTheme) { bgTypeNight = bgType bgStrNight = bg } else { @@ -121,15 +133,16 @@ object ReadBookConfig { } fun setTextColor(color: Int) { - if (App.INSTANCE.isNightTheme) { + if (AppConfig.isNightTheme) { textColorNight = "#${color.hexString}" } else { textColor = "#${color.hexString}" } + ChapterProvider.upStyle(this) } fun setStatusIconDark(isDark: Boolean) { - if (App.INSTANCE.isNightTheme) { + if (AppConfig.isNightTheme) { darkStatusIconNight = isDark } else { darkStatusIcon = isDark @@ -137,7 +150,7 @@ object ReadBookConfig { } fun statusIconDark(): Boolean { - return if (App.INSTANCE.isNightTheme) { + return if (AppConfig.isNightTheme) { darkStatusIconNight } else { darkStatusIcon @@ -145,17 +158,17 @@ object ReadBookConfig { } fun textColor(): Int { - return if (App.INSTANCE.isNightTheme) Color.parseColor(textColorNight) + return if (AppConfig.isNightTheme) Color.parseColor(textColorNight) else Color.parseColor(textColor) } fun bgStr(): String { - return if (App.INSTANCE.isNightTheme) bgStrNight + return if (AppConfig.isNightTheme) bgStrNight else bgStr } fun bgType(): Int { - return if (App.INSTANCE.isNightTheme) bgTypeNight + return if (AppConfig.isNightTheme) bgTypeNight else bgType } diff --git a/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt index 3f76acbe6..1eb1102ba 100644 --- a/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt +++ b/app/src/main/java/io/legado/app/help/coroutine/CompositeCoroutine.kt @@ -76,7 +76,7 @@ class CompositeCoroutine : CoroutineContainer { resources = null } - set?.forEachIndexed { index, coroutine -> + set?.forEachIndexed { _, coroutine -> coroutine.cancel() } } diff --git a/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt b/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt deleted file mode 100644 index 70391773a..000000000 --- a/app/src/main/java/io/legado/app/help/http/CoroutinesCallAdapterFactory.kt +++ /dev/null @@ -1,102 +0,0 @@ -package io.legado.app.help.http - -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred -import retrofit2.* -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type - -class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() { - companion object { - fun create(): CoroutinesCallAdapterFactory { - return CoroutinesCallAdapterFactory() - } - } - - override fun get( - returnType: Type, - annotations: Array, - retrofit: Retrofit - ): CallAdapter<*, *>? { - if (Deferred::class.java != getRawType(returnType)) { - return null - } - check(returnType is ParameterizedType) { "Deferred return type must be parameterized as Deferred or Deferred" } - val responseType = getParameterUpperBound(0, returnType) - - val rawDeferredType = getRawType(responseType) - return if (rawDeferredType == Response::class.java) { - check(responseType is ParameterizedType) { "Response must be parameterized as Response or Response" } - ResponseCallAdapter( - getParameterUpperBound( - 0, - responseType - ) - ) - } else { - BodyCallAdapter(responseType) - } - } - - private class BodyCallAdapter( - private val responseType: Type - ) : CallAdapter> { - - override fun responseType() = responseType - - override fun adapt(call: Call): Deferred { - val deferred = CompletableDeferred() - - deferred.invokeOnCompletion { - if (deferred.isCancelled) { - call.cancel() - } - } - - call.enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - deferred.completeExceptionally(t) - } - - override fun onResponse(call: Call, response: Response) { - if (response.isSuccessful) { - deferred.complete(response.body()!!) - } else { - deferred.completeExceptionally(HttpException(response)) - } - } - }) - - return deferred - } - } - - private class ResponseCallAdapter( - private val responseType: Type - ) : CallAdapter>> { - - override fun responseType() = responseType - - override fun adapt(call: Call): Deferred> { - val deferred = CompletableDeferred>() - - deferred.invokeOnCompletion { - if (deferred.isCancelled) { - call.cancel() - } - } - - call.enqueue(object : Callback { - override fun onFailure(call: Call, t: Throwable) { - deferred.completeExceptionally(t) - } - - override fun onResponse(call: Call, response: Response) { - deferred.complete(response) - } - }) - - return deferred - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt b/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt index 9e0b5ff1f..0cad990f0 100644 --- a/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt +++ b/app/src/main/java/io/legado/app/help/http/EncodeConverter.kt @@ -1,5 +1,6 @@ package io.legado.app.help.http +import io.legado.app.utils.EncodingDetect import io.legado.app.utils.UTF8BOMFighter import okhttp3.ResponseBody import retrofit2.Converter @@ -16,20 +17,22 @@ class EncodeConverter(private val encode: String? = null) : Converter.Factory() ): Converter? { return Converter { value -> val responseBytes = UTF8BOMFighter.removeUTF8BOM(value.bytes()) - encode?.let { return@Converter String(responseBytes, Charset.forName(encode)) } + var charsetName: String? = encode - var charsetName: String? = null - val mediaType = value.contentType() - //根据http头判断 - if (mediaType != null) { - val charset = mediaType.charset() - charsetName = charset?.displayName() + charsetName?.let { + try { + return@Converter String(responseBytes, Charset.forName(charsetName)) + } catch (e: Exception) { + } } - if (charsetName == null) { - charsetName = EncodingDetect.getHtmlEncode(responseBytes) + //根据http头判断 + value.contentType()?.charset()?.let { + return@Converter String(responseBytes, it) } + //根据内容判断 + charsetName = EncodingDetect.getHtmlEncode(responseBytes) String(responseBytes, Charset.forName(charsetName)) } } 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 da70b17a7..a37e053fb 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 @@ -1,5 +1,7 @@ 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 retrofit2.Retrofit @@ -7,6 +9,7 @@ import java.util.* import java.util.concurrent.TimeUnit import kotlin.coroutines.resume +@Suppress("unused") object HttpHelper { val client: OkHttpClient by lazy { @@ -35,11 +38,26 @@ object HttpHelper { builder.build() } - inline fun getApiService(baseUrl: String): T { - return getRetrofit(baseUrl).create(T::class.java) + fun simpleGet(url: String, encode: String? = null): String? { + NetworkUtils.getBaseUrl(url)?.let { baseUrl -> + val response = getApiService(baseUrl, encode) + .get(url, mapOf()) + .execute() + return response.body() + } + return null + } + + suspend fun simpleGetAsync(url: String, encode: String? = null): String? { + NetworkUtils.getBaseUrl(url)?.let { baseUrl -> + val response = getApiService(baseUrl, encode) + .getAsync(url, mapOf()) + return response.body() + } + return null } - inline fun getApiService(baseUrl: String, encode: String): T { + inline fun getApiService(baseUrl: String, encode: String? = null): T { return getRetrofit(baseUrl, encode).create(T::class.java) } @@ -47,8 +65,6 @@ object HttpHelper { return Retrofit.Builder().baseUrl(baseUrl) //增加返回值为字符串的支持(以实体类返回) .addConverterFactory(EncodeConverter(encode)) - //增加返回值为Observable的支持 - .addCallAdapterFactory(CoroutinesCallAdapterFactory.create()) .client(client) .build() } @@ -56,8 +72,6 @@ object HttpHelper { fun getByteRetrofit(baseUrl: String): Retrofit { return Retrofit.Builder().baseUrl(baseUrl) .addConverterFactory(ByteConverter()) - //增加返回值为Observable的支持 - .addCallAdapterFactory(CoroutinesCallAdapterFactory.create()) .client(client) .build() } diff --git a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt index ef70ac1b5..800304b62 100644 --- a/app/src/main/java/io/legado/app/help/http/SSLHelper.kt +++ b/app/src/main/java/io/legado/app/help/http/SSLHelper.kt @@ -14,11 +14,9 @@ import javax.net.ssl.* object SSLHelper { - val sslSocketFactory: SSLParams? - get() = getSslSocketFactoryBase(null, null, null) - /** - * 为了解决客户端不信任服务器数字证书的问题,网络上大部分的解决方案都是让客户端不对证书做任何检查, + * 为了解决客户端不信任服务器数字证书的问题, + * 网络上大部分的解决方案都是让客户端不对证书做任何检查, * 这是一种有很大安全漏洞的办法 */ val unsafeTrustManager: X509TrustManager = object : X509TrustManager { @@ -141,7 +139,7 @@ object SSLHelper { val keyStore = KeyStore.getInstance(KeyStore.getDefaultType()) keyStore.load(null) for ((index, certStream) in certificates.withIndex()) { - val certificateAlias = Integer.toString(index) + val certificateAlias = index.toString() // 证书工厂根据证书文件的流生成证书 cert val cert = certificateFactory.generateCertificate(certStream) // 将 cert 作为可信证书放入到keyStore中 diff --git a/app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt similarity index 66% rename from app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt rename to app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt index e4180fa59..315dc881f 100644 --- a/app/src/main/java/io/legado/app/data/api/IHttpGetApi.kt +++ b/app/src/main/java/io/legado/app/help/http/api/HttpGetApi.kt @@ -1,6 +1,5 @@ -package io.legado.app.data.api +package io.legado.app.help.http.api -import kotlinx.coroutines.Deferred import retrofit2.Call import retrofit2.Response import retrofit2.http.GET @@ -12,20 +11,20 @@ import retrofit2.http.Url * Created by GKF on 2018/1/21. * get web content */ - -interface IHttpGetApi { +@Suppress("unused") +interface HttpGetApi { @GET - fun getAsync( + suspend fun getAsync( @Url url: String, @HeaderMap headers: Map - ): Deferred> + ): Response @GET - fun getMapAsync( + suspend fun getMapAsync( @Url url: String, @QueryMap(encoded = true) queryMap: Map, @HeaderMap headers: Map - ): Deferred> + ): Response @GET fun get( @@ -39,4 +38,11 @@ interface IHttpGetApi { @QueryMap(encoded = true) queryMap: Map, @HeaderMap headers: Map ): Call + + @GET + suspend fun getMapByteAsync( + @Url url: String, + @QueryMap(encoded = true) queryMap: Map, + @HeaderMap headers: Map + ): Response } diff --git a/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt b/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt similarity index 78% rename from app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt rename to app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt index 17bf16ee8..9634b5b79 100644 --- a/app/src/main/java/io/legado/app/data/api/IHttpPostApi.kt +++ b/app/src/main/java/io/legado/app/help/http/api/HttpPostApi.kt @@ -1,6 +1,5 @@ -package io.legado.app.data.api +package io.legado.app.help.http.api -import kotlinx.coroutines.Deferred import okhttp3.RequestBody import retrofit2.Call import retrofit2.Response @@ -10,23 +9,23 @@ import retrofit2.http.* * Created by GKF on 2018/1/29. * post */ - -interface IHttpPostApi { +@Suppress("unused") +interface HttpPostApi { @FormUrlEncoded @POST - fun postMapAsync( + suspend fun postMapAsync( @Url url: String, @FieldMap(encoded = true) fieldMap: Map, @HeaderMap headers: Map - ): Deferred> + ): Response @POST - fun postBodyAsync( + suspend fun postBodyAsync( @Url url: String, @Body body: RequestBody, @HeaderMap headers: Map - ): Deferred> + ): Response @FormUrlEncoded @POST @@ -45,9 +44,9 @@ interface IHttpPostApi { @FormUrlEncoded @POST - fun postMapByteAsync( + suspend fun postMapByteAsync( @Url url: String, @FieldMap(encoded = true) fieldMap: Map, @HeaderMap headers: Map - ): Deferred> + ): Response } diff --git a/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt b/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt index d6e81a68f..4a51881b0 100644 --- a/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt +++ b/app/src/main/java/io/legado/app/help/permission/OnPermissionsDeniedCallback.kt @@ -1,5 +1,7 @@ package io.legado.app.help.permission interface OnPermissionsDeniedCallback { + fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array) + } diff --git a/app/src/main/java/io/legado/app/help/permission/Request.kt b/app/src/main/java/io/legado/app/help/permission/Request.kt index 1fc765bf9..600cbe93c 100644 --- a/app/src/main/java/io/legado/app/help/permission/Request.kt +++ b/app/src/main/java/io/legado/app/help/permission/Request.kt @@ -43,7 +43,7 @@ internal class Request : OnRequestPermissionsResultCallback { } fun addPermissions(vararg permissions: String) { - this.permissions?.addAll(Arrays.asList(*permissions)) + this.permissions?.addAll(listOf(*permissions)) } fun setRequestCode(requestCode: Int) { diff --git a/app/src/main/java/io/legado/app/help/permission/RequestManager.kt b/app/src/main/java/io/legado/app/help/permission/RequestManager.kt index b3fede03f..18bedb800 100644 --- a/app/src/main/java/io/legado/app/help/permission/RequestManager.kt +++ b/app/src/main/java/io/legado/app/help/permission/RequestManager.kt @@ -33,6 +33,7 @@ internal object RequestManager : OnPermissionsResultCallback { if (index >= 0) { val to = it.size - 1 if (index != to) { + @Suppress("NULLABILITY_MISMATCH_BASED_ON_JAVA_ANNOTATIONS") Collections.swap(requests, index, to) } } else { 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 90bf3b9e2..c7de266ca 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 @@ -4,23 +4,21 @@ import android.content.Context import android.net.Uri import androidx.documentfile.provider.DocumentFile import io.legado.app.App -import io.legado.app.help.FileHelp +import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig -import io.legado.app.utils.DocumentUtils -import io.legado.app.utils.FileUtils -import io.legado.app.utils.GSON +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import org.jetbrains.anko.defaultSharedPreferences import java.io.File +import java.util.concurrent.TimeUnit object Backup { - val backupPath = App.INSTANCE.filesDir.absolutePath + File.separator + "backup" - - val defaultPath by lazy { - FileUtils.getSdCardPath() + File.separator + "YueDu" + val backupPath: String by lazy { + FileUtils.getDirFile(App.INSTANCE.filesDir, "backup").absolutePath } val legadoPath by lazy { @@ -33,52 +31,37 @@ object Backup { val backupFileNames by lazy { arrayOf( - "bookshelf.json", - "bookGroup.json", - "bookSource.json", - "rssSource.json", - "replaceRule.json", - ReadBookConfig.readConfigFileName, - "config.xml" + "bookshelf.json", "bookGroup.json", "bookSource.json", "rssSource.json", + "rssStar.json", "replaceRule.json", ReadBookConfig.readConfigFileName, "config.xml" ) } - suspend fun backup(context: Context, uri: Uri?) { - withContext(IO) { - App.db.bookDao().allBooks.let { - if (it.isNotEmpty()) { - val json = GSON.toJson(it) - FileHelp.getFile(backupPath + File.separator + "bookshelf.json").writeText(json) - } - } - App.db.bookGroupDao().all().let { - if (it.isNotEmpty()) { - val json = GSON.toJson(it) - FileHelp.getFile(backupPath + File.separator + "bookGroup.json").writeText(json) - } - } - App.db.bookSourceDao().all.let { - if (it.isNotEmpty()) { - val json = GSON.toJson(it) - FileHelp.getFile(backupPath + File.separator + "bookSource.json") - .writeText(json) - } - } - App.db.rssSourceDao().all.let { - if (it.isNotEmpty()) { - val json = GSON.toJson(it) - FileHelp.getFile(backupPath + File.separator + "rssSource.json").writeText(json) - } - } - App.db.replaceRuleDao().all.let { - if (it.isNotEmpty()) { - val json = GSON.toJson(it) - FileHelp.getFile(backupPath + File.separator + "replaceRule.json") - .writeText(json) - } + fun autoBack(context: Context) { + val lastBackup = context.getPrefLong(PreferKey.lastBackup) + if (lastBackup + TimeUnit.DAYS.toMillis(1) < System.currentTimeMillis()) { + return + } + Coroutine.async { + val backupPath = context.getPrefString(PreferKey.backupPath) + if (backupPath.isNullOrEmpty()) { + backup(context) + } else { + backup(context, backupPath) } + } + } + + suspend fun backup(context: Context, path: String = legadoPath) { + context.putPrefLong(PreferKey.lastBackup, System.currentTimeMillis()) + withContext(IO) { + writeListToJson(App.db.bookDao().all, "bookshelf.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.rssStarDao().all, "rssStar.json", backupPath) + writeListToJson(App.db.replaceRuleDao().all, "replaceRule.json", backupPath) GSON.toJson(ReadBookConfig.configList)?.let { - FileHelp.getFile(backupPath + File.separator + ReadBookConfig.readConfigFileName) + FileUtils.createFileIfNotExist(backupPath + File.separator + ReadBookConfig.readConfigFileName) .writeText(it) } Preferences.getSharedPreferences(App.INSTANCE, backupPath, "config")?.let { sp -> @@ -96,37 +79,50 @@ object Backup { edit.commit() } WebDavHelp.backUpWebDav(backupPath) - if (uri != null) { - copyBackup(context, uri) + if (path.isContentPath()) { + copyBackup(context, Uri.parse(path)) } else { - copyBackup() + copyBackup(File(path)) } } } + private fun writeListToJson(list: List, fileName: String, path: String) { + if (list.isNotEmpty()) { + val json = GSON.toJson(list) + FileUtils.createFileIfNotExist(path + File.separator + fileName).writeText(json) + } + } + + @Throws(java.lang.Exception::class) private fun copyBackup(context: Context, uri: Uri) { DocumentFile.fromTreeUri(context, uri)?.let { treeDoc -> for (fileName in backupFileNames) { - val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName) - doc?.let { - DocumentUtils.writeText( - context, - FileHelp.getFile(backupPath + File.separator + fileName).readText(), - doc.uri - ) + val file = File(backupPath + File.separator + fileName) + if (file.exists()) { + val doc = treeDoc.findFile(fileName) ?: treeDoc.createFile("", fileName) + doc?.let { + DocumentUtils.writeText( + context, + file.readText(), + doc.uri + ) + } } } } } - private fun copyBackup() { - try { - for (fileName in backupFileNames) { - FileHelp.getFile(backupPath + File.separator + "bookshelf.json") - .copyTo(FileHelp.getFile(legadoPath + File.separator + "bookshelf.json"), true) + @Throws(java.lang.Exception::class) + private fun copyBackup(rootFile: File) { + for (fileName in backupFileNames) { + val file = File(backupPath + File.separator + fileName) + if (file.exists()) { + file.copyTo( + FileUtils.createFileIfNotExist(rootFile, fileName), + true + ) } - } catch (e: Exception) { - e.printStackTrace() } } } \ No newline at end of file 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 new file mode 100644 index 000000000..7329705e9 --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/ImportOldData.kt @@ -0,0 +1,149 @@ +package io.legado.app.help.storage + +import android.content.Context +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.App +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.ReplaceRule +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.utils.DocumentUtils +import io.legado.app.utils.FileUtils +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.anko.toast +import java.io.File + +object ImportOldData { + val yueDuPath by lazy { + FileUtils.getSdCardPath() + File.separator + "YueDu" + } + + fun import(context: Context) { + GlobalScope.launch(Dispatchers.IO) { + try {// 导入书架 + val shelfFile = + FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookShelf.json") + val json = shelfFile.readText() + val importCount = importOldBookshelf(json) + withContext(Dispatchers.Main) { + context.toast("成功导入书籍${importCount}") + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + context.toast("导入书籍失败\n${e.localizedMessage}") + } + } + + try {// Book source + val sourceFile = + FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookSource.json") + val json = sourceFile.readText() + val importCount = importOldSource(json) + withContext(Dispatchers.Main) { + context.toast("成功导入书源${importCount}") + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + context.toast("导入源失败\n${e.localizedMessage}") + } + } + + try {// Replace rules + val ruleFile = + FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookReplaceRule.json") + val json = ruleFile.readText() + val importCount = importOldReplaceRule(json) + withContext(Dispatchers.Main) { + context.toast("成功导入替换规则${importCount}") + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + context.toast("导入替换规则失败\n${e.localizedMessage}") + } + } + } + } + + fun importUri(uri: Uri) { + Coroutine.async { + DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { + when (it.name) { + "myBookShelf.json" -> + try { + DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> + val importCount = importOldBookshelf(json) + withContext(Dispatchers.Main) { + App.INSTANCE.toast("成功导入书籍${importCount}") + } + } + } catch (e: java.lang.Exception) { + withContext(Dispatchers.Main) { + App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}") + } + } + "myBookSource.json" -> + try { + DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> + val importCount = importOldSource(json) + withContext(Dispatchers.Main) { + App.INSTANCE.toast("成功导入书源${importCount}") + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + App.INSTANCE.toast("导入源失败\n${e.localizedMessage}") + } + } + "myBookReplaceRule.json" -> + try { + DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> + val importCount = importOldReplaceRule(json) + withContext(Dispatchers.Main) { + App.INSTANCE.toast("成功导入替换规则${importCount}") + } + } + } catch (e: Exception) { + withContext(Dispatchers.Main) { + App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}") + } + } + } + } + } + } + + fun importOldBookshelf(json: String): Int { + val books = OldBook.toNewBook(json) + App.db.bookDao().insert(*books.toTypedArray()) + return books.size + } + + fun importOldSource(json: String): Int { + val bookSources = mutableListOf() + val items: List> = Restore.jsonPath.parse(json).read("$") + for (item in items) { + val jsonItem = Restore.jsonPath.parse(item) + OldRule.jsonToBookSource(jsonItem.jsonString())?.let { + bookSources.add(it) + } + } + App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + 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 { + replaceRules.add(it) + } + } + App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) + return replaceRules.size + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/OldBook.kt b/app/src/main/java/io/legado/app/help/storage/OldBook.kt new file mode 100644 index 000000000..f8321973c --- /dev/null +++ b/app/src/main/java/io/legado/app/help/storage/OldBook.kt @@ -0,0 +1,55 @@ +package io.legado.app.help.storage + +import android.util.Log +import io.legado.app.App +import io.legado.app.constant.AppConst +import io.legado.app.data.entities.Book +import io.legado.app.utils.readBool +import io.legado.app.utils.readInt +import io.legado.app.utils.readLong +import io.legado.app.utils.readString + +object OldBook { + + fun toNewBook(json: String): List { + val books = mutableListOf() + val items: List> = Restore.jsonPath.parse(json).read("$") + val existingBooks = App.db.bookDao().allBookUrls.toSet() + for (item in items) { + val jsonItem = Restore.jsonPath.parse(item) + val book = Book() + book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" + if (book.bookUrl.isBlank()) continue + book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" + if (book.bookUrl in existingBooks) { + Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") + continue + } + book.origin = jsonItem.readString("$.tag") ?: "" + book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" + book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" + book.type = + if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 + book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl + book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") + book.customCoverUrl = jsonItem.readString("$.customCoverPath") + book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 + book.canUpdate = jsonItem.readBool("$.allowUpdate") == true + book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 + book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 + book.durChapterTitle = jsonItem.readString("$.durChapterName") + book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 + book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 + book.group = jsonItem.readInt("$.group") ?: 0 + book.intro = jsonItem.readString("$.bookInfoBean.introduce") + book.latestChapterTitle = jsonItem.readString("$.lastChapterName") + book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 + book.order = jsonItem.readInt("$.serialNumber") ?: 0 + book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true + book.variable = jsonItem.readString("$.variable") + books.add(book) + } + return books + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/Preferences.kt b/app/src/main/java/io/legado/app/help/storage/Preferences.kt index 11907e83f..d0674f01e 100644 --- a/app/src/main/java/io/legado/app/help/storage/Preferences.kt +++ b/app/src/main/java/io/legado/app/help/storage/Preferences.kt @@ -30,7 +30,6 @@ object Preferences { val fieldMPreferencesDir = objMBase.javaClass.getDeclaredField("mPreferencesDir") fieldMPreferencesDir.isAccessible = true // 创建自定义路径 - // String FILE_PATH = Environment.getExternalStorageDirectory().getAbsolutePath()+"/Android"; val file = File(dir) // 修改mPreferencesDir变量的值 fieldMPreferencesDir.set(objMBase, file) 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 15c14051c..123332df6 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 @@ -2,25 +2,20 @@ package io.legado.app.help.storage import android.content.Context import android.net.Uri -import android.util.Log import androidx.documentfile.provider.DocumentFile import com.jayway.jsonpath.Configuration import com.jayway.jsonpath.JsonPath import com.jayway.jsonpath.Option import com.jayway.jsonpath.ParseContext import io.legado.app.App -import io.legado.app.constant.AppConst +import io.legado.app.constant.PreferKey import io.legado.app.data.entities.* -import io.legado.app.help.FileHelp +import io.legado.app.help.LauncherIconHelp import io.legado.app.help.ReadBookConfig import io.legado.app.utils.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import org.jetbrains.anko.defaultSharedPreferences -import org.jetbrains.anko.toast import java.io.File object Restore { @@ -32,17 +27,35 @@ object Restore { ) } - suspend fun restore(context: Context, uri: Uri) { + suspend fun restore(context: Context, path: String) { withContext(IO) { - DocumentFile.fromTreeUri(context, uri)?.listFiles()?.forEach { doc -> - for (fileName in Backup.backupFileNames) { - if (doc.name == fileName) { - DocumentUtils.readText(context, doc.uri)?.let { - FileHelp.getFile(Backup.backupPath + File.separator + fileName) - .writeText(it) + if (path.isContentPath()) { + DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc -> + for (fileName in Backup.backupFileNames) { + if (doc.name == fileName) { + DocumentUtils.readText(context, doc.uri)?.let { + FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName) + .writeText(it) + } } } } + } else { + try { + val file = File(path) + for (fileName in Backup.backupFileNames) { + FileUtils.getFile(file, fileName).let { + if (it.exists()) { + it.copyTo( + FileUtils.createFileIfNotExist(Backup.backupPath + File.separator + fileName), + true + ) + } + } + } + } catch (e: Exception) { + e.printStackTrace() + } } } restore(Backup.backupPath) @@ -50,54 +63,27 @@ object Restore { suspend fun restore(path: String) { withContext(IO) { - try { - val file = FileHelp.getFile(path + File.separator + "bookshelf.json") - val json = file.readText() - GSON.fromJsonArray(json)?.let { - App.db.bookDao().insert(*it.toTypedArray()) - } - } catch (e: Exception) { - e.printStackTrace() + fileToListT(path, "bookshelf.json")?.let { + App.db.bookDao().insert(*it.toTypedArray()) } - try { - val file = FileHelp.getFile(path + File.separator + "bookGroup.json") - val json = file.readText() - GSON.fromJsonArray(json)?.let { - App.db.bookGroupDao().insert(*it.toTypedArray()) - } - } catch (e: Exception) { - e.printStackTrace() + fileToListT(path, "bookGroup.json")?.let { + App.db.bookGroupDao().insert(*it.toTypedArray()) } - try { - val file = FileHelp.getFile(path + File.separator + "bookSource.json") - val json = file.readText() - GSON.fromJsonArray(json)?.let { - App.db.bookSourceDao().insert(*it.toTypedArray()) - } - } catch (e: Exception) { - e.printStackTrace() + fileToListT(path, "bookSource.json")?.let { + App.db.bookSourceDao().insert(*it.toTypedArray()) } - try { - val file = FileHelp.getFile(path + File.separator + "rssSource.json") - val json = file.readText() - GSON.fromJsonArray(json)?.let { - App.db.rssSourceDao().insert(*it.toTypedArray()) - } - } catch (e: Exception) { - e.printStackTrace() + fileToListT(path, "rssSource.json")?.let { + App.db.rssSourceDao().insert(*it.toTypedArray()) } - try { - val file = FileHelp.getFile(path + File.separator + "replaceRule.json") - val json = file.readText() - GSON.fromJsonArray(json)?.let { - App.db.replaceRuleDao().insert(*it.toTypedArray()) - } - } catch (e: Exception) { - e.printStackTrace() + fileToListT(path, "rssStar.json")?.let { + App.db.rssStarDao().insert(*it.toTypedArray()) + } + fileToListT(path, "replaceRule.json")?.let { + App.db.replaceRuleDao().insert(*it.toTypedArray()) } try { val file = - FileHelp.getFile(path + File.separator + ReadBookConfig.readConfigFileName) + FileUtils.createFileIfNotExist(path + File.separator + ReadBookConfig.readConfigFileName) val configFile = File(App.INSTANCE.filesDir.absolutePath + File.separator + ReadBookConfig.readConfigFileName) if (file.exists()) { @@ -117,122 +103,22 @@ object Restore { is String -> edit.putString(it.key, value) else -> Unit } + edit.putInt(PreferKey.versionCode, App.INSTANCE.versionCode) edit.commit() } } + LauncherIconHelp.changeIcon(App.INSTANCE.getPrefString(PreferKey.launcherIcon)) } - fun importYueDuData(context: Context) { - GlobalScope.launch(IO) { - try {// 导入书架 - val shelfFile = - FileHelp.getFile(Backup.defaultPath + File.separator + "myBookShelf.json") - val json = shelfFile.readText() - val importCount = importOldBookshelf(json) - withContext(Main) { - context.toast("成功导入书籍${importCount}") - } - } catch (e: Exception) { - withContext(Main) { - context.toast("导入书籍失败\n${e.localizedMessage}") - } - } - - try {// Book source - val sourceFile = - FileHelp.getFile(Backup.defaultPath + File.separator + "myBookSource.json") - val json = sourceFile.readText() - val importCount = importOldSource(json) - withContext(Main) { - context.toast("成功导入书源${importCount}") - } - } catch (e: Exception) { - withContext(Main) { - context.toast("导入源失败\n${e.localizedMessage}") - } - } - - try {// Replace rules - val ruleFile = - FileHelp.getFile(Backup.defaultPath + File.separator + "myBookReplaceRule.json") - val json = ruleFile.readText() - val importCount = importOldReplaceRule(json) - withContext(Main) { - context.toast("成功导入替换规则${importCount}") - } - } catch (e: Exception) { - withContext(Main) { - context.toast("导入替换规则失败\n${e.localizedMessage}") - } - } - } - } - - fun importOldBookshelf(json: String): Int { - val books = mutableListOf() - val items: List> = jsonPath.parse(json).read("$") - val existingBooks = App.db.bookDao().allBookUrls.toSet() - for (item in items) { - val jsonItem = jsonPath.parse(item) - val book = Book() - book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" - if (book.bookUrl.isBlank()) continue - book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" - if (book.bookUrl in existingBooks) { - Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") - continue - } - book.origin = jsonItem.readString("$.tag") ?: "" - book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" - book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" - book.type = - if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 - book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl - book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") - book.customCoverUrl = jsonItem.readString("$.customCoverPath") - book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 - book.canUpdate = jsonItem.readBool("$.allowUpdate") == true - book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 - book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 - book.durChapterTitle = jsonItem.readString("$.durChapterName") - book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 - book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 - book.group = jsonItem.readInt("$.group") ?: 0 - book.intro = jsonItem.readString("$.bookInfoBean.introduce") - book.latestChapterTitle = jsonItem.readString("$.lastChapterName") - book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 - book.order = jsonItem.readInt("$.serialNumber") ?: 0 - book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true - book.variable = jsonItem.readString("$.variable") - books.add(book) - } - App.db.bookDao().insert(*books.toTypedArray()) - return books.size - } - - fun importOldSource(json: String): Int { - val bookSources = mutableListOf() - val items: List> = jsonPath.parse(json).read("$") - for (item in items) { - val jsonItem = jsonPath.parse(item) - OldRule.jsonToBookSource(jsonItem.jsonString())?.let { - bookSources.add(it) - } + private inline fun fileToListT(path: String, fileName: String): List? { + try { + val file = FileUtils.createFileIfNotExist(path + File.separator + fileName) + val json = file.readText() + return GSON.fromJsonArray(json) + } catch (e: Exception) { + e.printStackTrace() } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) - return bookSources.size + return null } - fun importOldReplaceRule(json: String): Int { - val replaceRules = mutableListOf() - val items: List> = jsonPath.parse(json).read("$") - for (item in items) { - val jsonItem = jsonPath.parse(item) - OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { - replaceRules.add(it) - } - } - App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) - return replaceRules.size - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt index 4a0f3e152..a1386f9f7 100644 --- a/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt +++ b/app/src/main/java/io/legado/app/help/storage/WebDavHelp.kt @@ -1,38 +1,42 @@ package io.legado.app.help.storage import android.content.Context +import android.os.Handler +import android.os.Looper import io.legado.app.App -import io.legado.app.help.FileHelp -import io.legado.app.help.ReadBookConfig +import io.legado.app.constant.PreferKey import io.legado.app.help.coroutine.Coroutine import io.legado.app.lib.webdav.WebDav import io.legado.app.lib.webdav.http.HttpAuth +import io.legado.app.utils.FileUtils import io.legado.app.utils.ZipUtils import io.legado.app.utils.getPrefString import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.withContext import org.jetbrains.anko.selector +import org.jetbrains.anko.toast import java.io.File import java.text.SimpleDateFormat import java.util.* import kotlin.math.min object WebDavHelp { - private val zipFilePath = FileHelp.getCachePath() + "/backup" + ".zip" - private val unzipFilesPath by lazy { - FileHelp.getCachePath() - } + private const val defaultWebDavUrl = "https://dav.jianguoyun.com/dav/" + private val zipFilePath = "${FileUtils.getCachePath()}${File.separator}backup.zip" - private fun getWebDavUrl(): String? { - var url = App.INSTANCE.getPrefString("web_dav_url") - if (url.isNullOrBlank()) return null + private fun getWebDavUrl(): String { + var url = App.INSTANCE.getPrefString(PreferKey.webDavUrl) + if (url.isNullOrEmpty()) { + url = defaultWebDavUrl + } if (!url.endsWith("/")) url += "/" return url } private fun initWebDav(): Boolean { - val account = App.INSTANCE.getPrefString("web_dav_account") - val password = App.INSTANCE.getPrefString("web_dav_password") + val account = App.INSTANCE.getPrefString(PreferKey.webDavAccount) + val password = App.INSTANCE.getPrefString(PreferKey.webDavPassword) if (!account.isNullOrBlank() && !password.isNullOrBlank()) { HttpAuth.auth = HttpAuth.Auth(account, password) return true @@ -43,24 +47,30 @@ object WebDavHelp { private fun getWebDavFileNames(): ArrayList { val url = getWebDavUrl() val names = arrayListOf() - if (!url.isNullOrBlank() && initWebDav()) { - var files = WebDav(url + "legado/").listFiles() - files = files.reversed() - for (index: Int in 0 until min(10, files.size)) { - files[index].displayName?.let { - names.add(it) + if (initWebDav()) { + try { + var files = WebDav(url + "legado/").listFiles() + files = files.reversed() + for (index: Int in 0 until min(10, files.size)) { + files[index].displayName?.let { + names.add(it) + } } + } catch (e: Exception) { + return names } } return names } - suspend fun showRestoreDialog(context: Context): Boolean { + suspend fun showRestoreDialog(context: Context, restoreSuccess: () -> Unit): Boolean { val names = withContext(IO) { getWebDavFileNames() } return if (names.isNotEmpty()) { - context.selector(title = "选择恢复文件", items = names) { _, index -> - if (index in 0 until names.size) { - restoreWebDav(names[index]) + withContext(Main) { + context.selector(title = "选择恢复文件", items = names) { _, index -> + if (index in 0 until names.size) { + restoreWebDav(names[index], restoreSuccess) + } } } true @@ -69,35 +79,39 @@ object WebDavHelp { } } - private fun restoreWebDav(name: String) { + private fun restoreWebDav(name: String, success: () -> Unit) { Coroutine.async { - getWebDavUrl()?.let { + getWebDavUrl().let { val file = WebDav(it + "legado/" + name) file.downloadTo(zipFilePath, true) @Suppress("BlockingMethodInNonBlockingContext") - ZipUtils.unzipFile(zipFilePath, unzipFilesPath) - Restore.restore(unzipFilesPath) + ZipUtils.unzipFile(zipFilePath, Backup.backupPath) + Restore.restore(Backup.backupPath) } + }.onSuccess { + success.invoke() } } fun backUpWebDav(path: String) { - if (initWebDav()) { - val paths = arrayListOf( - path + File.separator + "bookshelf.json", - path + File.separator + "bookSource.json", - path + File.separator + "rssSource.json", - path + File.separator + "replaceRule.json", - path + File.separator + "config.xml", - path + File.separator + ReadBookConfig.readConfigFileName - ) - FileHelp.deleteFile(zipFilePath) - if (ZipUtils.zipFiles(paths, zipFilePath)) { - WebDav(getWebDavUrl() + "legado").makeAsDir() - val putUrl = getWebDavUrl() + "legado/backup" + - SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) - .format(Date(System.currentTimeMillis())) + ".zip" - WebDav(putUrl).upload(zipFilePath) + try { + if (initWebDav()) { + val paths = arrayListOf(*Backup.backupFileNames) + for (i in 0 until paths.size) { + paths[i] = path + File.separator + paths[i] + } + FileUtils.deleteFile(zipFilePath) + if (ZipUtils.zipFiles(paths, zipFilePath)) { + WebDav(getWebDavUrl() + "legado").makeAsDir() + val putUrl = getWebDavUrl() + "legado/backup" + + SimpleDateFormat("yyyy-MM-dd", Locale.getDefault()) + .format(Date(System.currentTimeMillis())) + ".zip" + WebDav(putUrl).upload(zipFilePath) + } + } + } catch (e: Exception) { + Handler(Looper.getMainLooper()).post { + App.INSTANCE.toast("WebDav\n${e.localizedMessage}") } } } diff --git a/app/src/main/java/io/legado/app/lib/README.md b/app/src/main/java/io/legado/app/lib/README.md index 8b248561d..1089be064 100644 --- a/app/src/main/java/io/legado/app/lib/README.md +++ b/app/src/main/java/io/legado/app/lib/README.md @@ -1 +1,4 @@ -## 放置一些copy过来的库 \ No newline at end of file +## 放置一些copy过来的库 +* dialogs 弹出框 +* theme 主题 +* webDav 网络存储 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt index 60404b266..ae477acd6 100644 --- a/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt +++ b/app/src/main/java/io/legado/app/lib/dialogs/AlertBuilder.kt @@ -24,10 +24,8 @@ import android.content.DialogInterface import android.graphics.drawable.Drawable import android.view.KeyEvent import android.view.View -import android.view.ViewManager import androidx.annotation.DrawableRes import androidx.annotation.StringRes -import org.jetbrains.anko.UI import org.jetbrains.anko.internals.AnkoInternals.NO_GETTER import kotlin.DeprecationLevel.ERROR diff --git a/app/src/main/java/io/legado/app/lib/theme/ATH.kt b/app/src/main/java/io/legado/app/lib/theme/ATH.kt index 57c7c8403..e20741987 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ATH.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ATH.kt @@ -16,9 +16,8 @@ import androidx.recyclerview.widget.RecyclerView import androidx.viewpager.widget.ViewPager import com.google.android.material.bottomnavigation.BottomNavigationView import io.legado.app.R +import io.legado.app.help.AppConfig import io.legado.app.utils.getCompatColor -import io.legado.app.utils.isNightTheme -import io.legado.app.utils.isTransparentStatusBar import kotlinx.android.synthetic.main.activity_main.view.* import org.jetbrains.anko.backgroundColor @@ -37,7 +36,7 @@ object ATH { } fun setStatusBarColorAuto(activity: Activity, fullScreen: Boolean) { - val isTransparentStatusBar = activity.isTransparentStatusBar + val isTransparentStatusBar = AppConfig.isTransparentStatusBar setStatusBarColor( activity, ThemeStore.statusBarColor(activity, isTransparentStatusBar), @@ -131,14 +130,14 @@ object ATH { fun setTint( view: View, @ColorInt color: Int, - isDark: Boolean = view.context.isNightTheme + isDark: Boolean = AppConfig.isNightTheme(view.context) ) { TintHelper.setTintAuto(view, color, false, isDark) } fun setBackgroundTint( view: View, @ColorInt color: Int, - isDark: Boolean = view.context.isNightTheme + isDark: Boolean = AppConfig.isNightTheme ) { TintHelper.setTintAuto(view, color, true, isDark) } @@ -206,10 +205,7 @@ object ATH { .setSelectedColor(ThemeStore.accentColor(bottom_navigation_view.context)).create() itemIconTintList = colorStateList itemTextColor = colorStateList - itemBackgroundResource = when(context.isNightTheme) { - true -> R.drawable.item_bg_dark - false -> R.drawable.item_bg_light - } + itemBackgroundResource = R.color.background_menu } } diff --git a/app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt b/app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt index 24d0a923a..73e342713 100644 --- a/app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt +++ b/app/src/main/java/io/legado/app/lib/theme/ATHUtils.kt @@ -8,10 +8,6 @@ import androidx.annotation.AttrRes */ object ATHUtils { - fun isWindowBackgroundDark(context: Context): Boolean { - return !ColorUtils.isColorLight(resolveColor(context, android.R.attr.windowBackground)) - } - @JvmOverloads fun resolveColor(context: Context, @AttrRes attr: Int, fallback: Int = 0): Int { val a = context.theme.obtainStyledAttributes(intArrayOf(attr)) diff --git a/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt index 7b3653eb2..a54046dd3 100644 --- a/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt +++ b/app/src/main/java/io/legado/app/lib/theme/TintHelper.kt @@ -192,6 +192,7 @@ object TintHelper { } } + @SuppressLint("PrivateResource") fun setTint(radioButton: RadioButton, @ColorInt color: Int, useDarker: Boolean) { val sl = ColorStateList( arrayOf( @@ -254,11 +255,10 @@ object TintHelper { if (!skipIndeterminate) progressBar.indeterminateTintList = sl } else { - val mode = PorterDuff.Mode.SRC_IN if (!skipIndeterminate && progressBar.indeterminateDrawable != null) - progressBar.indeterminateDrawable.setColorFilter(color, mode) + progressBar.indeterminateDrawable.setTint(color) if (progressBar.progressDrawable != null) - progressBar.progressDrawable.setColorFilter(color, mode) + progressBar.progressDrawable.setTint(color) } } @@ -291,6 +291,7 @@ object TintHelper { setCursorTint(editText, color) } + @SuppressLint("PrivateResource") fun setTint(box: CheckBox, @ColorInt color: Int, useDarker: Boolean) { val sl = ColorStateList( arrayOf( diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEAccentBgTextView.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATEAccentBgTextView.kt deleted file mode 100644 index fe1f0b0ea..000000000 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEAccentBgTextView.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.legado.app.lib.theme.view - -import android.content.Context -import android.graphics.Color -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView -import io.legado.app.R -import io.legado.app.lib.theme.ColorUtils -import io.legado.app.lib.theme.Selector -import io.legado.app.lib.theme.ThemeStore - -class ATEAccentBgTextView(context: Context, attrs: AttributeSet) : - AppCompatTextView(context, attrs) { - - init { - val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ATEAccentBgTextView) - val radios = - typedArray.getDimensionPixelOffset(R.styleable.ATEAccentBgTextView_abt_radius, 0) - typedArray.recycle() - background = Selector.shapeBuild() - .setCornerRadius(radios) - .setDefaultBgColor(ThemeStore.accentColor(context)) - .setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) - .create() - setTextColor(Color.WHITE) - } -} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEStrokeTextView.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATEStrokeTextView.kt deleted file mode 100644 index d113321d9..000000000 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEStrokeTextView.kt +++ /dev/null @@ -1,31 +0,0 @@ -package io.legado.app.lib.theme.view - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatTextView -import io.legado.app.R -import io.legado.app.lib.theme.Selector -import io.legado.app.lib.theme.ThemeStore -import io.legado.app.utils.dp -import io.legado.app.utils.getCompatColor - -class ATEStrokeTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { - - init { - background = Selector.shapeBuild() - .setCornerRadius(1.dp) - .setStrokeWidth(1.dp) - .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) - .setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) - .setSelectedStrokeColor(ThemeStore.accentColor(context)) - .setPressedBgColor(context.getCompatColor(R.color.transparent30)) - .create() - setTextColor( - Selector.colorBuild() - .setDefaultColor(ThemeStore.textColorSecondary(context)) - .setSelectedColor(ThemeStore.accentColor(context)) - .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) - .create() - ) - } -} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATETextInputLayout.kt b/app/src/main/java/io/legado/app/lib/theme/view/ATETextInputLayout.kt deleted file mode 100644 index 5444a309c..000000000 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATETextInputLayout.kt +++ /dev/null @@ -1,15 +0,0 @@ -package io.legado.app.lib.theme.view - -import android.content.Context -import android.util.AttributeSet -import com.google.android.material.textfield.TextInputLayout -import io.legado.app.lib.theme.Selector -import io.legado.app.lib.theme.ThemeStore - -class ATETextInputLayout(context: Context, attrs: AttributeSet?) : TextInputLayout(context, attrs) { - - init { - defaultHintTextColor = Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create() - } - -} 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 8cf806786..32b79ce79 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 @@ -23,13 +23,20 @@ class WebDav @Throws(MalformedURLException::class) constructor(urlStr: String) { companion object { // 指定返回哪些属性 - private const val DIR = "\n" + - "\n" + - "\n" + - "\n\n\n\n\n%s" + - "\n" + - "" + private const val DIR = + """ + + + + + + + + %s + + """ } + private val url: URL = URL(null, urlStr, Handler) private val httpUrl: String? by lazy { val raw = url.toString().replace("davs://", "https://").replace("dav://", "http://") diff --git a/app/src/main/java/io/legado/app/model/README.md b/app/src/main/java/io/legado/app/model/README.md index 0b9c08ba9..d791c861e 100644 --- a/app/src/main/java/io/legado/app/model/README.md +++ b/app/src/main/java/io/legado/app/model/README.md @@ -1,2 +1,6 @@ ## 放置一些模块类 -* 书源解析 +* analyzeRule 书源规则解析 +* localBook 本地书籍解析 +* rss 订阅规则解析 +* webBook 获取网络书籍 + diff --git a/app/src/main/java/io/legado/app/model/WebBook.kt b/app/src/main/java/io/legado/app/model/WebBook.kt index e28c09519..4cc2544f5 100644 --- a/app/src/main/java/io/legado/app/model/WebBook.kt +++ b/app/src/main/java/io/legado/app/model/WebBook.kt @@ -6,10 +6,10 @@ import io.legado.app.data.entities.BookSource import io.legado.app.data.entities.SearchBook import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.analyzeRule.AnalyzeUrl -import io.legado.app.model.webbook.BookChapterList -import io.legado.app.model.webbook.BookContent -import io.legado.app.model.webbook.BookInfo -import io.legado.app.model.webbook.BookList +import io.legado.app.model.webBook.BookChapterList +import io.legado.app.model.webBook.BookContent +import io.legado.app.model.webBook.BookInfo +import io.legado.app.model.webBook.BookList import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlin.coroutines.CoroutineContext diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt index 84b5045e7..e7360da7e 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSonPath.kt @@ -72,7 +72,7 @@ class AnalyzeByJSonPath { } } } - return TextUtils.join(",", textList) + return textList.joinToString("\n") } } diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt index c5b9423df..38fb633c5 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByJSoup.kt @@ -51,7 +51,10 @@ class AnalyzeByJSoup { val textS = getStringList(ruleStr) return if (textS.isEmpty()) { null - } else join(",", textS).trim { it <= ' ' } + } else { + textS.joinToString("\n") + } + } /** @@ -356,8 +359,7 @@ class AnalyzeByJSoup { try { when (lastRule) { "text" -> for (element in elements) { - val text = element.text() - textS.add(text) + textS.add(element.text()) } "textNodes" -> for (element in elements) { val tn = arrayListOf() @@ -370,12 +372,15 @@ class AnalyzeByJSoup { } textS.add(join("\n", tn)) } - "ownText", "html" -> { - elements.select("script").remove() + "ownText" -> for (element in elements) { + textS.add(element.ownText()) + } + "html" -> { + elements.select("script, style").remove() val html = elements.html() textS.add(html) } - "all" -> textS.add(elements.html()) + "all" -> textS.add(elements.outerHtml()) else -> for (element in elements) { val url = element.attr(lastRule) if (!isEmpty(url) && !textS.contains(url)) { diff --git a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt index f2745ee69..707ab9a22 100644 --- a/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt +++ b/app/src/main/java/io/legado/app/model/analyzeRule/AnalyzeByXPath.kt @@ -181,7 +181,7 @@ class AnalyzeByXPath { } } } - return TextUtils.join(",", textList) + return textList.joinToString("\n") } } } 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 3aa920237..f9dbba8ee 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 @@ -8,6 +8,7 @@ import io.legado.app.data.entities.BaseBook import io.legado.app.data.entities.BookChapter import io.legado.app.help.JsExtensions import io.legado.app.utils.* +import org.jsoup.nodes.Entities import org.mozilla.javascript.NativeObject import java.util.* import java.util.regex.Pattern @@ -108,18 +109,21 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { */ @Throws(Exception::class) @JvmOverloads - fun getStringList(rule: String, isUrl: Boolean = false): List? { - if (TextUtils.isEmpty(rule)) return null + fun getStringList(rule: String?, isUrl: Boolean = false): List? { + if (rule.isNullOrEmpty()) return null val ruleList = splitSourceRule(rule) return getStringList(ruleList, isUrl) } @Throws(Exception::class) - fun getStringList(ruleList: List, isUrl: Boolean): List? { + fun getStringList(ruleList: List, isUrl: Boolean = false): List? { var result: Any? = null - content?.let { o -> - if (ruleList.isNotEmpty()) { - if (ruleList.isNotEmpty()) result = o + val content = this.content + if (content != null && ruleList.isNotEmpty()) { + result = content + if (content is NativeObject) { + result = content[ruleList[0].rule]?.toString() + } else { for (sourceRule in ruleList) { putRule(sourceRule.putMap) sourceRule.makeUpRule(result) @@ -203,7 +207,7 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { else -> sourceRule.rule } } - if (sourceRule.replaceRegex.isNotEmpty()) { + if ((result != null) && sourceRule.replaceRegex.isNotEmpty()) { result = replaceRegex(result.toString(), sourceRule) } } @@ -211,10 +215,15 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { } } if (result == null) result = "" + val str = try { + Entities.unescape(result.toString()) + } catch (e: Exception) { + result.toString() + } if (isUrl) { - return NetworkUtils.getAbsoluteURL(baseUrl, result.toString()) ?: "" + return NetworkUtils.getAbsoluteURL(baseUrl, str) ?: "" } - return result.toString() + return str } /** @@ -582,12 +591,17 @@ class AnalyzeRule(var book: BaseBook? = null) : JsExtensions { */ @Throws(Exception::class) private fun evalJS(jsStr: String, result: Any?): Any? { - val bindings = SimpleBindings() - bindings["java"] = this - bindings["book"] = book - bindings["result"] = result - bindings["baseUrl"] = baseUrl - return SCRIPT_ENGINE.eval(jsStr, bindings) + try { + val bindings = SimpleBindings() + bindings["java"] = this + bindings["book"] = book + bindings["result"] = result + bindings["baseUrl"] = baseUrl + return SCRIPT_ENGINE.eval(jsStr, bindings) + } catch (e: Exception) { + e.printStackTrace() + throw e + } } /** 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 b5343cf7f..f1c92e798 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 @@ -6,14 +6,14 @@ import androidx.annotation.Keep import io.legado.app.constant.AppConst.SCRIPT_ENGINE import io.legado.app.constant.Pattern.EXP_PATTERN import io.legado.app.constant.Pattern.JS_PATTERN -import io.legado.app.data.api.IHttpGetApi -import io.legado.app.data.api.IHttpPostApi import io.legado.app.data.entities.BaseBook import io.legado.app.help.JsExtensions import io.legado.app.help.http.AjaxWebView import io.legado.app.help.http.HttpHelper import io.legado.app.help.http.RequestMethod import io.legado.app.help.http.Res +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 @@ -162,7 +162,7 @@ class AnalyzeUrl( } if (urlArray.size > 1) { val options = GSON.fromJsonObject>(urlArray[1]) - options?.let { + options?.let { _ -> options["method"]?.let { if (it.equals("POST", true)) method = RequestMethod.POST } options["headers"]?.let { headers -> GSON.fromJsonObject>(headers)?.let { headerMap.putAll(it) } @@ -248,19 +248,19 @@ class AnalyzeUrl( method == RequestMethod.POST -> { if (fieldMap.isNotEmpty()) { HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .postMap(url, fieldMap, headerMap) } else { HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .postBody(url, body!!, headerMap) } } fieldMap.isEmpty() -> HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .get(url, headerMap) else -> HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .getMap(url, fieldMap, headerMap) } } @@ -284,24 +284,20 @@ class AnalyzeUrl( method == RequestMethod.POST -> { if (fieldMap.isNotEmpty()) { HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .postMapAsync(url, fieldMap, headerMap) - .await() } else { HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .postBodyAsync(url, body!!, headerMap) - .await() } } fieldMap.isEmpty() -> HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .getAsync(url, headerMap) - .await() else -> HttpHelper - .getApiService(baseUrl) + .getApiService(baseUrl, charset) .getMapAsync(url, fieldMap, headerMap) - .await() } return Res(NetworkUtils.getUrl(res), res.body()) } 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 new file mode 100644 index 000000000..fad747b34 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/AnalyzeTxtFile.kt @@ -0,0 +1,247 @@ +package io.legado.app.model.localBook + +import android.content.Context +import android.net.Uri +import io.legado.app.App +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.utils.* +import java.io.File +import java.io.RandomAccessFile +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 + private val cacheFolder: File by lazy { + val rootFile = App.INSTANCE.getExternalFilesDir(null) + ?: App.INSTANCE.externalCacheDir + ?: App.INSTANCE.cacheDir + FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName)) + } + + fun analyze(context: Context, book: Book): ArrayList { + val bookFile = getBookFile(context, book) + book.charset = EncodingDetect.getEncode(bookFile) + val charset = book.fileCharset() + val toc = arrayListOf() + //获取文件流 + val bookStream = RandomAccessFile(bookFile, "r") + val rulePattern = getTocRule(book, bookStream, charset) + + //加载章节 + val buffer = ByteArray(BUFFER_SIZE) + //获取到的块起始点,在文件中的位置 + var curOffset: Long = 0 + //block的个数 + var blockPos = 0 + //读取的长度 + var length: Int + var allLength = 0 + + //获取文件中的数据到buffer,直到没有数据为止 + while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) { + ++blockPos + //如果存在Chapter + if (rulePattern != null) { //将数据转换成String + var blockContent = String(buffer, 0, length, charset) + val lastN = blockContent.lastIndexOf("\n") + if (lastN != 0) { + blockContent = blockContent.substring(0, lastN) + length = blockContent.toByteArray(charset).size + allLength += length + bookStream.seek(allLength.toLong()) + } + //当前Block下使过的String的指针 + var seekPos = 0 + //进行正则匹配 + val matcher: Matcher = rulePattern.matcher(blockContent) + //如果存在相应章节 + while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 + val chapterStart = matcher.start() + //如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 + //第一种情况一定是序章 第二种情况可能是上一个章节的内容 + if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容 + val chapterContent = blockContent.substring(seekPos, chapterStart) + //设置指针偏移 + seekPos += chapterContent.length + if (toc.size == 0) { //如果当前没有章节,那么就是序章 + //加入简介 + book.intro = chapterContent + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = chapterContent.toByteArray(charset).size.toLong() + toc.add(curChapter) + } else { //否则就block分割之后,上一个章节的剩余内容 + //获取上一章节 + val lastChapter = toc.last() + //将当前段落添加上一章去 + lastChapter.end = + lastChapter.end!! + chapterContent.toByteArray(charset).size + //创建当前章节 + 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 + //获取上一章节 + val lastChapter = toc.last() + lastChapter.end = + lastChapter.start!! + chapterContent.toByteArray(charset).size + //创建当前章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = lastChapter.end + toc.add(curChapter) + } else { //如果章节不存在则创建章节 + val curChapter = BookChapter() + curChapter.title = matcher.group() + curChapter.start = 0L + curChapter.end = 0L + toc.add(curChapter) + } + } + } + } else { //进行本地虚拟分章 + //章节在buffer的偏移量 + var chapterOffset = 0 + //当前剩余可分配的长度 + var strLength = length + //分章的位置 + var chapterPos = 0 + while (strLength > 0) { + ++chapterPos + //是否长度超过一章 + if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { //在buffer中一章的终止点 + var end = length + //寻找换行符作为终止点 + for (i in chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER until length) { + if (buffer[i] == BLANK) { + end = i + break + } + } + val chapter = BookChapter() + chapter.title = "第${blockPos}章($chapterPos)" + chapter.start = curOffset + chapterOffset + 1 + chapter.end = curOffset + end + toc.add(chapter) + //减去已经被分配的长度 + strLength -= (end - chapterOffset) + //设置偏移的位置 + chapterOffset = end + } else { + val chapter = BookChapter() + chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")" + chapter.start = curOffset + chapterOffset + 1 + chapter.end = curOffset + length + toc.add(chapter) + strLength = 0 + } + } + } + + //block的偏移点 + curOffset += length.toLong() + + if (rulePattern != null) { //设置上一章的结尾 + val lastChapter = toc.last() + lastChapter.end = curOffset + } + + //当添加的block太多的时候,执行GC + if (blockPos % 15 == 0) { + System.gc() + System.runFinalization() + } + } + bookStream.close() + for (i in toc.indices) { + val bean = toc[i] + bean.index = i + bean.bookUrl = book.bookUrl + bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "") + } + book.latestChapterTitle = toc.last().title + + 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 { + 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 + } + + 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 + //首先获取128k的数据 + val buffer = ByteArray(BUFFER_SIZE / 4) + val length = bookStream.read(buffer, 0, buffer.size) + val content = String(buffer, 0, length, charset) + for (tocRule in tocRules) { + val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE) + val matcher = pattern.matcher(content) + if (matcher.find()) { + book.tocUrl = tocRule.rule + rulePattern = pattern + break + } + } + bookStream.seek(0) + return rulePattern + } + + 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() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt new file mode 100644 index 000000000..44ee337b2 --- /dev/null +++ b/app/src/main/java/io/legado/app/model/localBook/LocalBook.kt @@ -0,0 +1,30 @@ +package io.legado.app.model.localBook + +import androidx.documentfile.provider.DocumentFile +import io.legado.app.App +import io.legado.app.data.entities.Book + + +object LocalBook { + + fun importFile(doc: DocumentFile) { + doc.name?.let { fileName -> + val str = fileName.substringBeforeLast(".") + var name = str.substringBefore("作者") + val author = str.substringAfter("作者", "") + val smhStart = name.indexOf("《") + val smhEnd = name.indexOf("》") + if (smhStart != -1 && smhEnd != -1) { + name = (name.substring(smhStart + 1, smhEnd)) + } + val book = Book( + bookUrl = doc.uri.toString(), + name = name, + author = author, + originName = fileName + ) + App.db.bookDao().insert(book) + } + } + +} \ No newline at end of file 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 new file mode 100644 index 000000000..ff3467a8a --- /dev/null +++ b/app/src/main/java/io/legado/app/model/webBook/BookChapterList.kt @@ -0,0 +1,234 @@ +package io.legado.app.model.webBook + +import android.text.TextUtils +import io.legado.app.App +import io.legado.app.R +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookChapter +import io.legado.app.data.entities.BookSource +import io.legado.app.data.entities.rule.TocRule +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.Debug +import io.legado.app.model.analyzeRule.AnalyzeRule +import io.legado.app.model.analyzeRule.AnalyzeUrl +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume +import kotlin.coroutines.resumeWithException + +object BookChapterList { + + suspend fun analyzeChapterList( + coroutineScope: CoroutineScope, + book: Book, + body: String?, + bookSource: BookSource, + baseUrl: String + ): List = suspendCancellableCoroutine { block -> + try { + val chapterList = ArrayList() + body ?: throw Exception( + App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) + ) + Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") + val tocRule = bookSource.getTocRule() + val nextUrlList = arrayListOf(baseUrl) + var reverse = false + var listRule = tocRule.chapterList ?: "" + if (listRule.startsWith("-")) { + reverse = true + listRule = listRule.substring(1) + } + if (listRule.startsWith("+")) { + listRule = listRule.substring(1) + } + var chapterData = + analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true) + chapterData.chapterList?.let { + chapterList.addAll(it) + } + when (chapterData.nextUrl.size) { + 0 -> { + block.resume(finish(book, chapterList, reverse)) + } + 1 -> { + Coroutine.async(scope = coroutineScope) { + var nextUrl = chapterData.nextUrl[0] + while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { + nextUrlList.add(nextUrl) + AnalyzeUrl( + ruleUrl = nextUrl, + book = book, + headerMapF = bookSource.getHeaderMap() + ).getResponseAwait() + .body?.let { nextBody -> + chapterData = analyzeChapterList( + nextBody, nextUrl, tocRule, listRule, + book, bookSource, log = false + ) + nextUrl = if (chapterData.nextUrl.isNotEmpty()) { + chapterData.nextUrl[0] + } else "" + chapterData.chapterList?.let { + chapterList.addAll(it) + } + } + } + Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") + block.resume(finish(book, chapterList, reverse)) + }.onError { + block.resumeWithException(it) + } + } + else -> { + val chapterDataList = arrayListOf>() + for (item in chapterData.nextUrl) { + if (!nextUrlList.contains(item)) { + val data = ChapterData(nextUrl = item) + chapterDataList.add(data) + nextUrlList.add(item) + } + } + Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") + for (item in chapterDataList) { + Coroutine.async(scope = coroutineScope) { + val nextBody = AnalyzeUrl( + ruleUrl = item.nextUrl, + book = book, + headerMapF = bookSource.getHeaderMap() + ).getResponseAwait().body + val nextChapterData = analyzeChapterList( + nextBody, item.nextUrl, tocRule, listRule, book, bookSource + ) + synchronized(chapterDataList) { + val isFinished = addChapterListIsFinish( + chapterDataList, + item, + nextChapterData.chapterList + ) + if (isFinished) { + chapterDataList.forEach { item -> + item.chapterList?.let { + chapterList.addAll(it) + } + } + block.resume(finish(book, chapterList, reverse)) + } + } + }.onError { + block.resumeWithException(it) + } + } + } + } + } catch (e: Exception) { + block.resumeWithException(e) + } + } + + private fun addChapterListIsFinish( + chapterDataList: ArrayList>, + chapterData: ChapterData, + chapterList: List? + ): Boolean { + chapterData.chapterList = chapterList + chapterDataList.forEach { + if (it.chapterList == null) { + return false + } + } + return true + } + + private fun finish( + book: Book, + chapterList: ArrayList, + reverse: Boolean + ): ArrayList { + //去重 + if (!reverse) { + chapterList.reverse() + } + val lh = LinkedHashSet(chapterList) + val list = ArrayList(lh) + list.reverse() + Debug.log(book.origin, "◇目录总数:${list.size}") + for ((index, item) in list.withIndex()) { + item.index = index + } + book.latestChapterTitle = list.last().title + book.durChapterTitle = + list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle + if (book.totalChapterNum < list.size) { + book.lastCheckCount = list.size - book.totalChapterNum + } + book.totalChapterNum = list.size + return list + } + + private fun analyzeChapterList( + body: String?, + baseUrl: String, + tocRule: TocRule, + listRule: String, + book: Book, + bookSource: BookSource, + getNextUrl: Boolean = true, + log: Boolean = false + ): ChapterData> { + val chapterList = arrayListOf() + val nextUrlList = arrayListOf() + val analyzeRule = AnalyzeRule(book) + analyzeRule.setContent(body, baseUrl) + val nextTocRule = tocRule.nextTocUrl + if (getNextUrl && !nextTocRule.isNullOrEmpty()) { + Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) + analyzeRule.getStringList(nextTocRule, true)?.let { + for (item in it) { + if (item != baseUrl) { + nextUrlList.add(item) + } + } + } + Debug.log( + bookSource.bookSourceUrl, + "└" + TextUtils.join(",\n", nextUrlList), + log + ) + } + Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) + val elements = analyzeRule.getElements(listRule) + Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) + if (elements.isNotEmpty()) { + Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) + val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) + val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) + val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) + val update = analyzeRule.splitSourceRule(tocRule.updateTime) + var isVip: String? + for (item in elements) { + analyzeRule.setContent(item) + val bookChapter = BookChapter(bookUrl = book.bookUrl) + analyzeRule.chapter = bookChapter + bookChapter.title = analyzeRule.getString(nameRule) + bookChapter.url = analyzeRule.getString(urlRule, true) + bookChapter.tag = analyzeRule.getString(update) + isVip = analyzeRule.getString(vipRule) + if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl + if (bookChapter.title.isNotEmpty()) { + if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") { + bookChapter.title = "\uD83D\uDD12" + bookChapter.title + } + chapterList.add(bookChapter) + } + } + Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].title}", log) + Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log) + Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].url}", log) + Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log) + Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].tag}", log) + } + return ChapterData(chapterList, nextUrlList) + } + +} \ No newline at end of file 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 similarity index 97% rename from app/src/main/java/io/legado/app/model/webbook/BookContent.kt rename to app/src/main/java/io/legado/app/model/webBook/BookContent.kt index 43e90c4cb..47396ad9c 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 @@ -1,4 +1,4 @@ -package io.legado.app.model.webbook +package io.legado.app.model.webBook import io.legado.app.App import io.legado.app.R @@ -37,7 +37,7 @@ object BookContent { val nextUrlList = arrayListOf(baseUrl) val contentRule = bookSource.getContentRule() var contentData = analyzeContent(body, contentRule, book, bookChapter, bookSource, baseUrl) - content.append(contentData.content) + content.append(contentData.content.replace(bookChapter.title, "")) if (contentData.nextUrl.size == 1) { var nextUrl = contentData.nextUrl[0] val nextChapterUrl = if (!nextChapterUrlF.isNullOrEmpty()) diff --git a/app/src/main/java/io/legado/app/model/webbook/BookInfo.kt b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt similarity index 93% rename from app/src/main/java/io/legado/app/model/webbook/BookInfo.kt rename to app/src/main/java/io/legado/app/model/webBook/BookInfo.kt index e56c25347..3e25fc9c3 100644 --- a/app/src/main/java/io/legado/app/model/webbook/BookInfo.kt +++ b/app/src/main/java/io/legado/app/model/webBook/BookInfo.kt @@ -1,4 +1,4 @@ -package io.legado.app.model.webbook +package io.legado.app.model.webBook import io.legado.app.App import io.legado.app.R @@ -42,9 +42,11 @@ object BookInfo { } Debug.log(bookSource.bookSourceUrl, "└${book.author}") Debug.log(bookSource.bookSourceUrl, "┌获取分类") - analyzeRule.getString(infoRule.kind).let { - if (it.isNotEmpty()) book.kind = it - } + analyzeRule.getStringList(infoRule.kind) + ?.joinToString(",") + ?.let { + if (it.isNotEmpty()) book.kind = it + } Debug.log(bookSource.bookSourceUrl, "└${book.kind}") Debug.log(bookSource.bookSourceUrl, "┌获取字数") analyzeRule.getString(infoRule.wordCount).let { @@ -68,7 +70,7 @@ object BookInfo { } Debug.log(bookSource.bookSourceUrl, "└${book.coverUrl}") Debug.log(bookSource.bookSourceUrl, "┌获取目录链接") - book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true) ?: baseUrl + book.tocUrl = analyzeRule.getString(infoRule.tocUrl, true) if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl if (book.tocUrl == baseUrl) { book.tocHtml = body 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 similarity index 97% rename from app/src/main/java/io/legado/app/model/webbook/BookList.kt rename to app/src/main/java/io/legado/app/model/webBook/BookList.kt index ed440b9f2..a205c950a 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 @@ -1,4 +1,4 @@ -package io.legado.app.model.webbook +package io.legado.app.model.webBook import io.legado.app.App import io.legado.app.R @@ -120,7 +120,7 @@ object BookList { searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(author)) Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}") Debug.log(bookSource.bookSourceUrl, "┌获取分类") - searchBook.kind = analyzeRule.getString(kind) + searchBook.kind = analyzeRule.getStringList(kind)?.joinToString(",") Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind}") Debug.log(bookSource.bookSourceUrl, "┌获取字数") searchBook.wordCount = analyzeRule.getString(wordCount) @@ -170,7 +170,7 @@ object BookList { searchBook.author = BookHelp.formatAuthor(analyzeRule.getString(ruleAuthor)) Debug.log(bookSource.bookSourceUrl, "└${searchBook.author}", log) Debug.log(bookSource.bookSourceUrl, "┌获取分类", log) - searchBook.kind = analyzeRule.getString(ruleKind) + searchBook.kind = analyzeRule.getStringList(ruleKind)?.joinToString(",") Debug.log(bookSource.bookSourceUrl, "└${searchBook.kind}", log) Debug.log(bookSource.bookSourceUrl, "┌获取字数", log) searchBook.wordCount = analyzeRule.getString(ruleWordCount) diff --git a/app/src/main/java/io/legado/app/model/webbook/ChapterData.kt b/app/src/main/java/io/legado/app/model/webBook/ChapterData.kt similarity index 79% rename from app/src/main/java/io/legado/app/model/webbook/ChapterData.kt rename to app/src/main/java/io/legado/app/model/webBook/ChapterData.kt index bbbf6060b..6b06eb508 100644 --- a/app/src/main/java/io/legado/app/model/webbook/ChapterData.kt +++ b/app/src/main/java/io/legado/app/model/webBook/ChapterData.kt @@ -1,4 +1,4 @@ -package io.legado.app.model.webbook +package io.legado.app.model.webBook import io.legado.app.data.entities.BookChapter diff --git a/app/src/main/java/io/legado/app/model/webbook/ContentData.kt b/app/src/main/java/io/legado/app/model/webBook/ContentData.kt similarity index 67% rename from app/src/main/java/io/legado/app/model/webbook/ContentData.kt rename to app/src/main/java/io/legado/app/model/webBook/ContentData.kt index 195778ce8..65185d806 100644 --- a/app/src/main/java/io/legado/app/model/webbook/ContentData.kt +++ b/app/src/main/java/io/legado/app/model/webBook/ContentData.kt @@ -1,4 +1,4 @@ -package io.legado.app.model.webbook +package io.legado.app.model.webBook data class ContentData( var content: String = "", 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 deleted file mode 100644 index c8fd7c9f8..000000000 --- a/app/src/main/java/io/legado/app/model/webbook/BookChapterList.kt +++ /dev/null @@ -1,176 +0,0 @@ -package io.legado.app.model.webbook - -import android.text.TextUtils -import io.legado.app.App -import io.legado.app.R -import io.legado.app.data.entities.Book -import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.BookSource -import io.legado.app.data.entities.rule.TocRule -import io.legado.app.model.Debug -import io.legado.app.model.analyzeRule.AnalyzeRule -import io.legado.app.model.analyzeRule.AnalyzeUrl -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.withContext - -object BookChapterList { - - suspend fun analyzeChapterList( - coroutineScope: CoroutineScope, - book: Book, - body: String?, - bookSource: BookSource, - baseUrl: String - ): List { - var chapterList = arrayListOf() - body ?: throw Exception( - App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) - ) - Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") - val tocRule = bookSource.getTocRule() - val nextUrlList = arrayListOf(baseUrl) - var reverse = false - var listRule = tocRule.chapterList ?: "" - if (listRule.startsWith("-")) { - reverse = true - listRule = listRule.substring(1) - } - if (listRule.startsWith("+")) { - listRule = listRule.substring(1) - } - var chapterData = - analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true) - chapterData.chapterList?.let { - chapterList.addAll(it) - } - if (chapterData.nextUrl.size == 1) { - var nextUrl = chapterData.nextUrl[0] - while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { - nextUrlList.add(nextUrl) - AnalyzeUrl( - ruleUrl = nextUrl, book = book, headerMapF = bookSource.getHeaderMap() - ).getResponseAwait() - .body?.let { nextBody -> - chapterData = analyzeChapterList( - nextBody, nextUrl, tocRule, listRule, - book, bookSource, log = false - ) - nextUrl = if (chapterData.nextUrl.isNotEmpty()) - chapterData.nextUrl[0] - else "" - chapterData.chapterList?.let { - chapterList.addAll(it) - } - } - } - Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") - } else if (chapterData.nextUrl.size > 1) { - val chapterDataList = arrayListOf>() - for (item in chapterData.nextUrl) { - val data = ChapterData(nextUrl = item) - chapterDataList.add(data) - } - for (item in chapterDataList) { - withContext(coroutineScope.coroutineContext) { - val nextBody = AnalyzeUrl( - ruleUrl = item.nextUrl, - book = book, - headerMapF = bookSource.getHeaderMap() - ).getResponseAwait().body - val nextChapterData = analyzeChapterList( - nextBody, item.nextUrl, tocRule, listRule, book, bookSource - ) - item.chapterList = nextChapterData.chapterList - } - } - for (item in chapterDataList) { - item.chapterList?.let { - chapterList.addAll(it) - } - } - } - //去重 - if (!reverse) { - chapterList.reverse() - } - val lh = LinkedHashSet(chapterList) - chapterList = ArrayList(lh) - chapterList.reverse() - for ((index, item) in chapterList.withIndex()) { - item.index = index - } - book.latestChapterTitle = chapterList.last().title - if (book.totalChapterNum < chapterList.size) { - book.lastCheckCount = chapterList.size - book.totalChapterNum - } - book.totalChapterNum = chapterList.size - return chapterList - } - - - private fun analyzeChapterList( - body: String?, - baseUrl: String, - tocRule: TocRule, - listRule: String, - book: Book, - bookSource: BookSource, - getNextUrl: Boolean = true, - log: Boolean = false - ): ChapterData> { - val chapterList = arrayListOf() - val nextUrlList = arrayListOf() - val analyzeRule = AnalyzeRule(book) - analyzeRule.setContent(body, baseUrl) - val nextTocRule = tocRule.nextTocUrl - if (getNextUrl && !nextTocRule.isNullOrEmpty()) { - Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) - analyzeRule.getStringList(nextTocRule, true)?.let { - for (item in it) { - if (item != baseUrl) { - nextUrlList.add(item) - } - } - } - Debug.log( - bookSource.bookSourceUrl, - "└" + TextUtils.join(",\n", nextUrlList), - log - ) - } - Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) - val elements = analyzeRule.getElements(listRule) - Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) - if (elements.isNotEmpty()) { - Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) - val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) - val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) - val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) - val update = analyzeRule.splitSourceRule(tocRule.updateTime) - var isVip: String? - for (item in elements) { - analyzeRule.setContent(item) - val bookChapter = BookChapter(bookUrl = book.bookUrl) - analyzeRule.chapter = bookChapter - bookChapter.title = analyzeRule.getString(nameRule) - bookChapter.url = analyzeRule.getString(urlRule, true) - bookChapter.tag = analyzeRule.getString(update) - isVip = analyzeRule.getString(vipRule) - if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl - if (bookChapter.title.isNotEmpty()) { - if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") { - bookChapter.title = "\uD83D\uDD12" + bookChapter.title - } - chapterList.add(bookChapter) - } - } - Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].title}", log) - Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log) - Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].url}", log) - Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log) - Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].tag}", log) - } - return ChapterData(chapterList, nextUrlList) - } - -} \ No newline at end of file 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 5cc36a680..12083c652 100644 --- a/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/MediaButtonReceiver.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.view.KeyEvent import io.legado.app.App -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.data.entities.Book import io.legado.app.help.ActivityHelp import io.legado.app.ui.audio.AudioPlayActivity @@ -51,9 +51,9 @@ class MediaButtonReceiver : BroadcastReceiver() { private fun readAloud(context: Context) { when { ActivityHelp.isExist(AudioPlayActivity::class.java) -> - postEvent(Bus.MEDIA_BUTTON, true) + postEvent(EventBus.MEDIA_BUTTON, true) ActivityHelp.isExist(ReadBookActivity::class.java) -> - postEvent(Bus.MEDIA_BUTTON, true) + postEvent(EventBus.MEDIA_BUTTON, true) else -> { GlobalScope.launch(Main) { val lastBook: Book? = withContext(IO) { diff --git a/app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt b/app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt index 613db4fe4..765d05b3e 100644 --- a/app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt +++ b/app/src/main/java/io/legado/app/receiver/TimeElectricityReceiver.kt @@ -5,7 +5,7 @@ import android.content.Context import android.content.Intent import android.content.IntentFilter import android.os.BatteryManager -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.utils.postEvent @@ -28,11 +28,11 @@ class TimeElectricityReceiver : BroadcastReceiver() { intent?.action?.let { when (it) { Intent.ACTION_TIME_TICK -> { - postEvent(Bus.TIME_CHANGED, "") + postEvent(EventBus.TIME_CHANGED, "") } Intent.ACTION_BATTERY_CHANGED -> { val level = intent.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) - postEvent(Bus.BATTERY_CHANGED, level) + postEvent(EventBus.BATTERY_CHANGED, level) } } } diff --git a/app/src/main/java/io/legado/app/service/AudioPlayService.kt b/app/src/main/java/io/legado/app/service/AudioPlayService.kt index ef2638c29..c165f3142 100644 --- a/app/src/main/java/io/legado/app/service/AudioPlayService.kt +++ b/app/src/main/java/io/legado/app/service/AudioPlayService.kt @@ -18,9 +18,9 @@ import androidx.core.app.NotificationCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService -import io.legado.app.constant.Action +import io.legado.app.constant.IntentAction import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.Status import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp @@ -81,21 +81,21 @@ class AudioPlayService : BaseService(), override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.action?.let { action -> when (action) { - Action.play -> { + IntentAction.play -> { AudioPlay.book?.let { title = it.name position = it.durChapterPos loadContent(it.durChapterIndex) } } - Action.pause -> pause(true) - Action.resume -> resume() - Action.prev -> moveToPrev() - Action.next -> moveToNext() - Action.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) - Action.addTimer -> addTimer() - Action.setTimer -> setTimer(intent.getIntExtra("minute", 0)) - Action.adjustProgress -> adjustProgress(intent.getIntExtra("position", position)) + IntentAction.pause -> pause(true) + IntentAction.resume -> resume() + IntentAction.prev -> moveToPrev() + IntentAction.next -> moveToNext() + IntentAction.adjustSpeed -> upSpeed(intent.getFloatExtra("adjust", 1f)) + IntentAction.addTimer -> addTimer() + IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) + IntentAction.adjustProgress -> adjustProgress(intent.getIntExtra("position", position)) else -> stopSelf() } } @@ -112,7 +112,7 @@ class AudioPlayService : BaseService(), unregisterReceiver(broadcastReceiver) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) AudioPlay.status = Status.STOP - postEvent(Bus.AUDIO_STATE, Status.STOP) + postEvent(EventBus.AUDIO_STATE, Status.STOP) } private fun play() { @@ -120,7 +120,7 @@ class AudioPlayService : BaseService(), if (requestFocus()) { try { AudioPlay.status = Status.PLAY - postEvent(Bus.AUDIO_STATE, Status.PLAY) + postEvent(EventBus.AUDIO_STATE, Status.PLAY) mediaPlayer.reset() val analyzeUrl = AnalyzeUrl(url, headerMapF = AudioPlay.headers(), useWebView = true) @@ -146,7 +146,7 @@ class AudioPlayService : BaseService(), mediaPlayer.pause() upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) AudioPlay.status = Status.PAUSE - postEvent(Bus.AUDIO_STATE, Status.PAUSE) + postEvent(EventBus.AUDIO_STATE, Status.PAUSE) upNotification() } } @@ -159,7 +159,7 @@ class AudioPlayService : BaseService(), handler.postDelayed(mpRunnable, 1000) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PLAYING) AudioPlay.status = Status.PLAY - postEvent(Bus.AUDIO_STATE, Status.PLAY) + postEvent(EventBus.AUDIO_STATE, Status.PLAY) upNotification() } @@ -178,7 +178,7 @@ class AudioPlayService : BaseService(), if (isPlaying) { playbackParams = playbackParams.apply { speed += adjust } } - postEvent(Bus.AUDIO_SPEED, playbackParams.speed) + postEvent(EventBus.AUDIO_SPEED, playbackParams.speed) } } } @@ -191,7 +191,7 @@ class AudioPlayService : BaseService(), if (pause) return mediaPlayer.start() mediaPlayer.seekTo(position) - postEvent(Bus.AUDIO_SIZE, mediaPlayer.duration) + postEvent(EventBus.AUDIO_SIZE, mediaPlayer.duration) bookChapter?.let { it.end = mediaPlayer.duration.toLong() } @@ -205,7 +205,7 @@ class AudioPlayService : BaseService(), override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { if (!mediaPlayer.isPlaying) { AudioPlay.status = Status.STOP - postEvent(Bus.AUDIO_STATE, Status.STOP) + postEvent(EventBus.AUDIO_STATE, Status.STOP) launch { toast("error: $what $extra $url") } } return true @@ -238,7 +238,7 @@ class AudioPlayService : BaseService(), handler.removeCallbacks(dsRunnable) handler.postDelayed(dsRunnable, 60000) } - postEvent(Bus.TTS_DS, timeMinute) + postEvent(EventBus.TTS_DS, timeMinute) upNotification() } @@ -247,7 +247,7 @@ class AudioPlayService : BaseService(), */ private fun upPlayProgress() { saveProgress() - postEvent(Bus.AUDIO_PROGRESS, mediaPlayer.currentPosition) + postEvent(EventBus.AUDIO_PROGRESS, mediaPlayer.currentPosition) handler.postDelayed(mpRunnable, 1000) } @@ -260,9 +260,9 @@ class AudioPlayService : BaseService(), if (index == AudioPlay.durChapterIndex) { bookChapter = chapter subtitle = chapter.title - postEvent(Bus.AUDIO_SUB_TITLE, subtitle) - postEvent(Bus.AUDIO_SIZE, chapter.end?.toInt() ?: 0) - postEvent(Bus.AUDIO_PROGRESS, position) + postEvent(EventBus.AUDIO_SUB_TITLE, subtitle) + postEvent(EventBus.AUDIO_SIZE, chapter.end?.toInt() ?: 0) + postEvent(EventBus.AUDIO_PROGRESS, position) } loadContent(chapter) } ?: removeLoading(index) @@ -376,7 +376,7 @@ class AudioPlayService : BaseService(), handler.postDelayed(dsRunnable, 60000) } } - postEvent(Bus.TTS_DS, timeMinute) + postEvent(EventBus.TTS_DS, timeMinute) upNotification() } @@ -485,24 +485,24 @@ class AudioPlayService : BaseService(), builder.addAction( R.drawable.ic_play_24dp, getString(R.string.resume), - thisPendingIntent(Action.resume) + thisPendingIntent(IntentAction.resume) ) } else { builder.addAction( R.drawable.ic_pause_24dp, getString(R.string.pause), - thisPendingIntent(Action.pause) + thisPendingIntent(IntentAction.pause) ) } builder.addAction( R.drawable.ic_stop_black_24dp, getString(R.string.stop), - thisPendingIntent(Action.stop) + thisPendingIntent(IntentAction.stop) ) builder.addAction( R.drawable.ic_time_add_24dp, getString(R.string.set_timer), - thisPendingIntent(Action.addTimer) + thisPendingIntent(IntentAction.addTimer) ) builder.setStyle( androidx.media.app.NotificationCompat.MediaStyle() 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 c657f621e..e1e3f8620 100644 --- a/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/BaseReadAloudService.kt @@ -16,17 +16,14 @@ import androidx.core.app.NotificationCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService -import io.legado.app.constant.Action -import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus -import io.legado.app.constant.Status +import io.legado.app.constant.* import io.legado.app.help.IntentDataHelp import io.legado.app.help.IntentHelp import io.legado.app.help.MediaHelp import io.legado.app.receiver.MediaButtonReceiver import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.ReadBookActivity -import io.legado.app.ui.book.read.page.TextChapter +import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent @@ -43,18 +40,18 @@ abstract class BaseReadAloudService : BaseService(), } } - private val handler = Handler() + internal val handler = Handler() private lateinit var audioManager: AudioManager private var mFocusRequest: AudioFocusRequest? = null private var broadcastReceiver: BroadcastReceiver? = null private var mediaSessionCompat: MediaSessionCompat? = null private var title: String = "" private var subtitle: String = "" - val contentList = arrayListOf() - var nowSpeak: Int = 0 - var readAloudNumber: Int = 0 - var textChapter: TextChapter? = null - var pageIndex = 0 + internal val contentList = arrayListOf() + internal var nowSpeak: Int = 0 + internal var readAloudNumber: Int = 0 + internal var textChapter: TextChapter? = null + internal var pageIndex = 0 private val dsRunnable: Runnable = Runnable { doDs() } override fun onCreate() { @@ -73,7 +70,7 @@ abstract class BaseReadAloudService : BaseService(), isRun = false pause = true unregisterReceiver(broadcastReceiver) - postEvent(Bus.ALOUD_STATE, Status.STOP) + postEvent(EventBus.ALOUD_STATE, Status.STOP) upMediaSessionPlaybackState(PlaybackStateCompat.STATE_STOPPED) mediaSessionCompat?.release() } @@ -81,7 +78,7 @@ abstract class BaseReadAloudService : BaseService(), override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.action?.let { action -> when (action) { - Action.play -> { + IntentAction.play -> { title = intent.getStringExtra("title") ?: "" subtitle = intent.getStringExtra("subtitle") ?: "" pageIndex = intent.getIntExtra("pageIndex", 0) @@ -90,13 +87,13 @@ abstract class BaseReadAloudService : BaseService(), intent.getBooleanExtra("play", true) ) } - Action.pause -> pauseReadAloud(true) - Action.resume -> resumeReadAloud() - Action.upTtsSpeechRate -> upSpeechRate(true) - Action.prevParagraph -> prevP() - Action.nextParagraph -> nextP() - Action.addTimer -> addTimer() - Action.setTimer -> setTimer(intent.getIntExtra("minute", 0)) + IntentAction.pause -> pauseReadAloud(true) + IntentAction.resume -> resumeReadAloud() + IntentAction.upTtsSpeechRate -> upSpeechRate(true) + IntentAction.prevParagraph -> prevP() + IntentAction.nextParagraph -> nextP() + IntentAction.addTimer -> addTimer() + IntentAction.setTimer -> setTimer(intent.getIntExtra("minute", 0)) else -> stopSelf() } } @@ -111,7 +108,7 @@ abstract class BaseReadAloudService : BaseService(), nowSpeak = 0 readAloudNumber = textChapter.getReadLength(pageIndex) contentList.clear() - if (getPrefBoolean("readAloudByPage")) { + if (getPrefBoolean(PreferKey.readAloudByPage)) { for (index in pageIndex..textChapter.lastIndex()) { textChapter.page(index)?.text?.split("\n")?.let { contentList.addAll(it) @@ -127,13 +124,13 @@ abstract class BaseReadAloudService : BaseService(), open fun play() { pause = false - postEvent(Bus.ALOUD_STATE, Status.PLAY) + postEvent(EventBus.ALOUD_STATE, Status.PLAY) upNotification() } @CallSuper open fun pauseReadAloud(pause: Boolean) { - postEvent(Bus.ALOUD_STATE, Status.PAUSE) + postEvent(EventBus.ALOUD_STATE, Status.PAUSE) BaseReadAloudService.pause = pause upNotification() upMediaSessionPlaybackState(PlaybackStateCompat.STATE_PAUSED) @@ -170,7 +167,7 @@ abstract class BaseReadAloudService : BaseService(), handler.removeCallbacks(dsRunnable) handler.postDelayed(dsRunnable, 60000) } - postEvent(Bus.TTS_DS, timeMinute) + postEvent(EventBus.TTS_DS, timeMinute) upNotification() } @@ -186,7 +183,7 @@ abstract class BaseReadAloudService : BaseService(), handler.postDelayed(dsRunnable, 60000) } } - postEvent(Bus.TTS_DS, timeMinute) + postEvent(EventBus.TTS_DS, timeMinute) upNotification() } @@ -301,24 +298,24 @@ abstract class BaseReadAloudService : BaseService(), builder.addAction( R.drawable.ic_play_24dp, getString(R.string.resume), - aloudServicePendingIntent(Action.resume) + aloudServicePendingIntent(IntentAction.resume) ) } else { builder.addAction( R.drawable.ic_pause_24dp, getString(R.string.pause), - aloudServicePendingIntent(Action.pause) + aloudServicePendingIntent(IntentAction.pause) ) } builder.addAction( R.drawable.ic_stop_black_24dp, getString(R.string.stop), - aloudServicePendingIntent(Action.stop) + aloudServicePendingIntent(IntentAction.stop) ) builder.addAction( R.drawable.ic_time_add_24dp, getString(R.string.set_timer), - aloudServicePendingIntent(Action.addTimer) + aloudServicePendingIntent(IntentAction.addTimer) ) builder.setStyle( androidx.media.app.NotificationCompat.MediaStyle() 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 5fa36c1ba..18f704a6e 100644 --- a/app/src/main/java/io/legado/app/service/CheckSourceService.kt +++ b/app/src/main/java/io/legado/app/service/CheckSourceService.kt @@ -2,28 +2,77 @@ package io.legado.app.service import android.content.Intent import androidx.core.app.NotificationCompat +import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService -import io.legado.app.constant.Action import io.legado.app.constant.AppConst -import io.legado.app.data.entities.BookSource +import io.legado.app.constant.IntentAction +import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp +import io.legado.app.help.coroutine.Coroutine +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 java.util.concurrent.Executors class CheckSourceService : BaseService() { - - private var sourceList: List? = null + private var searchPool = + Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() + private var task: Coroutine<*>? = null + private var idsCount = 0 + private val unCheckIds = LinkedHashSet() override fun onCreate() { super.onCreate() + updateNotification(0, getString(R.string.start)) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + when (intent?.action) { + IntentAction.start -> intent.getStringArrayListExtra("selectIds")?.let { + check(it) + } + else -> stopSelf() + } return super.onStartCommand(intent, flags, startId) } override fun onDestroy() { super.onDestroy() + task?.cancel() + searchPool.close() + } + + private fun check(ids: List) { + task?.cancel() + unCheckIds.clear() + idsCount = ids.size + unCheckIds.addAll(ids) + updateNotification(0, getString(R.string.progress_show, 0, idsCount)) + task = execute { + unCheckIds.forEach { sourceUrl -> + 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 { + unCheckIds.remove(sourceUrl) + val checkedCount = idsCount - unCheckIds.size + updateNotification( + checkedCount, + getString(R.string.progress_show, checkedCount, idsCount) + ) + } + } + } + } + + task?.invokeOnCompletion { + stopSelf() + } } /** @@ -41,11 +90,9 @@ class CheckSourceService : BaseService() { .addAction( R.drawable.ic_stop_black_24dp, getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, Action.stop) + IntentHelp.servicePendingIntent(this, IntentAction.stop) ) - sourceList?.let { - builder.setProgress(it.size, state, false) - } + builder.setProgress(idsCount, state, false) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) val notification = builder.build() startForeground(112202, notification) 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 d8bfae260..b5b983476 100644 --- a/app/src/main/java/io/legado/app/service/DownloadService.kt +++ b/app/src/main/java/io/legado/app/service/DownloadService.kt @@ -6,9 +6,11 @@ import androidx.core.app.NotificationCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService -import io.legado.app.constant.Action import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus +import io.legado.app.constant.IntentAction +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.Coroutine @@ -16,13 +18,18 @@ import io.legado.app.model.WebBook import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.isActive +import org.jetbrains.anko.toast import java.util.concurrent.Executors class DownloadService : BaseService() { - private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() + private var searchPool = + Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() private var tasks: ArrayList> = arrayListOf() private val handler = Handler() private var runnable: Runnable = Runnable { upDownload() } + private val downloadMap = hashMapOf>() + private val finalMap = hashMapOf>() private var notificationContent = "正在启动下载" private val notificationBuilder by lazy { val builder = NotificationCompat.Builder(this, AppConst.channelIdDownload) @@ -32,7 +39,7 @@ class DownloadService : BaseService() { builder.addAction( R.drawable.ic_stop_black_24dp, getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, Action.stop) + IntentHelp.servicePendingIntent(this, IntentAction.stop) ) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) } @@ -46,12 +53,13 @@ class DownloadService : BaseService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { intent?.action?.let { action -> when (action) { - Action.start -> download( + IntentAction.start -> addDownloadData( intent.getStringExtra("bookUrl"), intent.getIntExtra("start", 0), intent.getIntExtra("end", 0) ) - Action.stop -> stopDownload() + IntentAction.remove -> removeDownload(intent.getStringExtra("bookUrl")) + IntentAction.stop -> stopDownload() } } return super.onStartCommand(intent, flags, startId) @@ -61,32 +69,83 @@ class DownloadService : BaseService() { tasks.clear() searchPool.close() handler.removeCallbacks(runnable) + downloadMap.clear() + finalMap.clear() super.onDestroy() - postEvent(Bus.UP_DOWNLOAD, false) + postEvent(EventBus.UP_DOWNLOAD, downloadMap) } - private fun download(bookUrl: String?, start: Int, end: Int) { - if (bookUrl == null) return - val task = Coroutine.async(this) { - val book = App.db.bookDao().getBook(bookUrl) ?: return@async - val bookSource = App.db.bookSourceDao().getBookSource(book.origin) ?: return@async - val webBook = WebBook(bookSource) - for (index in start..end) { - App.db.bookChapterDao().getChapter(bookUrl, index)?.let { chapter -> - if (!BookHelp.hasContent(book, chapter)) { - webBook.getContent(book, chapter, scope = this, context = searchPool) - .onStart { - notificationContent = chapter.title - } - .onSuccess(IO) { content -> - content?.let { - BookHelp.saveContent(book, chapter, content) - } + private fun addDownloadData(bookUrl: String?, start: Int, end: Int) { + bookUrl ?: return + if (downloadMap.containsKey(bookUrl)) { + toast("该书已在下载列表") + return + } + execute { + val chapterMap = downloadMap[bookUrl] ?: linkedSetOf().apply { + downloadMap[bookUrl] = this + } + App.db.bookChapterDao().getChapterList(bookUrl, start, end).let { + chapterMap.addAll(it) + } + download() + } + } + + private fun removeDownload(bookUrl: String?) { + downloadMap.remove(bookUrl) + finalMap.remove(bookUrl) + } + + private fun download() { + val task = Coroutine.async(this, context = searchPool) { + downloadMap.forEach { entry -> + if (!isActive) return@async + if (!finalMap.containsKey(entry.key)) { + val book = App.db.bookDao().getBook(entry.key) ?: return@async + val bookSource = + App.db.bookSourceDao().getBookSource(book.origin) ?: return@async + val webBook = WebBook(bookSource) + entry.value.forEach { chapter -> + if (!isActive) return@async + if (downloadMap.containsKey(book.bookUrl)) { + if (!BookHelp.hasContent(book, chapter)) { + webBook.getContent( + book, + chapter, + scope = this, + context = searchPool + ) + .onStart { + notificationContent = chapter.title + } + .onSuccess(IO) { content -> + content?.let { + BookHelp.saveContent(book, chapter, content) + } + } + .onFinally(IO) { + synchronized(this@DownloadService) { + val chapterMap = + finalMap[book.bookUrl] + ?: linkedSetOf().apply { + finalMap[book.bookUrl] = this + } + chapterMap.add(chapter) + if (chapterMap.size == entry.value.size) { + downloadMap.remove(book.bookUrl) + finalMap.remove(book.bookUrl) + } + } + } } + } } } + } } + tasks.add(task) task.invokeOnCompletion { tasks.remove(task) @@ -103,7 +162,7 @@ class DownloadService : BaseService() { private fun upDownload() { updateNotification(notificationContent) - postEvent(Bus.UP_DOWNLOAD, true) + postEvent(EventBus.UP_DOWNLOAD, downloadMap) handler.removeCallbacks(runnable) handler.postDelayed(runnable, 1000) } 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 c2467c1eb..2cf46831c 100644 --- a/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/HttpReadAloudService.kt @@ -2,15 +2,14 @@ package io.legado.app.service import android.app.PendingIntent import android.media.MediaPlayer -import io.legado.app.constant.Bus -import io.legado.app.data.api.IHttpPostApi -import io.legado.app.help.FileHelp +import io.legado.app.constant.EventBus +import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp import io.legado.app.help.http.HttpHelper +import io.legado.app.help.http.api.HttpPostApi import io.legado.app.service.help.ReadBook +import io.legado.app.utils.FileUtils import io.legado.app.utils.LogUtils -import io.legado.app.utils.getPrefInt -import io.legado.app.utils.getPrefString import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Job @@ -41,6 +40,7 @@ class HttpReadAloudService : BaseReadAloudService(), override fun onDestroy() { super.onDestroy() + job?.cancel() mediaPlayer.release() } @@ -65,16 +65,15 @@ class HttpReadAloudService : BaseReadAloudService(), private fun downloadAudio() { job = launch(IO) { - FileHelp.deleteFile(ttsFolder) + FileUtils.deleteFile(ttsFolder) for (index in 0 until contentList.size) { if (isActive) { val bytes = HttpHelper.getByteRetrofit("http://tts.baidu.com") - .create(IHttpPostApi::class.java) + .create(HttpPostApi::class.java) .postMapByteAsync( "http://tts.baidu.com/text2audio", getAudioBody(contentList[index]), mapOf() - ).await() - .body() + ).body() if (bytes != null && isActive) { val file = getSpeakFile(index) file.writeBytes(bytes) @@ -92,11 +91,12 @@ class HttpReadAloudService : BaseReadAloudService(), @Synchronized private fun playAudio(fd: FileDescriptor) { if (playingIndex != nowSpeak && requestFocus()) { - playingIndex = nowSpeak try { mediaPlayer.reset() mediaPlayer.setDataSource(fd) mediaPlayer.prepareAsync() + playingIndex = nowSpeak + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } catch (e: Exception) { e.printStackTrace() } @@ -104,14 +104,14 @@ class HttpReadAloudService : BaseReadAloudService(), } private fun getSpeakFile(index: Int = nowSpeak): File { - return FileHelp.getFile("${ttsFolder}${File.separator}${index}.mp3") + return FileUtils.createFileIfNotExist("${ttsFolder}${File.separator}${index}.mp3") } private fun getAudioBody(content: String): Map { return mapOf( Pair("tex", encodeTwo(content)), - Pair("spd", ((getPrefInt("ttsSpeechRate", 25) + 5) / 5).toString()), - Pair("per", getPrefString("ttsSpeechPer") ?: "0"), + Pair("spd", ((AppConfig.ttsSpeechRate + 5) / 10 + 4).toString()), + Pair("per", AppConfig.ttsSpeechPer), Pair("cuid", "baidu_speech_demo"), Pair("idx", "1"), Pair("cod", "2"), @@ -139,20 +139,26 @@ class HttpReadAloudService : BaseReadAloudService(), override fun resumeReadAloud() { super.resumeReadAloud() - mediaPlayer.start() + if (playingIndex == -1) { + play() + } else { + mediaPlayer.start() + } } + /** + * 更新朗读速度 + */ override fun upSpeechRate(reset: Boolean) { job?.cancel() - mediaPlayer.reset() - for (i in 0 until nowSpeak) { - contentList.removeAt(0) - } - nowSpeak = 0 + mediaPlayer.stop() playingIndex = -1 - play() + downloadAudio() } + /** + * 上一段 + */ override fun prevP() { if (nowSpeak > 0) { mediaPlayer.stop() @@ -162,6 +168,9 @@ class HttpReadAloudService : BaseReadAloudService(), } } + /** + * 下一段 + */ override fun nextP() { if (nowSpeak < contentList.size - 1) { mediaPlayer.stop() @@ -181,15 +190,27 @@ class HttpReadAloudService : BaseReadAloudService(), ReadBook.moveToNextPage() } } - postEvent(Bus.TTS_START, readAloudNumber + 1) + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } override fun onError(mp: MediaPlayer?, what: Int, extra: Int): Boolean { + LogUtils.d("mp", "what:$what extra:$extra") + if (what == -38 && extra == 0) { + return true + } + handler.postDelayed({ + readAloudNumber += contentList[nowSpeak].length + 1 + if (nowSpeak < contentList.lastIndex) { + nowSpeak++ + play() + } else { + nextChapter() + } + }, 1000) return true } override fun onCompletion(mp: MediaPlayer?) { - LogUtils.d("播放完成", contentList[nowSpeak]) readAloudNumber += contentList[nowSpeak].length + 1 if (nowSpeak < contentList.lastIndex) { nowSpeak++ diff --git a/app/src/main/java/io/legado/app/service/README.md b/app/src/main/java/io/legado/app/service/README.md index 0b66f70aa..64c4ca44c 100644 --- a/app/src/main/java/io/legado/app/service/README.md +++ b/app/src/main/java/io/legado/app/service/README.md @@ -1 +1,7 @@ -## android服务 \ No newline at end of file +## android服务 +* AudioPlayService 音频播放服务 +* CheckSourceService 书源检测服务 +* DownloadService 缓存服务 +* HttpReadAloudService 在线朗读服务 +* TTSReadAloudService tts朗读服务 +* WebService web服务 \ 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 977e8b0ac..6fd32219d 100644 --- a/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt +++ b/app/src/main/java/io/legado/app/service/TTSReadAloudService.kt @@ -6,12 +6,12 @@ import android.speech.tts.TextToSpeech import android.speech.tts.UtteranceProgressListener import io.legado.app.R import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus +import io.legado.app.help.AppConfig import io.legado.app.help.IntentHelp import io.legado.app.help.MediaHelp import io.legado.app.service.help.ReadBook import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.getPrefInt import io.legado.app.utils.postEvent import kotlinx.coroutines.launch import org.jetbrains.anko.toast @@ -96,7 +96,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener textToSpeech = TextToSpeech(this, this) } } else { - textToSpeech?.setSpeechRate((this.getPrefInt("ttsSpeechRate", 5) + 5) / 10f) + textToSpeech?.setSpeechRate((AppConfig.ttsSpeechRate + 5) / 10f) } } @@ -152,7 +152,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener ReadBook.moveToNextPage() } } - postEvent(Bus.TTS_START, readAloudNumber + 1) + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + 1) } override fun onDone(s: String) { @@ -169,7 +169,7 @@ class TTSReadAloudService : BaseReadAloudService(), TextToSpeech.OnInitListener if (readAloudNumber + start > it.getReadLength(pageIndex + 1)) { pageIndex++ ReadBook.moveToNextPage() - postEvent(Bus.TTS_START, readAloudNumber + start) + postEvent(EventBus.TTS_PROGRESS, readAloudNumber + start) } } } diff --git a/app/src/main/java/io/legado/app/service/WebService.kt b/app/src/main/java/io/legado/app/service/WebService.kt index 25f2b6822..e9d223956 100644 --- a/app/src/main/java/io/legado/app/service/WebService.kt +++ b/app/src/main/java/io/legado/app/service/WebService.kt @@ -6,9 +6,9 @@ import androidx.core.app.NotificationCompat import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseService -import io.legado.app.constant.Action +import io.legado.app.constant.IntentAction import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.help.IntentHelp import io.legado.app.utils.NetworkUtils import io.legado.app.utils.getPrefInt @@ -32,7 +32,7 @@ class WebService : BaseService() { fun stop(context: Context) { if (isRun) { val intent = Intent(context, WebService::class.java) - intent.action = Action.stop + intent.action = IntentAction.stop context.startService(intent) } } @@ -56,12 +56,12 @@ class WebService : BaseService() { if (webSocketServer?.isAlive == true) { webSocketServer?.stop() } - postEvent(Bus.WEB_SERVICE_STOP, true) + postEvent(EventBus.WEB_SERVICE_STOP, true) } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { when (intent?.action) { - Action.stop -> stopSelf() + IntentAction.stop -> stopSelf() else -> upWebServer() } return super.onStartCommand(intent, flags, startId) @@ -115,7 +115,7 @@ class WebService : BaseService() { builder.addAction( R.drawable.ic_stop_black_24dp, getString(R.string.cancel), - IntentHelp.servicePendingIntent(this, Action.stop) + IntentHelp.servicePendingIntent(this, IntentAction.stop) ) builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC) val notification = builder.build() diff --git a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt index add1a3fab..7c8d120b3 100644 --- a/app/src/main/java/io/legado/app/service/help/AudioPlay.kt +++ b/app/src/main/java/io/legado/app/service/help/AudioPlay.kt @@ -3,7 +3,7 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent import androidx.lifecycle.MutableLiveData -import io.legado.app.constant.Action +import io.legado.app.constant.IntentAction import io.legado.app.constant.Status import io.legado.app.data.entities.Book import io.legado.app.model.WebBook @@ -27,14 +27,14 @@ object AudioPlay { fun play(context: Context) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.play + intent.action = IntentAction.play context.startService(intent) } fun pause(context: Context) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.pause + intent.action = IntentAction.pause context.startService(intent) } } @@ -42,7 +42,7 @@ object AudioPlay { fun resume(context: Context) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.resume + intent.action = IntentAction.resume context.startService(intent) } } @@ -50,7 +50,7 @@ object AudioPlay { fun stop(context: Context) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.stop + intent.action = IntentAction.stop context.startService(intent) } } @@ -58,7 +58,7 @@ object AudioPlay { fun adjustSpeed(context: Context, adjust: Float) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.adjustSpeed + intent.action = IntentAction.adjustSpeed intent.putExtra("adjust", adjust) context.startService(intent) } @@ -67,7 +67,7 @@ object AudioPlay { fun adjustProgress(context: Context, position: Int) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.adjustProgress + intent.action = IntentAction.adjustProgress intent.putExtra("position", position) context.startService(intent) } @@ -76,7 +76,7 @@ object AudioPlay { fun prev(context: Context) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.prev + intent.action = IntentAction.prev context.startService(intent) } } @@ -84,7 +84,7 @@ object AudioPlay { fun next(context: Context) { if (AudioPlayService.isRun) { val intent = Intent(context, AudioPlayService::class.java) - intent.action = Action.next + intent.action = IntentAction.next context.startService(intent) } } 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 new file mode 100644 index 000000000..1f13723a2 --- /dev/null +++ b/app/src/main/java/io/legado/app/service/help/CheckSource.kt @@ -0,0 +1,35 @@ +package io.legado.app.service.help + +import android.content.Context +import android.content.Intent +import io.legado.app.R +import io.legado.app.constant.IntentAction +import io.legado.app.data.entities.BookSource +import io.legado.app.service.CheckSourceService +import org.jetbrains.anko.toast + +object CheckSource { + + fun start(context: Context, sources: LinkedHashSet) { + 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 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/Download.kt b/app/src/main/java/io/legado/app/service/help/Download.kt index 1c17a7ee1..9f93dc358 100644 --- a/app/src/main/java/io/legado/app/service/help/Download.kt +++ b/app/src/main/java/io/legado/app/service/help/Download.kt @@ -2,14 +2,14 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent -import io.legado.app.constant.Action +import io.legado.app.constant.IntentAction import io.legado.app.service.DownloadService object Download { fun start(context: Context, bookUrl: String, start: Int, end: Int) { Intent(context, DownloadService::class.java).let { - it.action = Action.start + it.action = IntentAction.start it.putExtra("bookUrl", bookUrl) it.putExtra("start", start) it.putExtra("end", end) @@ -17,4 +17,19 @@ object Download { } } + fun remove(context: Context, bookUrl: String) { + Intent(context, DownloadService::class.java).let { + it.action = IntentAction.remove + it.putExtra("bookUrl", bookUrl) + context.startService(it) + } + } + + fun stop(context: Context) { + Intent(context, DownloadService::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/ReadAloud.kt b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt index d2a789a5a..b5c12a965 100644 --- a/app/src/main/java/io/legado/app/service/help/ReadAloud.kt +++ b/app/src/main/java/io/legado/app/service/help/ReadAloud.kt @@ -3,7 +3,8 @@ package io.legado.app.service.help import android.content.Context import android.content.Intent import io.legado.app.App -import io.legado.app.constant.Action +import io.legado.app.constant.IntentAction +import io.legado.app.constant.PreferKey import io.legado.app.service.BaseReadAloudService import io.legado.app.service.HttpReadAloudService import io.legado.app.service.TTSReadAloudService @@ -13,7 +14,7 @@ object ReadAloud { var aloudClass: Class<*> = getReadAloudClass() fun getReadAloudClass(): Class<*> { - return if (App.INSTANCE.getPrefBoolean("readAloudOnLine")) { + return if (App.INSTANCE.getPrefBoolean(PreferKey.readAloudOnLine)) { HttpReadAloudService::class.java } else { TTSReadAloudService::class.java @@ -29,7 +30,7 @@ object ReadAloud { play: Boolean = true ) { val intent = Intent(context, aloudClass) - intent.action = Action.play + intent.action = IntentAction.play intent.putExtra("title", title) intent.putExtra("subtitle", subtitle) intent.putExtra("pageIndex", pageIndex) @@ -41,7 +42,7 @@ object ReadAloud { fun pause(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.pause + intent.action = IntentAction.pause context.startService(intent) } } @@ -49,7 +50,7 @@ object ReadAloud { fun resume(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.resume + intent.action = IntentAction.resume context.startService(intent) } } @@ -57,7 +58,7 @@ object ReadAloud { fun stop(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.stop + intent.action = IntentAction.stop context.startService(intent) } } @@ -65,7 +66,7 @@ object ReadAloud { fun prevParagraph(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.prevParagraph + intent.action = IntentAction.prevParagraph context.startService(intent) } } @@ -73,7 +74,7 @@ object ReadAloud { fun nextParagraph(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.nextParagraph + intent.action = IntentAction.nextParagraph context.startService(intent) } } @@ -81,7 +82,7 @@ object ReadAloud { fun upTtsSpeechRate(context: Context) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.upTtsSpeechRate + intent.action = IntentAction.upTtsSpeechRate context.startService(intent) } } @@ -89,7 +90,7 @@ object ReadAloud { fun setTimer(context: Context, minute: Int) { if (BaseReadAloudService.isRun) { val intent = Intent(context, aloudClass) - intent.action = Action.setTimer + intent.action = IntentAction.setTimer intent.putExtra("minute", minute) context.startService(intent) } 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 451883b55..d038ad600 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 @@ -12,9 +12,12 @@ import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import io.legado.app.service.BaseReadAloudService import io.legado.app.ui.book.read.page.ChapterProvider -import io.legado.app.ui.book.read.page.TextChapter -import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers.Main +import io.legado.app.ui.book.read.page.entities.TextChapter +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import org.jetbrains.anko.toast object ReadBook { @@ -39,13 +42,16 @@ object ReadBook { durChapterIndex = book.durChapterIndex durPageIndex = book.durChapterPos isLocalBook = book.origin == BookType.local - App.db.bookSourceDao().getBookSource(book.origin)?.let { - webBook = WebBook(it) - } chapterSize = 0 prevTextChapter = null curTextChapter = null nextTextChapter = null + upWebBook(book.origin) + } + + fun upWebBook(origin: String) { + val bookSource = App.db.bookSourceDao().getBookSource(origin) + webBook = if (bookSource != null) WebBook(bookSource) else null } fun moveToNextPage() { @@ -197,6 +203,7 @@ object ReadBook { private fun download(index: Int) { book?.let { book -> + if (book.isLocalBook()) return if (addLoading(index)) { Coroutine.async { App.db.bookChapterDao().getChapter(book.bookUrl, index)?.let { chapter -> @@ -260,23 +267,26 @@ object ReadBook { book!!.useReplaceRule ) when (chapter.index) { - durChapterIndex -> withContext(Main) { + durChapterIndex -> { curTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) callBack?.upContent() callBack?.upView() curPageChanged() callBack?.contentLoadFinish() } - durChapterIndex - 1 -> withContext(Main) { + durChapterIndex - 1 -> { prevTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) callBack?.upContent(-1) } - durChapterIndex + 1 -> withContext(Main) { + durChapterIndex + 1 -> { nextTextChapter = ChapterProvider.getTextChapter(chapter, c, chapterSize) callBack?.upContent(1) } } } + }.onError { + it.printStackTrace() + App.INSTANCE.toast(it.localizedMessage ?: "ChapterProvider ERROR") } } diff --git a/app/src/main/java/io/legado/app/ui/README.md b/app/src/main/java/io/legado/app/ui/README.md index 18c04f014..a8bf2b216 100644 --- a/app/src/main/java/io/legado/app/ui/README.md +++ b/app/src/main/java/io/legado/app/ui/README.md @@ -1 +1,25 @@ -## 放置与界面有关的类 \ No newline at end of file +## 放置与界面有关的类 + +* about 关于界面 +* audio 音频播放界面 +* book\arrange 书架整理界面 +* book\info 书籍信息查看 +* book\read 书籍阅读界面 +* book\search 搜索书籍界面 +* book\source 搜索书源界面 +* changeCover 封面换源界面 +* changeSource 换源界面 +* chapterList 目录界面 +* config 配置界面 +* download 下载界面 +* explore 发现界面 +* fileChooser 文件选择界面 +* importBook 书籍导入界面 +* main 主界面 +* qrCode 二维码扫描界面 +* replaceRule 替换净化界面 +* rss\article 订阅条目界面 +* rss\read 订阅阅读界面 +* rss\source 订阅源界面 +* welcome 欢迎界面 +* widget 自定义插件 \ No newline at end of file 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 f5636c134..33587d0a2 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 @@ -1,13 +1,12 @@ package io.legado.app.ui.about -import android.content.Intent -import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem import io.legado.app.R import io.legado.app.base.BaseActivity -import org.jetbrains.anko.toast +import io.legado.app.utils.openUrl +import io.legado.app.utils.shareText class AboutActivity : BaseActivity(R.layout.activity_about) { @@ -27,18 +26,13 @@ class AboutActivity : BaseActivity(R.layout.activity_about) { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_scoring -> openIntent("market://details?id=$packageName") + R.id.menu_scoring -> openUrl("market://details?id=$packageName") + R.id.menu_share_it -> shareText( + "App Share", + getString(R.string.app_share_description) + ) } return super.onCompatOptionsItemSelected(item) } - private fun openIntent(address: String) { - try { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(address) - startActivity(intent) - } catch (e: Exception) { - toast(R.string.can_not_open) - } - } } 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 08a5fa6a1..365381fbd 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 @@ -4,16 +4,35 @@ import android.content.Intent import android.net.Uri import android.os.Bundle import android.view.View +import androidx.annotation.StringRes import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.App import io.legado.app.R +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.widget.dialog.TextDialog +import io.legado.app.utils.openUrl +import io.legado.app.utils.sendToClip import io.legado.app.utils.toast class AboutFragment : PreferenceFragmentCompat() { + + private val licenseUrl = "https://github.com/gedoor/legado/blob/master/LICENSE" + private val disclaimerUrl = "https://gedoor.github.io/MyBookshelf/disclaimer.html" + private val qqGroups = linkedMapOf( + Pair("(QQ群VIP1)701903217", "-iolizL4cbJSutKRpeImHlXlpLDZnzeF"), + Pair("(QQ群VIP2)263949160", "xwfh7_csb2Gf3Aw2qexEcEtviLfLfd4L"), + 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") + ) + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.about) - findPreference("check_update")?.summary = getString(R.string.version) + " " + App.INSTANCE.versionName + findPreference("check_update")?.summary = + "${getString(R.string.version)} ${App.INSTANCE.versionName}" } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -23,35 +42,68 @@ class AboutFragment : PreferenceFragmentCompat() { override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { - "contributors" -> openIntent(Intent.ACTION_VIEW, getString(R.string.contributors_url)) - "update_log" -> UpdateLog().show(childFragmentManager, "update_log") - "check_update" -> openIntent(Intent.ACTION_VIEW, getString(R.string.latest_release_url)) - "mail" -> openIntent(Intent.ACTION_SENDTO, "mailto:kunfei.ge@gmail.com") - "git" -> openIntent(Intent.ACTION_VIEW, getString(R.string.this_github_url)) - "home_page" -> openIntent(Intent.ACTION_VIEW, getString(R.string.home_page_url)) - "share_app" -> shareText("App Share",getString(R.string.app_share_description)) + "contributors" -> openUrl(R.string.contributors_url) + "update_log" -> showUpdateLog() + "check_update" -> openUrl(R.string.latest_release_url) + "mail" -> sendMail() + "git" -> openUrl(R.string.this_github_url) + "home_page" -> openUrl(R.string.home_page_url) + "license" -> requireContext().openUrl(licenseUrl) + "disclaimer" -> requireContext().openUrl(disclaimerUrl) + "qq" -> showQqGroups() + "gzGzh" -> requireContext().sendToClip("开源阅读软件") } return super.onPreferenceTreeClick(preference) } - private fun openIntent(intentName: String, address: String) { + @Suppress("SameParameterValue") + private fun openUrl(@StringRes addressID: Int) { + requireContext().openUrl(getString(addressID)) + } + + private fun sendMail() { try { - val intent = Intent(intentName) - intent.data = Uri.parse(address) + val intent = Intent(Intent.ACTION_SENDTO) + intent.data = Uri.parse("mailto:kunfei.ge@gmail.com") startActivity(intent) } catch (e: Exception) { - toast(R.string.can_not_open) + toast(e.localizedMessage ?: "Error") } } - private fun shareText(title: String, text: String) { - try { - val textIntent = Intent(Intent.ACTION_SEND) - textIntent.type = "text/plain" - textIntent.putExtra(Intent.EXTRA_TEXT, text) - startActivity(Intent.createChooser(textIntent, title)) - } catch (e: Exception) { - toast(R.string.can_not_share) + private fun showUpdateLog() { + val log = String(requireContext().assets.open("updateLog.md").readBytes()) + TextDialog.show(childFragmentManager, log, TextDialog.MD) + } + + private fun showQqGroups() { + alert(title = R.string.join_qq_group) { + val names = arrayListOf() + qqGroups.forEach { + names.add(it.key) + } + items(names) { _, index -> + qqGroups[names[index]]?.let { + if (!joinQQGroup(it)) { + requireContext().sendToClip(it) + } + } + } + }.show() + } + + private fun joinQQGroup(key: String): Boolean { + val intent = Intent() + intent.data = + Uri.parse("mqqopensdkapi://bizAgent/qm/qr?url=http%3A%2F%2Fqm.qq.com%2Fcgi-bin%2Fqm%2Fqr%3Ffrom%3Dapp%26p%3Dandroid%26k%3D$key") + // 此Flag可根据具体产品需要自定义,如设置,则在加群界面按返回,返回手Q主界面,不设置,按返回会返回到呼起产品界面 + // intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + return try { + startActivity(intent) + true + } catch (e: java.lang.Exception) { + false } } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt index 19e950ec8..c29d47fcb 100644 --- a/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt +++ b/app/src/main/java/io/legado/app/ui/about/DonateActivity.kt @@ -1,20 +1,9 @@ package io.legado.app.ui.about -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.content.Intent -import android.net.Uri import android.os.Bundle -import android.widget.Toast import io.legado.app.R import io.legado.app.base.BaseActivity -import io.legado.app.lib.theme.ATH -import io.legado.app.utils.ACache -import kotlinx.android.synthetic.main.activity_donate.* -import org.jetbrains.anko.toast -import java.net.URLEncoder /** * Created by GKF on 2018/1/13. @@ -24,68 +13,12 @@ import java.net.URLEncoder class DonateActivity : BaseActivity(R.layout.activity_donate) { override fun onActivityCreated(savedInstanceState: Bundle?) { - ATH.applyEdgeEffectColor(scroll_view) - vw_zfb_tz.setOnClickListener { aliDonate(this) } - cv_wx_gzh.setOnClickListener { - val clipboard = this.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - val clipData = ClipData.newPlainText(null, "开源阅读软件") - clipboard?.let { - clipboard.setPrimaryClip(clipData) - toast(R.string.copy_complete) - } - } - vw_zfb_hb.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png") } - vw_zfb_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg") } - vw_wx_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg") } - vw_qq_rwm.setOnClickListener { openActionViewIntent("https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg") } - vw_zfb_hb_ssm.setOnClickListener { getZfbHb(this) } + val fTag = "donateFragment" + var donateFragment = supportFragmentManager.findFragmentByTag(fTag) + if (donateFragment == null) donateFragment = DonateFragment() + supportFragmentManager.beginTransaction() + .replace(R.id.fl_fragment, donateFragment, fTag) + .commit() } - private fun getZfbHb(context: Context) { - val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager - val clipData = ClipData.newPlainText(null, "537954522") - clipboard?.let { - clipboard.setPrimaryClip(clipData) - Toast.makeText(context, "高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包", Toast.LENGTH_LONG) - .show() - } - try { - val packageManager = context.applicationContext.packageManager - val intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone")!! - intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } finally { - ACache.get(this, cacheDir = false).put("proTime", System.currentTimeMillis()) - } - } - - private fun openActionViewIntent(address: String) { - try { - val intent = Intent(Intent.ACTION_VIEW) - intent.data = Uri.parse(address) - startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - Toast.makeText(this, R.string.can_not_open, Toast.LENGTH_SHORT).show() - } - - } - - private fun aliDonate(context: Context) { - try { - val qrCode = URLEncoder.encode( - "https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other", - "utf-8" - ) - val aliPayQr = - "alipayqr://platformapi/startapp?saId=10000007&qrcode=$qrCode&_t=${System.currentTimeMillis()}" - val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr)) - context.startActivity(intent) - } catch (e: Exception) { - e.printStackTrace() - } - - } } 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 new file mode 100644 index 000000000..0270b3f77 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/about/DonateFragment.kt @@ -0,0 +1,77 @@ +package io.legado.app.ui.about + +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.view.View +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import io.legado.app.R +import io.legado.app.utils.ACache +import io.legado.app.utils.openUrl +import io.legado.app.utils.sendToClip +import org.jetbrains.anko.longToast +import java.net.URLEncoder + +class DonateFragment : PreferenceFragmentCompat() { + + private val zfbHbRwmUrl = "https://gitee.com/gekunfei/Donate/raw/master/zfbhbrwm.png" + private val zfbSkRwmUrl = "https://gitee.com/gekunfei/Donate/raw/master/zfbskrwm.jpg" + private val wxZsRwmUrl = "https://gitee.com/gekunfei/Donate/raw/master/wxskrwm.jpg" + private val qqSkRwmUrl = "https://gitee.com/gekunfei/Donate/raw/master/qqskrwm.jpg" + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.donate) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + listView.overScrollMode = View.OVER_SCROLL_NEVER + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { + "wxZsm" -> requireContext().openUrl(wxZsRwmUrl) + "zfbHbRwm" -> requireContext().openUrl(zfbHbRwmUrl) + "zfbSkRwm" -> requireContext().openUrl(zfbSkRwmUrl) + "qqSkRwm" -> requireContext().openUrl(qqSkRwmUrl) + "zfbSk" -> aliDonate(requireContext()) + "zfbHbSsm" -> getZfbHb(requireContext()) + "gzGzh" -> requireContext().sendToClip("开源阅读软件") + } + return super.onPreferenceTreeClick(preference) + } + + private fun getZfbHb(context: Context) { + requireContext().sendToClip("537954522") + context.longToast("高级功能已开启\n红包码已复制\n支付宝首页搜索“537954522” 立即领红包") + try { + val packageManager = context.applicationContext.packageManager + val intent = packageManager.getLaunchIntentForPackage("com.eg.android.AlipayGphone")!! + intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } finally { + ACache.get(requireContext(), cacheDir = false) + .put("proTime", System.currentTimeMillis()) + } + } + + private fun aliDonate(context: Context) { + try { + val qrCode = URLEncoder.encode( + "https://qr.alipay.com/tsx06677nwdk3javroq4ef0?_s=Dweb-other", + "utf-8" + ) + val aliPayQr = + "alipayqr://platformapi/startapp?saId=10000007&qrcode=$qrCode&_t=${System.currentTimeMillis()}" + val intent = Intent(Intent.ACTION_VIEW, Uri.parse(aliPayQr)) + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/about/UpdateLog.kt b/app/src/main/java/io/legado/app/ui/about/UpdateLog.kt deleted file mode 100644 index 2bab8c1de..000000000 --- a/app/src/main/java/io/legado/app/ui/about/UpdateLog.kt +++ /dev/null @@ -1,42 +0,0 @@ -package io.legado.app.ui.about - -import android.os.Bundle -import android.util.DisplayMetrics -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.fragment.app.DialogFragment -import io.legado.app.R -import kotlinx.android.synthetic.main.dialog_text_view.* -import ru.noties.markwon.Markwon - -class UpdateLog : DialogFragment() { - - override fun onStart() { - super.onStart() - val dm = DisplayMetrics() - activity?.windowManager?.defaultDisplay?.getMetrics(dm) - dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - return inflater.inflate(R.layout.dialog_text_view, container) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - text_view.post { - Markwon.create(requireContext()) - .setMarkdown( - text_view, - String(requireContext().assets.open("updateLog.md").readBytes()) - ) - } - } - - -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt b/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt index a348b6e1e..dffd6a50f 100644 --- a/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt +++ b/app/src/main/java/io/legado/app/ui/audio/AudioPlayActivity.kt @@ -14,7 +14,7 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions import com.bumptech.glide.request.RequestOptions import io.legado.app.R import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.Status import io.legado.app.constant.Theme import io.legado.app.data.entities.Book @@ -193,12 +193,12 @@ class AudioPlayActivity : } override fun observeLiveBus() { - observeEvent(Bus.MEDIA_BUTTON) { + observeEvent(EventBus.MEDIA_BUTTON) { if (it) { playButton() } } - observeEventSticky(Bus.AUDIO_STATE) { + observeEventSticky(EventBus.AUDIO_STATE) { AudioPlay.status = it if (it == Status.PLAY) { fab_play_stop.setImageResource(R.drawable.ic_pause_24dp) @@ -206,19 +206,19 @@ class AudioPlayActivity : fab_play_stop.setImageResource(R.drawable.ic_play_24dp) } } - observeEventSticky(Bus.AUDIO_SUB_TITLE) { + observeEventSticky(EventBus.AUDIO_SUB_TITLE) { tv_sub_title.text = it } - observeEventSticky(Bus.AUDIO_SIZE) { + observeEventSticky(EventBus.AUDIO_SIZE) { player_progress.max = it tv_all_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") } - observeEventSticky(Bus.AUDIO_PROGRESS) { + observeEventSticky(EventBus.AUDIO_PROGRESS) { AudioPlay.durPageIndex = it if (!adjustProgress) player_progress.progress = it tv_dur_time.text = DateFormatUtils.format(it.toLong(), "mm:ss") } - observeEventSticky(Bus.AUDIO_SPEED) { + observeEventSticky(EventBus.AUDIO_SPEED) { tv_speed.text = String.format("%.1fX", it) tv_speed.visible() } 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 5adc2c7be..b5dd5bfba 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 @@ -91,7 +91,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application) fun changeTo(book1: Book) { execute { AudioPlay.book?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(it) } withContext(Dispatchers.Main) { @@ -142,7 +142,7 @@ class AudioPlayViewModel(application: Application) : BaseViewModel(application) fun removeFromBookshelf(success: (() -> Unit)?) { execute { AudioPlay.book?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(it) } }.onSuccess { success?.invoke() 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 new file mode 100644 index 000000000..e3ca00964 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookActivity.kt @@ -0,0 +1,194 @@ +package io.legado.app.ui.book.arrange + +import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.PopupMenu +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +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.data.entities.Book +import io.legado.app.data.entities.BookGroup +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.noButton +import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.theme.ATH +import io.legado.app.ui.book.group.GroupManageDialog +import io.legado.app.ui.book.group.GroupSelectDialog +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.utils.applyTint +import io.legado.app.utils.getVerticalDivider +import io.legado.app.utils.getViewModel +import kotlinx.android.synthetic.main.activity_arrange_book.* + + +class ArrangeBookActivity : VMBaseActivity(R.layout.activity_arrange_book), + PopupMenu.OnMenuItemClickListener, + ArrangeBookAdapter.CallBack, GroupSelectDialog.CallBack { + override val viewModel: ArrangeBookViewModel + get() = getViewModel(ArrangeBookViewModel::class.java) + override val groupList: ArrayList = arrayListOf() + private val groupRequestCode = 22 + private lateinit var adapter: ArrangeBookAdapter + private var groupLiveData: LiveData>? = null + private var booksLiveData: LiveData>? = null + private var menu: Menu? = null + private var groupId: Int = -1 + + override fun onActivityCreated(savedInstanceState: Bundle?) { + groupId = intent.getIntExtra("groupId", -1) + title_bar.subtitle = intent.getStringExtra("groupName") ?: getString(R.string.all) + initView() + initGroupData() + initBookData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.arrange_book, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + private fun initView() { + ATH.applyEdgeEffectColor(recycler_view) + recycler_view.layoutManager = LinearLayoutManager(this) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) + adapter = ArrangeBookAdapter(this, this) + recycler_view.adapter = adapter + select_action_bar.setMainActionText(R.string.move_to_group) + select_action_bar.inflateMenu(R.menu.arrange_book_sel) + select_action_bar.setOnMenuItemClickListener(this) + select_action_bar.setCallBack(object : SelectActionBar.CallBack { + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + selectGroup(0, groupRequestCode) + } + }) + } + + private fun initGroupData() { + groupLiveData?.removeObservers(this) + groupLiveData = App.db.bookGroupDao().liveDataAll() + groupLiveData?.observe(this, Observer { + groupList.clear() + groupList.addAll(it) + adapter.notifyDataSetChanged() + upMenu() + }) + } + + private fun initBookData() { + booksLiveData?.removeObservers(this) + booksLiveData = + if (groupId == -1) { + App.db.bookDao().observeAll() + } else { + App.db.bookDao().observeByGroup(groupId) + } + booksLiveData?.observe(this, Observer { + adapter.setItems(it) + upSelectCount() + }) + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_group_manage -> GroupManageDialog() + .show(supportFragmentManager, "groupManage") + R.id.menu_all -> { + title_bar.subtitle = item.title + groupId = -1 + adapter.selectedBooks.clear() + initBookData() + } + R.id.menu_local -> { + title_bar.subtitle = item.title + groupId = -2 + adapter.selectedBooks.clear() + initBookData() + } + R.id.menu_audio -> { + title_bar.subtitle = item.title + groupId = -3 + adapter.selectedBooks.clear() + initBookData() + } + else -> if (item.groupId == R.id.menu_group) { + title_bar.subtitle = item.title + groupId = item.itemId + adapter.selectedBooks.clear() + initBookData() + } + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_del_selection -> + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.deleteBook(*adapter.selectedBooks.toTypedArray()) } + noButton { } + }.show().applyTint() + } + return false + } + + private fun upMenu() { + menu?.findItem(R.id.menu_book_group)?.subMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group) + groupList.forEach { bookGroup -> + subMenu.add(R.id.menu_group, bookGroup.groupId, Menu.NONE, bookGroup.groupName) + } + } + } + + override fun selectGroup(groupId: Int, requestCode: Int) { + GroupSelectDialog.show(supportFragmentManager, groupId, requestCode) + } + + override fun upGroup(requestCode: Int, groupId: Int) { + when (requestCode) { + groupRequestCode -> { + val books = arrayListOf() + adapter.selectedBooks.forEach { + books.add(it.copy(group = groupId)) + } + viewModel.updateBook(*books.toTypedArray()) + } + adapter.groupRequestCode -> { + adapter.actionItem?.let { + viewModel.updateBook(it.copy(group = groupId)) + } + } + } + } + + override fun upSelectCount() { + select_action_bar.upCountView(adapter.selectedBooks.size, adapter.getItems().size) + } + + override fun deleteBook(book: Book) { + alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { + viewModel.deleteBook(book) + } + }.show().applyTint() + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt new file mode 100644 index 000000000..66eff5706 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookAdapter.kt @@ -0,0 +1,114 @@ +package io.legado.app.ui.book.arrange + +import android.content.Context +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.data.entities.Book +import io.legado.app.data.entities.BookGroup +import kotlinx.android.synthetic.main.item_arrange_book.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + + +class ArrangeBookAdapter(context: Context, val callBack: CallBack) : + SimpleRecyclerAdapter(context, R.layout.item_arrange_book) { + val groupRequestCode = 12 + val selectedBooks: HashSet = hashSetOf() + var actionItem: Book? = null + + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + selectedBooks.add(it) + } + } else { + selectedBooks.clear() + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + fun revertSelection() { + getItems().forEach { + if (selectedBooks.contains(it)) { + selectedBooks.remove(it) + } else { + selectedBooks.add(it) + } + } + notifyDataSetChanged() + callBack.upSelectCount() + } + + override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { + with(holder.itemView) { + tv_name.text = if (item.author.isEmpty()) { + item.name + } else { + "${item.name}(${item.author})" + } + tv_author.text = getGroupName(item.group) + checkbox.isChecked = selectedBooks.contains(item) + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + checkbox.setOnCheckedChangeListener { buttonView, isChecked -> + getItem(holder.layoutPosition)?.let { + if (buttonView.isPressed) { + if (isChecked) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + callBack.upSelectCount() + } + + } + } + onClick { + getItem(holder.layoutPosition)?.let { + checkbox.isChecked = !checkbox.isChecked + if (checkbox.isChecked) { + selectedBooks.add(it) + } else { + selectedBooks.remove(it) + } + callBack.upSelectCount() + } + } + tv_delete.onClick { + getItem(holder.layoutPosition)?.let { + callBack.deleteBook(it) + } + } + tv_group.onClick { + getItem(holder.layoutPosition)?.let { + actionItem = it + callBack.selectGroup(it.group, groupRequestCode) + } + } + } + } + + private fun getGroupName(groupId: Int): String { + val groupNames = arrayListOf() + callBack.groupList.forEach { + if (it.groupId and groupId > 0) { + groupNames.add(it.groupName) + } + } + if (groupNames.isEmpty()) { + return context.getString(R.string.no_group) + } + return groupNames.joinToString(",") + } + + interface CallBack { + val groupList: List + fun upSelectCount() + fun deleteBook(book: Book) + fun selectGroup(groupId: Int, requestCode: Int) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt new file mode 100644 index 000000000..4f4972de9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/arrange/ArrangeBookViewModel.kt @@ -0,0 +1,23 @@ +package io.legado.app.ui.book.arrange + +import android.app.Application +import io.legado.app.App +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.Book + + +class ArrangeBookViewModel(application: Application) : BaseViewModel(application) { + + fun updateBook(vararg book: Book) { + execute { + App.db.bookDao().update(*book) + } + } + + fun deleteBook(vararg book: Book) { + execute { + App.db.bookDao().delete(*book) + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt similarity index 72% rename from app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt rename to app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt index 0d432e437..e982cbbe0 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupManageDialog.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.main.bookshelf +package io.legado.app.ui.book.group import android.annotation.SuppressLint import android.content.Context @@ -12,7 +12,7 @@ import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -29,15 +29,18 @@ import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.utils.applyTint +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModel import io.legado.app.utils.requestInputMethod import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.item_group_manage.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* +import kotlin.collections.ArrayList class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { - private lateinit var viewModel: BookshelfViewModel + private lateinit var viewModel: GroupViewModel private lateinit var adapter: GroupAdapter private var callBack: CallBack? = null @@ -53,7 +56,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(BookshelfViewModel::class.java) + viewModel = getViewModel(GroupViewModel::class.java) return inflater.inflate(R.layout.dialog_recycler_view, container) } @@ -74,12 +77,12 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { .isChecked = AppConst.bookGroupAudioShow adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { - adapter.setItems(it) + val diffResult = + DiffUtil.calculateDiff(GroupDiffCallBack(ArrayList(adapter.getItems()), it)) + adapter.setItems(it, diffResult) }) val itemTouchCallback = ItemTouchCallback() itemTouchCallback.onItemTouchCallbackListener = adapter @@ -145,32 +148,66 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { }.show().applyTint().requestInputMethod() } + private class GroupDiffCallBack( + private val oldItems: List, + private val newItems: List + ) : DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldItems.size + } + + override fun getNewListSize(): Int { + return newItems.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return true + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return oldItem.groupName == newItem.groupName + } + + } + private inner class GroupAdapter(context: Context) : SimpleRecyclerAdapter(context, R.layout.item_group_manage), ItemTouchCallback.OnItemTouchCallbackListener { + private var isMoved = false + override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList) { - with(holder.itemView) { + holder.itemView.apply { tv_group.text = item.groupName - tv_edit.onClick { editGroup(item) } - tv_del.onClick { viewModel.delGroup(item) } + } } - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - val srcItem = getItem(srcPosition) - val targetItem = getItem(targetPosition) - if (srcItem != null && targetItem != null) { - val order = srcItem.order - srcItem.order = targetItem.order - targetItem.order = order - viewModel.upGroup(srcItem, targetItem) + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + tv_edit.onClick { getItem(holder.layoutPosition)?.let { editGroup(it) } } + tv_del.onClick { getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } } } - return true } - override fun onSwiped(adapterPosition: Int) { + override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) + isMoved = true + return true + } + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.order = index + 1 + } + viewModel.upGroup(*getItems().toTypedArray()) + } + isMoved = false } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/GroupSelectDialog.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt similarity index 64% rename from app/src/main/java/io/legado/app/ui/book/info/GroupSelectDialog.kt rename to app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt index 517f9dc9d..2d90a82e2 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/GroupSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupSelectDialog.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.book.info +package io.legado.app.ui.book.group import android.annotation.SuppressLint import android.content.Context @@ -13,7 +13,6 @@ import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView @@ -28,29 +27,40 @@ import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton -import io.legado.app.ui.main.bookshelf.BookshelfViewModel +import io.legado.app.lib.theme.accentColor import io.legado.app.utils.applyTint +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModel import io.legado.app.utils.requestInputMethod +import kotlinx.android.synthetic.main.dialog_book_group_picker.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.dialog_recycler_view.* -import kotlinx.android.synthetic.main.item_group_manage.view.* +import kotlinx.android.synthetic.main.dialog_recycler_view.recycler_view +import kotlinx.android.synthetic.main.dialog_recycler_view.tool_bar +import kotlinx.android.synthetic.main.item_group_select.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { companion object { const val tag = "groupSelectDialog" - fun show(manager: FragmentManager) { - val fragment = GroupSelectDialog() + fun show(manager: FragmentManager, groupId: Int, requestCode: Int = -1) { + val fragment = GroupSelectDialog().apply { + val bundle = Bundle() + bundle.putInt("groupId", groupId) + bundle.putInt("requestCode", requestCode) + arguments = bundle + } fragment.show(manager, tag) } } - private lateinit var viewModel: BookshelfViewModel + private var requestCode: Int = -1 + private lateinit var viewModel: GroupViewModel private lateinit var adapter: GroupAdapter private var callBack: CallBack? = null + private var groupId = 0 override fun onStart() { super.onStart() @@ -64,17 +74,22 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { container: ViewGroup?, savedInstanceState: Bundle? ): View? { - viewModel = getViewModel(BookshelfViewModel::class.java) - return inflater.inflate(R.layout.dialog_recycler_view, container) + viewModel = getViewModel(GroupViewModel::class.java) + return inflater.inflate(R.layout.dialog_book_group_picker, container) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) callBack = activity as? CallBack + arguments?.let { + groupId = it.getInt("groupId") + requestCode = it.getInt("requestCode", -1) + } + initView() initData() } - private fun initData() { + private fun initView() { tool_bar.title = getString(R.string.group_select) tool_bar.inflateMenu(R.menu.book_group_manage) tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) @@ -83,17 +98,24 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { tool_bar.menu.findItem(R.id.menu_group_audio).isVisible = false adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter - App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { - adapter.setItems(it) - }) val itemTouchCallback = ItemTouchCallback() itemTouchCallback.onItemTouchCallbackListener = adapter itemTouchCallback.isCanDrag = true ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) + tv_cancel.onClick { dismiss() } + tv_ok.setTextColor(requireContext().accentColor) + tv_ok.onClick { + callBack?.upGroup(requestCode, groupId) + dismiss() + } + } + + private fun initData() { + App.db.bookGroupDao().liveDataAll().observe(viewLifecycleOwner, Observer { + adapter.setItems(it) + }) } override fun onMenuItemClick(item: MenuItem?): Boolean { @@ -145,39 +167,54 @@ class GroupSelectDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { } private inner class GroupAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_group_manage), + SimpleRecyclerAdapter(context, R.layout.item_group_select), ItemTouchCallback.OnItemTouchCallbackListener { + private var isMoved: Boolean = false + override fun convert(holder: ItemViewHolder, item: BookGroup, payloads: MutableList) { - with(holder.itemView) { - tv_group.text = item.groupName - tv_edit.onClick { editGroup(item) } - tv_del.onClick { viewModel.delGroup(item) } - this.onClick { - callBack?.upGroup(item) - dismiss() + holder.itemView.apply { + cb_group.text = item.groupName + cb_group.isChecked = (groupId and item.groupId) > 0 + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + cb_group.setOnCheckedChangeListener { buttonView, isChecked -> + getItem(holder.layoutPosition)?.let { + if (buttonView.isPressed) { + groupId = if (isChecked) { + groupId + it.groupId + } else { + groupId - it.groupId + } + } + } } + tv_edit.onClick { getItem(holder.layoutPosition)?.let { editGroup(it) } } } } override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - val srcItem = getItem(srcPosition) - val targetItem = getItem(targetPosition) - if (srcItem != null && targetItem != null) { - val order = srcItem.order - srcItem.order = targetItem.order - targetItem.order = order - viewModel.upGroup(srcItem, targetItem) - } + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) + isMoved = true return true } - override fun onSwiped(adapterPosition: Int) { - + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.order = index + 1 + } + viewModel.upGroup(*getItems().toTypedArray()) + } + isMoved = false } } interface CallBack { - fun upGroup(group: BookGroup) + fun upGroup(requestCode: Int, groupId: Int) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt new file mode 100644 index 000000000..eb811766e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/group/GroupViewModel.kt @@ -0,0 +1,39 @@ +package io.legado.app.ui.book.group + +import android.app.Application +import io.legado.app.App +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.BookGroup + +class GroupViewModel(application: Application) : BaseViewModel(application) { + + fun addGroup(groupName: String) { + execute { + var id = 1 + val idsSum = App.db.bookGroupDao().idsSum + while (id and idsSum != 0) { + id = id.shl(1) + } + val bookGroup = BookGroup( + groupId = id, + groupName = groupName, + order = App.db.bookGroupDao().maxOrder.plus(1) + ) + App.db.bookGroupDao().insert(bookGroup) + } + } + + fun upGroup(vararg bookGroup: BookGroup) { + execute { + App.db.bookGroupDao().update(*bookGroup) + } + } + + fun delGroup(vararg bookGroup: BookGroup) { + execute { + App.db.bookGroupDao().delete(*bookGroup) + } + } + + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt index c55c1812c..efcddba7c 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/BookInfoActivity.kt @@ -4,6 +4,7 @@ import android.app.Activity import android.content.Intent import android.graphics.drawable.Drawable import android.os.Bundle +import android.text.method.ScrollingMovementMethod import android.view.Menu import android.view.MenuItem import androidx.lifecycle.Observer @@ -16,14 +17,15 @@ import io.legado.app.constant.BookType import io.legado.app.constant.Theme import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.BookGroup import io.legado.app.help.BlurTransformation import io.legado.app.help.ImageLoader import io.legado.app.help.IntentDataHelp import io.legado.app.ui.audio.AudioPlayActivity +import io.legado.app.ui.book.group.GroupSelectDialog import io.legado.app.ui.book.info.edit.BookInfoEditActivity import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.book.source.edit.BookSourceEditActivity +import io.legado.app.ui.changecover.ChangeCoverDialog import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.utils.getViewModel @@ -32,6 +34,7 @@ import io.legado.app.utils.visible import kotlinx.android.synthetic.main.activity_book_info.* import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.startActivity +import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.toast @@ -39,23 +42,20 @@ class BookInfoActivity : VMBaseActivity(R.layout.activity_book_info, theme = Theme.Dark), GroupSelectDialog.CallBack, ChapterListAdapter.CallBack, - ChangeSourceDialog.CallBack { + ChangeSourceDialog.CallBack, + ChangeCoverDialog.CallBack { + + private val requestCodeChapterList = 568 + private val requestCodeSourceEdit = 562 override val viewModel: BookInfoViewModel get() = getViewModel(BookInfoViewModel::class.java) override fun onActivityCreated(savedInstanceState: Bundle?) { title_bar.background.alpha = 0 + tv_intro.movementMethod = ScrollingMovementMethod.getInstance() viewModel.bookData.observe(this, Observer { showBook(it) }) - viewModel.isLoadingData.observe(this, Observer { upLoading(it) }) - viewModel.chapterListData.observe(this, Observer { showChapter(it) }) - viewModel.groupData.observe(this, Observer { - if (it == null) { - tv_group.text = getString(R.string.group_s, getString(R.string.no_group)) - } else { - tv_group.text = getString(R.string.group_s, it.groupName) - } - }) + viewModel.chapterListData.observe(this, Observer { upLoading(false, it) }) viewModel.initData(intent) initOnClick() } @@ -70,7 +70,10 @@ class BookInfoActivity : R.id.menu_edit -> { if (viewModel.inBookshelf) { viewModel.bookData.value?.let { - startActivity(Pair("bookUrl", it.bookUrl)) + startActivityForResult( + requestCodeSourceEdit, + Pair("bookUrl", it.bookUrl) + ) } } else { toast(R.string.after_add_bookshelf) @@ -97,86 +100,85 @@ class BookInfoActivity : return super.onMenuOpened(featureId, menu) } - private fun defaultCover(): RequestBuilder { - return ImageLoader.load(this, R.drawable.image_cover_default) - .apply(bitmapTransform(BlurTransformation(this, 25))) - } - private fun showBook(book: Book) { + showCover(book) tv_name.text = book.name tv_author.text = getString(R.string.author_show, book.author) tv_origin.text = getString(R.string.origin_show, book.originName) tv_lasted.text = getString(R.string.lasted_show, book.latestChapterTitle) - tv_toc.text = getString(R.string.toc_s, book.latestChapterTitle) + tv_toc.text = getString(R.string.toc_s, getString(R.string.loading)) tv_intro.text = book.getDisplayIntro() - book.getDisplayCover()?.let { - ImageLoader.load(this, it) - .centerCrop() - .into(iv_cover) - ImageLoader.load(this, it) - .transition(DrawableTransitionOptions.withCrossFade(1500)) - .thumbnail(defaultCover()) - .centerCrop() - .apply(bitmapTransform(BlurTransformation(this, 25))) - .into(bg_book) //模糊、渐变、缩小效果 - } + upTvBookshelf() val kinds = book.getKindList() if (kinds.isEmpty()) { - ll_kind.gone() + lb_kind.gone() } else { - ll_kind.visible() - for (index in 0..2) { - if (kinds.size > index) { - when (index) { - 0 -> { - tv_kind.text = kinds[index] - tv_kind.visible() - } - 1 -> { - tv_kind_1.text = kinds[index] - tv_kind_1.visible() - } - 2 -> { - tv_kind_2.text = kinds[index] - tv_kind_2.visible() - } - } - } else { - when (index) { - 0 -> tv_kind.gone() - 1 -> tv_kind_1.gone() - 2 -> tv_kind_2.gone() + lb_kind.visible() + lb_kind.setLabels(kinds) + } + upGroup(book.group) + } + + private fun showCover(book: Book) { + iv_cover.load(book.getDisplayCover(), book.name, book.author) + ImageLoader.load(this, book.getDisplayCover()) + .transition(DrawableTransitionOptions.withCrossFade(1500)) + .thumbnail(defaultCover()) + .centerCrop() + .apply(bitmapTransform(BlurTransformation(this, 25))) + .into(bg_book) //模糊、渐变、缩小效果 + } + + private fun defaultCover(): RequestBuilder { + return ImageLoader.load(this, R.drawable.image_cover_default) + .apply(bitmapTransform(BlurTransformation(this, 25))) + } + + private fun upLoading(isLoading: Boolean, chapterList: List? = null) { + when { + isLoading -> { + tv_toc.text = getString(R.string.toc_s, getString(R.string.loading)) + } + chapterList.isNullOrEmpty() -> { + tv_toc.text = getString(R.string.toc_s, getString(R.string.error_load_toc)) + } + else -> { + viewModel.bookData.value?.let { + if (it.durChapterIndex < chapterList.size) { + tv_toc.text = + getString(R.string.toc_s, chapterList[it.durChapterIndex].title) + } else { + tv_toc.text = getString(R.string.toc_s, chapterList.last().title) } } } } } - private fun showChapter(chapterList: List) { - viewModel.bookData.value?.let { - if (it.durChapterIndex < chapterList.size) { - tv_toc.text = getString(R.string.toc_s, chapterList[it.durChapterIndex].title) - } else { - tv_toc.text = getString(R.string.toc_s, chapterList.last().title) - } + private fun upTvBookshelf() { + if (viewModel.inBookshelf) { + tv_shelf.text = getString(R.string.remove_from_bookshelf) + } else { + tv_shelf.text = getString(R.string.add_to_shelf) } - upLoading(false) } - private fun upLoading(isLoading: Boolean) { - if (isLoading) { - tv_loading.visible() - } else { - if (viewModel.inBookshelf) { - tv_shelf.text = getString(R.string.remove_from_bookshelf) + private fun upGroup(groupId: Int) { + viewModel.loadGroup(groupId) { + if (it.isNullOrEmpty()) { + tv_group.text = getString(R.string.group_s, getString(R.string.no_group)) } else { - tv_shelf.text = getString(R.string.add_to_shelf) + tv_group.text = getString(R.string.group_s, it) } - tv_loading.gone() } } private fun initOnClick() { + iv_cover.onClick { + viewModel.bookData.value?.let { + ChangeCoverDialog.show(supportFragmentManager, it.name, it.author) + } + } tv_read.onClick { viewModel.bookData.value?.let { readBook(it) @@ -185,19 +187,14 @@ class BookInfoActivity : tv_shelf.onClick { if (viewModel.inBookshelf) { viewModel.delBook { - tv_shelf.text = getString(R.string.add_to_shelf) + upTvBookshelf() } } else { viewModel.addToBookshelf { - tv_shelf.text = getString(R.string.remove_from_bookshelf) + upTvBookshelf() } } } - tv_loading.onClick { - viewModel.bookData.value?.let { - viewModel.loadBookInfo(it) - } - } tv_origin.onClick { viewModel.bookData.value?.let { startActivity(Pair("data", it.origin)) @@ -210,17 +207,32 @@ class BookInfoActivity : } tv_toc.onClick { if (!viewModel.inBookshelf) { - toast(R.string.after_add_bookshelf) - return@onClick + viewModel.saveBook { + viewModel.saveChapterList { + openChapterList() + } + } + } else { + openChapterList() } + } + tv_group.onClick { viewModel.bookData.value?.let { - startActivity( - Pair("bookUrl", it.bookUrl) - ) + GroupSelectDialog.show(supportFragmentManager, it.group) } } - tv_group.onClick { - GroupSelectDialog.show(supportFragmentManager) + } + + private fun openChapterList() { + if (viewModel.chapterListData.value.isNullOrEmpty()) { + toast(R.string.chapter_list_empty) + return + } + viewModel.bookData.value?.let { + startActivityForResult( + requestCodeChapterList, + Pair("bookUrl", it.bookUrl) + ) } } @@ -260,6 +272,14 @@ class BookInfoActivity : viewModel.changeTo(book) } + override fun coverChangeTo(coverUrl: String) { + viewModel.bookData.value?.let { + it.coverUrl = coverUrl + viewModel.saveBook() + showCover(it) + } + } + override fun openChapter(chapter: BookChapter) { if (chapter.index != viewModel.durChapterIndex) { viewModel.bookData.value?.let { @@ -274,9 +294,9 @@ class BookInfoActivity : return viewModel.durChapterIndex } - override fun upGroup(group: BookGroup) { - viewModel.groupData.postValue(group) - viewModel.bookData.value?.group = group.groupId + override fun upGroup(requestCode: Int, groupId: Int) { + upGroup(groupId) + viewModel.bookData.value?.group = groupId if (viewModel.inBookshelf) { viewModel.saveBook() } @@ -284,8 +304,25 @@ class BookInfoActivity : override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - if (resultCode == Activity.RESULT_OK) { - viewModel.initData(intent) + when (requestCode) { + requestCodeSourceEdit -> if (resultCode == Activity.RESULT_OK) { + viewModel.initData(intent) + } + requestCodeChapterList -> if (resultCode == Activity.RESULT_OK) { + viewModel.bookData.value?.let { + data?.getIntExtra("index", it.durChapterIndex)?.let { index -> + if (it.durChapterIndex != index) { + it.durChapterIndex = index + it.durChapterPos = 0 + } + startReadActivity(it) + } + } + } else { + if (!viewModel.inBookshelf) { + viewModel.delBook(null) + } + } } } } \ No newline at end of file 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 bace352ff..1a675e21a 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 @@ -8,25 +8,21 @@ 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.data.entities.BookGroup import io.legado.app.help.BookHelp import io.legado.app.model.WebBook +import io.legado.app.model.localBook.AnalyzeTxtFile import kotlinx.coroutines.Dispatchers.IO class BookInfoViewModel(application: Application) : BaseViewModel(application) { - val bookData = MutableLiveData() val chapterListData = MutableLiveData>() - val isLoadingData = MutableLiveData() var durChapterIndex = 0 var inBookshelf = false - var groupData = MutableLiveData() fun initData(intent: Intent) { execute { intent.getStringExtra("bookUrl")?.let { App.db.bookDao().getBook(it)?.let { book -> - groupData.postValue(App.db.bookGroupDao().getByID(book.group)) inBookshelf = true setBook(book) } ?: App.db.searchBookDao().getSearchBook(it)?.toBook()?.let { book -> @@ -45,7 +41,6 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { val chapterList = App.db.bookChapterDao().getChapterList(book.bookUrl) if (chapterList.isNotEmpty()) { chapterListData.postValue(chapterList) - isLoadingData.postValue(false) } else { loadChapter(book) } @@ -57,24 +52,26 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { changeDruChapterIndex: ((chapters: List) -> Unit)? = null ) { execute { - isLoadingData.postValue(true) - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getBookInfo(book, this) - .onSuccess(IO) { - it?.let { - bookData.postValue(book) - if (inBookshelf) { - App.db.bookDao().update(book) + if (book.isLocalBook()) { + loadChapter(book, changeDruChapterIndex) + } else { + App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> + WebBook(bookSource).getBookInfo(book, this) + .onSuccess(IO) { + it?.let { + bookData.postValue(book) + if (inBookshelf) { + App.db.bookDao().update(book) + } + loadChapter(it, changeDruChapterIndex) } - loadChapter(it, changeDruChapterIndex) + }.onError { + toast(R.string.error_get_book_info) } - }.onError { - isLoadingData.postValue(false) - toast(R.string.error_get_book_info) - } - } ?: let { - isLoadingData.postValue(false) - toast(R.string.error_no_source) + } ?: let { + chapterListData.postValue(null) + toast(R.string.error_no_source) + } } } } @@ -84,43 +81,62 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { changeDruChapterIndex: ((chapters: List) -> Unit)? = null ) { execute { - isLoadingData.postValue(true) - App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> - WebBook(bookSource).getChapterList(book, this) - .onSuccess(IO) { - it?.let { - if (it.isNotEmpty()) { - if (inBookshelf) { - App.db.bookDao().update(book) - App.db.bookChapterDao().insert(*it.toTypedArray()) - } - if (changeDruChapterIndex == null) { - chapterListData.postValue(it) - isLoadingData.postValue(false) + if (book.isLocalBook()) { + AnalyzeTxtFile.analyze(context, book).let { + App.db.bookDao().update(book) + App.db.bookChapterDao().insert(*it.toTypedArray()) + chapterListData.postValue(it) + } + } else { + App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> + WebBook(bookSource).getChapterList(book, this) + .onSuccess(IO) { + it?.let { + if (it.isNotEmpty()) { + if (inBookshelf) { + App.db.bookDao().update(book) + App.db.bookChapterDao().insert(*it.toTypedArray()) + } + if (changeDruChapterIndex == null) { + chapterListData.postValue(it) + } else { + changeDruChapterIndex(it) + } } else { - changeDruChapterIndex(it) + toast(R.string.chapter_list_empty) } - } else { - isLoadingData.postValue(false) - toast(R.string.chapter_list_empty) } + }.onError { + chapterListData.postValue(null) + toast(R.string.error_get_chapter_list) } - }.onError { - isLoadingData.postValue(false) - toast(R.string.error_get_chapter_list) - } - } ?: let { - isLoadingData.postValue(false) - toast(R.string.error_no_source) + } ?: let { + chapterListData.postValue(null) + toast(R.string.error_no_source) + } } } } + fun loadGroup(groupId: Int, success: ((groupNames: String?) -> Unit)) { + execute { + val groupNames = arrayListOf() + App.db.bookGroupDao().all.forEach { + if (groupId and it.groupId > 0) { + groupNames.add(it.groupName) + } + } + groupNames.joinToString(",") + }.onSuccess { + success.invoke(it) + } + } + fun changeTo(book: Book) { execute { if (inBookshelf) { bookData.value?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(it) } App.db.bookDao().insert(book) } @@ -185,7 +201,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) { fun delBook(success: (() -> Unit)?) { execute { bookData.value?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(it) } inBookshelf = false }.onSuccess { diff --git a/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt index bfe98f4d8..4af4e96fa 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/ChapterListAdapter.kt @@ -13,22 +13,24 @@ import org.jetbrains.anko.textColorResource class ChapterListAdapter(context: Context, var callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_chapter_list) { - var reorder: Boolean = false; // 是否倒序 - override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList) { holder.itemView.apply { - var _item: BookChapter = item; - if (reorder) { - _item = getItems().get(getItems().size - item.index - 1); - } - tv_chapter_name.text = _item.title - if (_item.index == callBack.durChapterIndex()) { + tv_chapter_name.text = item.title + if (item.index == callBack.durChapterIndex()) { tv_chapter_name.setTextColor(context.accentColor) } else { tv_chapter_name.textColorResource = R.color.tv_text_secondary } + + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { this.onClick { - callBack.openChapter(_item) + getItem(holder.layoutPosition)?.let { + callBack.openChapter(it) + } } } } diff --git a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt index a4e420af1..6bcae9257 100644 --- a/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/info/edit/BookInfoEditActivity.kt @@ -8,7 +8,6 @@ import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.Book -import io.legado.app.help.ImageLoader import io.legado.app.ui.changecover.ChangeCoverDialog import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.activity_book_info_edit.* @@ -59,10 +58,8 @@ class BookInfoEditActivity : } private fun upCover() { - viewModel.book?.getDisplayCover()?.let { - ImageLoader.load(this, it) - .centerCrop() - .into(iv_cover) + viewModel.book.let { + iv_cover.load(it?.getDisplayCover(), it?.name, it?.author) } } 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 33294b7e4..3a3e04191 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 @@ -1,25 +1,42 @@ package io.legado.app.ui.book.read +import android.annotation.SuppressLint import android.app.Activity +import android.content.Context +import android.os.AsyncTask import android.os.Build -import android.view.View +import android.view.* import android.view.View.NO_ID -import android.view.ViewGroup -import android.view.Window -import android.view.WindowManager +import android.widget.EditText import io.legado.app.App +import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.data.entities.Bookmark +import io.legado.app.help.AppConfig import io.legado.app.help.ReadBookConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.customView +import io.legado.app.lib.dialogs.noButton +import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ThemeStore +import io.legado.app.service.help.Download +import io.legado.app.service.help.ReadBook +import io.legado.app.utils.applyTint import io.legado.app.utils.getPrefBoolean -import io.legado.app.utils.isTransparentStatusBar +import io.legado.app.utils.requestInputMethod +import kotlinx.android.synthetic.main.dialog_download_choice.view.* +import kotlinx.android.synthetic.main.dialog_edit_text.view.* +import org.jetbrains.anko.layoutInflater object Help { private const val NAVIGATION = "navigationBarBackground" + /** + * 更新状态栏,导航栏 + */ fun upSystemUiVisibility(activity: Activity, toolBarHide: Boolean = true) { var flag = (View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN or View.SYSTEM_UI_FLAG_LAYOUT_STABLE @@ -39,11 +56,11 @@ object Help { } activity.window.decorView.systemUiVisibility = flag if (toolBarHide) { - ATH.setLightStatusBar(activity, ReadBookConfig.getConfig().statusIconDark()) + ATH.setLightStatusBar(activity, ReadBookConfig.durConfig.statusIconDark()) } else { ATH.setLightStatusBarAuto( activity, - ThemeStore.statusBarColor(activity, activity.isTransparentStatusBar) + ThemeStore.statusBarColor(activity, AppConfig.isTransparentStatusBar) ) } } @@ -70,6 +87,17 @@ object Help { return false } + /** + * 保持亮屏 + */ + fun keepScreenOn(window: Window, on: Boolean) { + if (on) { + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } else { + window.clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + } + } + /** * 适配刘海 */ @@ -81,4 +109,63 @@ object Help { } } } + + @SuppressLint("InflateParams") + fun showDownloadDialog(context: Context) { + ReadBook.book?.let { book -> + context.alert(titleResource = R.string.download_offline) { + var view: View? = null + customView { + LayoutInflater.from(context).inflate(R.layout.dialog_download_choice, null) + .apply { + view = this + edit_start.setText(book.durChapterIndex.toString()) + edit_end.setText(book.totalChapterNum.toString()) + } + } + yesButton { + view?.apply { + val start = edit_start?.text?.toString()?.toInt() ?: 0 + val end = edit_end?.text?.toString()?.toInt() ?: book.totalChapterNum + Download.start(context, book.bookUrl, start, end) + } + } + noButton() + }.show().applyTint() + } + } + + @SuppressLint("InflateParams") + fun showBookMark(context: Context) = with(context) { + val book = ReadBook.book ?: return + val textChapter = ReadBook.curTextChapter ?: return + context.alert(title = getString(R.string.bookmark_add)) { + var editText: EditText? = null + message = book.name + " • " + textChapter.title + customView { + layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { + editText = edit_view.apply { + hint = "备注内容" + } + } + } + yesButton { + editText?.text?.toString()?.let { editContent -> + AsyncTask.execute { + val bookmark = Bookmark( + book.durChapterTime, + book.bookUrl, + book.name, + ReadBook.durChapterIndex, + ReadBook.durPageIndex, + textChapter.title, + editContent + ) + App.db.bookmarkDao().insert(bookmark) + } + } + } + noButton() + }.show().applyTint().requestInputMethod() + } } \ No newline at end of file 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 3125f70d0..7d3d747aa 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 @@ -4,81 +4,96 @@ import android.annotation.SuppressLint import android.app.Activity import android.content.Intent import android.net.Uri -import android.os.AsyncTask.execute import android.os.Bundle -import android.text.SpannableStringBuilder -import android.view.KeyEvent -import android.view.Menu -import android.view.MenuItem -import android.view.View -import android.widget.EditText +import android.os.Handler +import android.view.* +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.core.view.get import androidx.core.view.isVisible +import androidx.core.view.size import androidx.lifecycle.Observer import com.jaredrummler.android.colorpicker.ColorPickerDialogListener -import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.constant.Status import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookChapter -import io.legado.app.data.entities.Bookmark import io.legado.app.help.ReadBookConfig -import io.legado.app.lib.dialogs.* +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.noButton +import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.theme.accentColor import io.legado.app.receiver.TimeElectricityReceiver import io.legado.app.service.BaseReadAloudService -import io.legado.app.service.help.Download import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.read.config.* import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.BG_COLOR import io.legado.app.ui.book.read.config.BgTextConfigDialog.Companion.TEXT_COLOR import io.legado.app.ui.book.read.page.ChapterProvider +import io.legado.app.ui.book.read.page.ContentTextView import io.legado.app.ui.book.read.page.PageView +import io.legado.app.ui.book.read.page.TextPageFactory import io.legado.app.ui.book.read.page.delegate.PageDelegate import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.changesource.ChangeSourceDialog import io.legado.app.ui.chapterlist.ChapterListActivity import io.legado.app.ui.replacerule.ReplaceRuleActivity import io.legado.app.ui.replacerule.edit.ReplaceEditDialog +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_book_read.* -import kotlinx.android.synthetic.main.dialog_download_choice.view.* -import kotlinx.android.synthetic.main.dialog_edit_text.view.* -import kotlinx.android.synthetic.main.view_book_page.* import kotlinx.android.synthetic.main.view_read_menu.* import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivityForResult import org.jetbrains.anko.toast class ReadBookActivity : VMBaseActivity(R.layout.activity_book_read), + View.OnTouchListener, PageView.CallBack, + TextActionMenu.CallBack, + ContentTextView.CallBack, ReadMenu.CallBack, ReadAloudDialog.CallBack, ChangeSourceDialog.CallBack, ReadBook.CallBack, + TocRegexDialog.CallBack, ColorPickerDialogListener { + private val requestCodeChapterList = 568 + private val requestCodeEditSource = 111 + private val requestCodeReplace = 312 + private var menu: Menu? = null + private var textActionMenu: TextActionMenu? = null + override val viewModel: ReadBookViewModel get() = getViewModel(ReadBookViewModel::class.java) - override val isInitFinish: Boolean - get() = viewModel.isInitFinish + override val isInitFinish: Boolean get() = viewModel.isInitFinish - private val requestCodeChapterList = 568 - private val requestCodeEditSource = 111 - private val requestCodeReplace = 31242 + private val mHandler = Handler() + private val keepScreenRunnable: Runnable = Runnable { Help.keepScreenOn(window, false) } + + private var screenTimeOut: Long = 0 private var timeElectricityReceiver: TimeElectricityReceiver? = null + override val pageFactory: TextPageFactory get() = page_view.pageFactory + override val headerHeight: Int get() = page_view.curPage.headerHeight override fun onActivityCreated(savedInstanceState: Bundle?) { Help.upLayoutInDisplayCutoutMode(window) initView() + upScreenTimeOut() ReadBook.callBack = this - ReadBook.titleDate.observe(this, Observer { title_bar.title = it }) + ReadBook.titleDate.observe(this, Observer { + title_bar.title = it + upMenu() + upView() + }) viewModel.initData(intent) } @@ -107,7 +122,10 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo * 初始化View */ private fun initView() { - ChapterProvider.textView = content_text_view + cursor_left.setColorFilter(accentColor) + cursor_right.setColorFilter(accentColor) + cursor_left.setOnTouchListener(this) + cursor_right.setOnTouchListener(this) tv_chapter_name.onClick { ReadBook.webBook?.let { startActivityForResult( @@ -139,10 +157,36 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.onCompatCreateOptionsMenu(menu) } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + this.menu = menu + upMenu() + return super.onPrepareOptionsMenu(menu) + } + + private fun upMenu() { + menu?.let { menu -> + ReadBook.book?.let { book -> + val onLine = !book.isLocalBook() + for (i in 0 until menu.size) { + val item = menu[i] + when (item.groupId) { + R.id.menu_group_on_line -> item.isVisible = onLine + R.id.menu_group_local -> item.isVisible = !onLine + R.id.menu_group_text -> item.isVisible = book.isTxt() + R.id.menu_group_login -> + item.isVisible = !ReadBook.webBook?.bookSource?.loginUrl.isNullOrEmpty() + else -> if (item.itemId == R.id.menu_enable_replace) { + item.isChecked = book.useReplaceRule + } + } + } + } + } + } + /** * 菜单 */ - @SuppressLint("InflateParams") override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_change_source -> { @@ -158,60 +202,24 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo viewModel.refreshContent(it) } } - R.id.menu_download -> ReadBook.book?.let { book -> - alert(titleResource = R.string.download_offline) { - var view: View? = null - customView { - layoutInflater.inflate(R.layout.dialog_download_choice, null).apply { - view = this - edit_start.setText(book.durChapterIndex.toString()) - edit_end.setText(book.totalChapterNum.toString()) - } - } - yesButton { - view?.apply { - val start = edit_start?.text?.toString()?.toInt() ?: 0 - val end = edit_end?.text?.toString()?.toInt() ?: book.totalChapterNum - Download.start(this@ReadBookActivity, book.bookUrl, start, end) - } - } - noButton() - }.show().applyTint() + R.id.menu_download -> Help.showDownloadDialog(this) + R.id.menu_add_bookmark -> Help.showBookMark(this) + R.id.menu_copy_text -> + TextDialog.show(supportFragmentManager, ReadBook.curTextChapter?.getContent()) + R.id.menu_update_toc -> ReadBook.book?.let { + viewModel.loadChapterList(it) } - R.id.menu_add_bookmark -> { - val book = ReadBook.book - val textChapter = ReadBook.curTextChapter - alert(title = getString(R.string.bookmark_add)) { - var editText: EditText? = null - message = book?.name + " • " + textChapter?.title - customView { - layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { - editText = edit_view.apply { - hint = "备注内容" - } - } - } - yesButton { - editText?.text?.toString()?.let { editContent -> - execute { - val bookmark = Bookmark( - book!!.durChapterTime, - book!!.bookUrl, - book!!.name, - ReadBook.durChapterIndex, - ReadBook.durPageIndex, - textChapter!!.title, - editContent) - App.db.bookmarkDao().insert(bookmark) - } - } - } - noButton() - }.show().applyTint().requestInputMethod() + R.id.menu_enable_replace -> ReadBook.book?.let { + it.useReplaceRule = !it.useReplaceRule + menu?.findItem(R.id.menu_enable_replace)?.isChecked = it.useReplaceRule } - R.id.menu_copy_text -> { - + R.id.menu_book_info -> ReadBook.book?.let { + startActivity(Pair("bookUrl", it.bookUrl)) } + R.id.menu_toc_regex -> TocRegexDialog.show( + supportFragmentManager, + ReadBook.book?.tocUrl + ) } return super.onCompatOptionsItemSelected(item) } @@ -313,6 +321,109 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo return super.onKeyUp(keyCode, event) } + /** + * view触摸 + */ + @SuppressLint("ClickableViewAccessibility") + override fun onTouch(v: View, event: MotionEvent): Boolean { + when (event.action) { + MotionEvent.ACTION_MOVE, MotionEvent.ACTION_UP -> { + when (v.id) { + R.id.cursor_left -> page_view.curPage.selectStartMove( + event.rawX + cursor_left.width, + event.rawY - cursor_left.height + ) + R.id.cursor_right -> page_view.curPage.selectEndMove( + event.rawX - cursor_right.width, + event.rawY - cursor_right.height + ) + } + } + } + return true + } + + /** + * 更新文字选择开始位置 + */ + override fun upSelectedStart(x: Float, y: Float) { + cursor_left.x = x - cursor_left.width + cursor_left.y = y + cursor_left.visible(true) + showTextActionMenu() + } + + /** + * 更新文字选择结束位置 + */ + override fun upSelectedEnd(x: Float, y: Float) { + cursor_right.x = x + cursor_right.y = y + cursor_right.visible(true) + showTextActionMenu() + } + + /** + * 取消文字选择 + */ + override fun onCancelSelect() { + cursor_left.invisible() + cursor_right.invisible() + textActionMenu?.dismiss() + } + + /** + * 显示文本操作菜单 + */ + private fun showTextActionMenu() { + textActionMenu ?: let { + textActionMenu = TextActionMenu(this, this) + } + textActionMenu?.let { popup -> + if (!popup.isShowing) { + popup.showAtLocation( + cursor_left, + Gravity.BOTTOM or Gravity.START, + cursor_left.x.toInt() + cursor_left.width, + page_view.height - cursor_left.y.toInt() + ReadBookConfig.durConfig.textSize.dp + popup.height + ) + } else { + popup.update( + cursor_left.x.toInt() + cursor_left.width, + page_view.height - cursor_left.y.toInt() + ReadBookConfig.durConfig.textSize.dp + popup.height, + ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + } + } + + /** + * 当前选择的文本 + */ + override val selectedText: String get() = page_view.curPage.selectedText + + /** + * 文本选择菜单操作 + */ + override fun onMenuItemSelected(item: MenuItemImpl): Boolean { + when (item.itemId) { + R.id.menu_replace -> { + ReplaceEditDialog.show(supportFragmentManager, pattern = selectedText) + return true + } + } + return false + } + + /** + * 文本选择菜单操作完成 + */ + override fun onMenuActionFinally() { + textActionMenu?.dismiss() + page_view.curPage.cancelSelect() + page_view.pageDelegate?.isTextSelected = false + } + /** * 音量键翻页 */ @@ -344,31 +455,48 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } + /** + * 更新内容 + */ override fun upContent(position: Int) { launch { page_view.upContent(position) } } + /** + * 更新视图 + */ override fun upView() { - ReadBook.curTextChapter?.let { - tv_chapter_name.text = it.title - tv_chapter_name.visible() - if (!ReadBook.isLocalBook) { - tv_chapter_url.text = it.url - tv_chapter_url.visible() + launch { + ReadBook.curTextChapter?.let { + tv_chapter_name.text = it.title + tv_chapter_name.visible() + if (!ReadBook.isLocalBook) { + tv_chapter_url.text = it.url + tv_chapter_url.visible() + } + 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 } - seek_read_page.max = it.pageSize().minus(1) - tv_pre.isEnabled = ReadBook.durChapterIndex != 0 - tv_next.isEnabled = ReadBook.durChapterIndex != ReadBook.chapterSize - 1 } } + /** + * 更新进度条 + */ override fun upPageProgress() { - seek_read_page.progress = ReadBook.durPageIndex + launch { + seek_read_page.progress = ReadBook.durPageIndex + } } - override fun showMenu() { + /** + * 显示菜单 + */ + override fun showMenuBar() { read_menu.runMenuIn() } @@ -393,10 +521,16 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } + /** + * 显示朗读菜单 + */ override fun showReadAloudDialog() { ReadAloudDialog().show(supportFragmentManager, "readAloud") } + /** + * 自动翻页 + */ override fun autoPage() { } @@ -446,16 +580,19 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } } - override fun onColorSelected(dialogId: Int, color: Int) = with(ReadBookConfig.getConfig()) { + /** + * colorSelectDialog + */ + override fun onColorSelected(dialogId: Int, color: Int) = with(ReadBookConfig.durConfig) { when (dialogId) { TEXT_COLOR -> { setTextColor(color) - postEvent(Bus.UP_CONFIG, false) + postEvent(EventBus.UP_CONFIG, false) } BG_COLOR -> { setBg(0, "#${color.hexString}") ReadBookConfig.upBg() - postEvent(Bus.UP_CONFIG, false) + postEvent(EventBus.UP_CONFIG, false) } } } @@ -465,6 +602,13 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo */ override fun onDialogDismissed(dialogId: Int) = Unit + override fun onTocRegexDialogResult(tocRegex: String) { + ReadBook.book?.let { + it.tocUrl = tocRegex + viewModel.loadChapterList(it) + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) if (resultCode == Activity.RESULT_OK) { @@ -472,7 +616,9 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo requestCodeEditSource -> viewModel.upBookSource() requestCodeChapterList -> data?.getIntExtra("index", ReadBook.durChapterIndex)?.let { index -> - viewModel.openChapter(index, data?.getIntExtra("pageIndex", ReadBook.durPageIndex)) + if (index != ReadBook.durChapterIndex) { + viewModel.openChapter(index) + } } requestCodeReplace -> ReadBook.loadContent() } @@ -493,59 +639,95 @@ class ReadBookActivity : VMBaseActivity(R.layout.activity_boo } ?: super.finish() } + override fun onDestroy() { + super.onDestroy() + mHandler.removeCallbacks(keepScreenRunnable) + textActionMenu?.dismiss() + page_view.onDestroy() + } + override fun observeLiveBus() { super.observeLiveBus() - observeEvent(Bus.ALOUD_STATE) { - if (it == Status.STOP || it == Status.PAUSE) { - ReadBook.curTextChapter?.let { textChapter -> - val page = textChapter.page(ReadBook.durPageIndex) - if (page != null && page.text is SpannableStringBuilder) { - page.text.removeSpan(ChapterProvider.readAloudSpan) - page_view.upContent() - } - } - } - } - observeEvent(Bus.TIME_CHANGED) { page_view.upTime() } - observeEvent(Bus.BATTERY_CHANGED) { page_view.upBattery(it) } - observeEvent(Bus.OPEN_CHAPTER) { + observeEvent(EventBus.TIME_CHANGED) { page_view.upTime() } + observeEvent(EventBus.BATTERY_CHANGED) { page_view.upBattery(it) } + observeEvent(EventBus.OPEN_CHAPTER) { viewModel.openChapter(it.index, ReadBook.durPageIndex) page_view.upContent() } - observeEvent(Bus.MEDIA_BUTTON) { + observeEvent(EventBus.MEDIA_BUTTON) { if (it) { onClickReadAloud() } else { ReadBook.readAloud(!BaseReadAloudService.pause) } } - observeEvent(Bus.UP_CONFIG) { + observeEvent(EventBus.UP_CONFIG) { upSystemUiVisibility() - content_view.upStyle() page_view.upBg() page_view.upStyle() + ChapterProvider.upStyle(ReadBookConfig.durConfig) if (it) { ReadBook.loadContent() } else { page_view.upContent() } } - observeEventSticky(Bus.TTS_START) { chapterStart -> + observeEvent(EventBus.ALOUD_STATE) { + if (it == Status.STOP || it == Status.PAUSE) { + ReadBook.curTextChapter?.let { textChapter -> + val page = textChapter.page(ReadBook.durPageIndex) + if (page != null) { + page.removePageAloudSpan() + page_view.upContent() + } + } + } + } + observeEventSticky(EventBus.TTS_PROGRESS) { chapterStart -> launch(IO) { if (BaseReadAloudService.isPlay()) { - ReadBook.curTextChapter?.let { - val pageStart = chapterStart - it.getReadLength(ReadBook.durPageIndex) - it.page(ReadBook.durPageIndex)?.upPageAloudSpan(pageStart) - withContext(Main) { - page_view.upContent() - } + ReadBook.curTextChapter?.let { textChapter -> + val pageStart = + chapterStart - textChapter.getReadLength(ReadBook.durPageIndex) + textChapter.page(ReadBook.durPageIndex)?.upPageAloudSpan(pageStart) + upContent() } } } } - observeEvent(Bus.REPLACE) { + observeEvent(EventBus.REPLACE) { ReplaceEditDialog().show(supportFragmentManager, "replaceEditDialog") } + observeEvent(PreferKey.keepLight) { + upScreenTimeOut() + } + observeEvent(PreferKey.textSelectAble) { + page_view.upSelectAble(it) + } } + private fun upScreenTimeOut() { + getPrefString(PreferKey.keepLight)?.let { + screenTimeOut = it.toLong() * 1000 + } + screenOffTimerStart() + } + + /** + * 重置黑屏时间 + */ + override fun screenOffTimerStart() { + if (screenTimeOut < 0) { + Help.keepScreenOn(window, true) + return + } + val t = screenTimeOut - getScreenOffTime() + if (t > 0) { + mHandler.removeCallbacks(keepScreenRunnable) + Help.keepScreenOn(window, true) + mHandler.postDelayed(keepScreenRunnable, screenTimeOut) + } else { + Help.keepScreenOn(window, false) + } + } } \ No newline at end of file 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 959068a8a..2ebab7875 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 @@ -10,6 +10,7 @@ import io.legado.app.data.entities.BookChapter import io.legado.app.help.BookHelp import io.legado.app.help.IntentDataHelp import io.legado.app.model.WebBook +import io.legado.app.model.localBook.AnalyzeTxtFile import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook @@ -59,6 +60,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } else { isInitFinish = true ReadBook.titleDate.postValue(book.name) + ReadBook.upWebBook(book.origin) ReadBook.chapterSize = App.db.bookChapterDao().getChapterCount(book.bookUrl) if (ReadBook.chapterSize == 0) { if (book.tocUrl.isEmpty()) { @@ -79,41 +81,56 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { changeDruChapterIndex: ((chapters: List) -> Unit)? = null ) { execute { - ReadBook.webBook?.getBookInfo(book, this) - ?.onSuccess { - loadChapterList(book, changeDruChapterIndex) - } + if (book.isLocalBook()) { + loadChapterList(book, changeDruChapterIndex) + } else { + ReadBook.webBook?.getBookInfo(book, this) + ?.onSuccess { + loadChapterList(book, changeDruChapterIndex) + } + } } } - private fun loadChapterList( + fun loadChapterList( book: Book, changeDruChapterIndex: ((chapters: List) -> Unit)? = null ) { execute { - ReadBook.webBook?.getChapterList(book, this) - ?.onSuccess(IO) { cList -> - if (!cList.isNullOrEmpty()) { - if (changeDruChapterIndex == null) { - App.db.bookChapterDao().insert(*cList.toTypedArray()) - ReadBook.chapterSize = cList.size - ReadBook.loadContent() + if (book.isLocalBook()) { + 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.loadContent() + } + } else { + ReadBook.webBook?.getChapterList(book, this) + ?.onSuccess(IO) { cList -> + if (!cList.isNullOrEmpty()) { + if (changeDruChapterIndex == null) { + App.db.bookChapterDao().insert(*cList.toTypedArray()) + App.db.bookDao().update(book) + ReadBook.chapterSize = cList.size + ReadBook.loadContent() + } else { + changeDruChapterIndex(cList) + } } else { - changeDruChapterIndex(cList) + toast(R.string.error_load_toc) } - } else { + }?.onError { toast(R.string.error_load_toc) - } - }?.onError { - toast(R.string.error_load_toc) - } ?: autoChangeSource() + } ?: autoChangeSource() + } } } fun changeTo(book1: Book) { execute { ReadBook.book?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(it) } ReadBook.prevTextChapter = null ReadBook.curTextChapter = null @@ -154,7 +171,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { } } - fun openChapter(index: Int, pageIndex: Int) { + fun openChapter(index: Int, pageIndex: Int = 0) { ReadBook.prevTextChapter = null ReadBook.curTextChapter = null ReadBook.nextTextChapter = null @@ -170,7 +187,7 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) { fun removeFromBookshelf(success: (() -> Unit)?) { execute { ReadBook.book?.let { - App.db.bookDao().delete(it.bookUrl) + App.db.bookDao().delete(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 19729f20b..12bbb6d8b 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 @@ -11,6 +11,7 @@ import androidx.core.view.isVisible import io.legado.app.App import io.legado.app.R import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig import io.legado.app.lib.theme.accentColor import io.legado.app.lib.theme.buttonDisabledColor import io.legado.app.service.help.ReadBook @@ -41,7 +42,7 @@ class ReadMenu : FrameLayout { init { callBack = activity as? CallBack inflate(context, R.layout.view_read_menu, this) - if (context.isNightTheme) { + if (AppConfig.isNightTheme) { fabNightTheme.setImageResource(R.drawable.ic_daytime) } else { fabNightTheme.setImageResource(R.drawable.ic_brightness) @@ -144,7 +145,7 @@ class ReadMenu : FrameLayout { //夜间模式 fabNightTheme.onClick { - context.putPrefBoolean("isNightTheme", !context.isNightTheme) + AppConfig.isNightTheme = !AppConfig.isNightTheme App.INSTANCE.applyDayNight() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt new file mode 100644 index 000000000..fe56a18be --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/TextActionMenu.kt @@ -0,0 +1,157 @@ +package io.legado.app.ui.book.read + +import android.annotation.SuppressLint +import android.app.SearchManager +import android.content.Context +import android.content.Intent +import android.content.pm.ResolveInfo +import android.net.Uri +import android.os.Build +import android.view.LayoutInflater +import android.view.Menu +import android.view.ViewGroup +import android.widget.PopupWindow +import androidx.annotation.RequiresApi +import androidx.appcompat.view.SupportMenuInflater +import androidx.appcompat.view.menu.MenuBuilder +import androidx.appcompat.view.menu.MenuItemImpl +import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.utils.isAbsUrl +import io.legado.app.utils.sendToClip +import kotlinx.android.synthetic.main.item_fillet_text.view.* +import kotlinx.android.synthetic.main.popup_action_menu.view.* +import org.jetbrains.anko.sdk27.listeners.onClick +import org.jetbrains.anko.toast + + +class TextActionMenu(private val context: Context, private val callBack: CallBack) : + PopupWindow(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT) { + + init { + @SuppressLint("InflateParams") + contentView = LayoutInflater.from(context).inflate(R.layout.popup_action_menu, null) + + isTouchable = true + isOutsideTouchable = false + isFocusable = false + + initRecyclerView() + } + + private fun initRecyclerView() = with(contentView) { + val adapter = Adapter(context) + recycler_view.layoutManager = LinearLayoutManager(context, RecyclerView.HORIZONTAL, false) + recycler_view.adapter = adapter + val menu = MenuBuilder(context) + SupportMenuInflater(context).inflate(R.menu.content_select_action, menu) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + onInitializeMenu(menu) + } + adapter.setItems(menu.visibleItems) + } + + inner class Adapter(context: Context) : + SimpleRecyclerAdapter(context, R.layout.item_text) { + + override fun convert( + holder: ItemViewHolder, + item: MenuItemImpl, + payloads: MutableList + ) { + with(holder.itemView) { + text_view.text = item.title + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + if (!callBack.onMenuItemSelected(it)) { + onMenuItemSelected(it) + } + } + } + } + } + + private fun onMenuItemSelected(item: MenuItemImpl) { + when (item.itemId) { + R.id.menu_copy -> context.sendToClip(callBack.selectedText) + R.id.menu_browser -> { + try { + val intent = if (callBack.selectedText.isAbsUrl()) { + Intent(Intent.ACTION_VIEW).apply { + data = Uri.parse(callBack.selectedText) + } + } else { + Intent(Intent.ACTION_WEB_SEARCH).apply { + putExtra(SearchManager.QUERY, callBack.selectedText) + } + } + context.startActivity(intent) + } catch (e: Exception) { + e.printStackTrace() + context.toast(e.localizedMessage ?: "ERROR") + } + } + else -> item.intent?.let { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + it.putExtra(Intent.EXTRA_PROCESS_TEXT, callBack.selectedText) + context.startActivity(it) + } + } + } + callBack.onMenuActionFinally() + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntent(): Intent { + return Intent() + .setAction(Intent.ACTION_PROCESS_TEXT) + .setType("text/plain") + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun getSupportedActivities(): List { + return context.packageManager + .queryIntentActivities(createProcessTextIntent(), 0) + } + + @RequiresApi(Build.VERSION_CODES.M) + private fun createProcessTextIntentForResolveInfo(info: ResolveInfo): Intent { + return createProcessTextIntent() + .putExtra(Intent.EXTRA_PROCESS_TEXT_READONLY, false) + .setClassName(info.activityInfo.packageName, info.activityInfo.name) + } + + /** + * Start with a menu Item order value that is high enough + * so that your "PROCESS_TEXT" menu items appear after the + * standard selection menu items like Cut, Copy, Paste. + */ + @RequiresApi(Build.VERSION_CODES.M) + private fun onInitializeMenu(menu: Menu) { + // Start with a menu Item order value that is high enough + // so that your "PROCESS_TEXT" menu items appear after the + // standard selection menu items like Cut, Copy, Paste. + var menuItemOrder = 100 + for (resolveInfo in getSupportedActivities()) { + menu.add( + Menu.NONE, Menu.NONE, + menuItemOrder++, resolveInfo.loadLabel(context.packageManager) + ).intent = createProcessTextIntentForResolveInfo(resolveInfo) + } + } + + interface CallBack { + val selectedText: String + + fun onMenuItemSelected(item: MenuItemImpl): Boolean + + fun onMenuActionFinally() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt index 2b4a7dc4f..5a3d3ec8b 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/BgTextConfigDialog.kt @@ -20,17 +20,13 @@ import com.jaredrummler.android.colorpicker.ColorPickerDialog import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -import io.legado.app.constant.Bus -import io.legado.app.help.FileHelp +import io.legado.app.constant.EventBus import io.legado.app.help.ImageLoader import io.legado.app.help.ReadBookConfig import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat import io.legado.app.ui.book.read.Help -import io.legado.app.utils.DocumentUtils -import io.legado.app.utils.FileUtils -import io.legado.app.utils.getCompatColor -import io.legado.app.utils.postEvent +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_read_bg_text.* import kotlinx.android.synthetic.main.item_bg_image.view.* import org.jetbrains.anko.sdk27.listeners.onCheckedChange @@ -55,8 +51,8 @@ class BgTextConfigDialog : DialogFragment() { it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { - it.setBackgroundDrawableResource(R.color.transparent) - it.decorView.setPadding(0, 0, 0, 0) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 5, 0, 0) val attr = it.attributes attr.dimAmount = 0.0f attr.gravity = Gravity.BOTTOM @@ -85,7 +81,7 @@ class BgTextConfigDialog : DialogFragment() { } @SuppressLint("InflateParams") - private fun initData() = with(ReadBookConfig.getConfig()) { + private fun initData() = with(ReadBookConfig.durConfig) { sw_dark_status_icon.isChecked = statusIconDark() adapter = BgAdapter(requireContext()) recycler_view.layoutManager = @@ -103,7 +99,7 @@ class BgTextConfigDialog : DialogFragment() { } } - private fun initView() = with(ReadBookConfig.getConfig()) { + private fun initView() = with(ReadBookConfig.durConfig) { sw_dark_status_icon.onCheckedChange { buttonView, isChecked -> if (buttonView?.isPressed == true) { setStatusIconDark(isChecked) @@ -132,7 +128,8 @@ class BgTextConfigDialog : DialogFragment() { .show(requireActivity()) } tv_default.onClick { - + ReadBookConfig.resetDur() + postEvent(EventBus.UP_CONFIG, false) } } @@ -152,14 +149,20 @@ class BgTextConfigDialog : DialogFragment() { .centerCrop() .into(iv_bg) tv_name.text = item.substringBeforeLast(".") + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { this.onClick { - ReadBookConfig.getConfig().setBg(1, item) - ReadBookConfig.upBg() - postEvent(Bus.UP_CONFIG, false) + getItemByLayoutPosition(holder.layoutPosition)?.let { + ReadBookConfig.durConfig.setBg(1, it) + ReadBookConfig.upBg() + postEvent(EventBus.UP_CONFIG, false) + } } } } - } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { @@ -168,18 +171,18 @@ class BgTextConfigDialog : DialogFragment() { resultSelectBg -> { if (resultCode == RESULT_OK) { data?.data?.let { uri -> - if (DocumentFile.isDocumentUri(requireContext(), uri)) { + if (uri.toString().isContentPath()) { val doc = DocumentFile.fromSingleUri(requireContext(), uri) doc?.let { var file = requireContext().getExternalFilesDir(null) ?: requireContext().filesDir file = - FileHelp.getFile(file.absolutePath + File.separator + "bg" + File.separator + doc.name) + FileUtils.createFileIfNotExist(file.absolutePath + File.separator + "bg" + File.separator + doc.name) DocumentUtils.readBytes(requireContext(), uri)?.let { file.writeBytes(it) - ReadBookConfig.getConfig().setBg(2, file.absolutePath) + ReadBookConfig.durConfig.setBg(2, file.absolutePath) ReadBookConfig.upBg() - postEvent(Bus.UP_CONFIG, false) + postEvent(EventBus.UP_CONFIG, false) } } } else { @@ -190,10 +193,10 @@ class BgTextConfigDialog : DialogFragment() { ) .rationale(R.string.bg_image_per) .onGranted { - FileUtils.getPath(requireContext(), uri)?.let { path -> - ReadBookConfig.getConfig().setBg(2, path) + RealPathUtil.getPath(requireContext(), uri)?.let { path -> + ReadBookConfig.durConfig.setBg(2, path) ReadBookConfig.upBg() - postEvent(Bus.UP_CONFIG, false) + postEvent(EventBus.UP_CONFIG, false) } } .request() diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt new file mode 100644 index 000000000..dc8f36d67 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ChineseConverter.kt @@ -0,0 +1,53 @@ +package io.legado.app.ui.book.read.config + +import android.content.Context +import android.text.Spannable +import android.text.SpannableString +import android.text.style.ForegroundColorSpan +import android.util.AttributeSet +import io.legado.app.R +import io.legado.app.help.AppConfig +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.widget.text.StrokeTextView +import org.jetbrains.anko.sdk27.listeners.onClick + +class ChineseConverter(context: Context, attrs: AttributeSet?) : StrokeTextView(context, attrs) { + + private val spannableString = SpannableString("简/繁") + private var enabledSpan: ForegroundColorSpan = ForegroundColorSpan(context.accentColor) + private var onChanged: (() -> Unit)? = null + + init { + text = spannableString + if (!isInEditMode) { + upUi(AppConfig.chineseConverterType) + } + onClick { + selectType() + } + } + + private fun upUi(type: Int) { + spannableString.removeSpan(enabledSpan) + when (type) { + 1 -> spannableString.setSpan(enabledSpan, 0, 1, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + 2 -> spannableString.setSpan(enabledSpan, 2, 3, Spannable.SPAN_INCLUSIVE_EXCLUSIVE) + } + text = spannableString + } + + private fun selectType() { + context.alert(titleResource = R.string.chinese_converter) { + items(context.resources.getStringArray(R.array.chinese_mode).toList()) { _, i -> + AppConfig.chineseConverterType = i + upUi(i) + onChanged?.invoke() + } + }.show() + } + + fun onChanged(unit: () -> Unit) { + onChanged = unit + } +} \ No newline at end of file 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 899d58d63..b087a1508 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 @@ -12,10 +12,11 @@ import androidx.fragment.app.DialogFragment import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.R -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.lib.theme.ATH import io.legado.app.ui.book.read.Help +import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.postEvent class MoreConfigDialog : DialogFragment() { @@ -29,8 +30,8 @@ class MoreConfigDialog : DialogFragment() { it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { - it.setBackgroundDrawableResource(R.color.transparent) - it.decorView.setPadding(0, 0, 0, 0) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 5, 0, 0) val attr = it.attributes attr.dimAmount = 0.0f attr.gravity = Gravity.BOTTOM @@ -54,8 +55,7 @@ class MoreConfigDialog : DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) var preferenceFragment = childFragmentManager.findFragmentByTag(readPreferTag) - if (preferenceFragment == null) preferenceFragment = - ReadPreferenceFragment() + if (preferenceFragment == null) preferenceFragment = ReadPreferenceFragment() childFragmentManager.beginTransaction() .replace(view.id, preferenceFragment, readPreferTag) .commit() @@ -92,9 +92,10 @@ class MoreConfigDialog : DialogFragment() { key: String? ) { when (key) { - PreferKey.hideStatusBar -> postEvent(Bus.UP_CONFIG, true) - PreferKey.hideNavigationBar -> postEvent(Bus.UP_CONFIG, true) - PreferKey.clickAllNext -> postEvent(Bus.UP_CONFIG, true) + PreferKey.hideStatusBar -> postEvent(EventBus.UP_CONFIG, true) + PreferKey.hideNavigationBar -> postEvent(EventBus.UP_CONFIG, true) + PreferKey.keepLight -> postEvent(key, true) + PreferKey.textSelectAble -> postEvent(key, getPrefBoolean(key)) } } 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 fe04055e9..1cb0715e3 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 @@ -5,16 +5,13 @@ import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.SeekBar import androidx.fragment.app.DialogFragment import io.legado.app.R -import io.legado.app.constant.Bus +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.postEvent -import io.legado.app.utils.progressAdd import kotlinx.android.synthetic.main.dialog_read_padding.* -import org.jetbrains.anko.sdk27.listeners.onClick class PaddingConfigDialog : DialogFragment() { @@ -26,8 +23,6 @@ class PaddingConfigDialog : DialogFragment() { it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { - it.setBackgroundDrawableResource(R.color.transparent) - it.decorView.setPadding(0, 0, 0, 0) val attr = it.attributes attr.dimAmount = 0.0f it.attributes = attr @@ -49,99 +44,81 @@ class PaddingConfigDialog : DialogFragment() { initView() } - private fun initData() = with(ReadBookConfig.getConfig()) { - seek_padding_top.progress = paddingTop - seek_padding_bottom.progress = paddingBottom - seek_padding_left.progress = paddingLeft - seek_padding_right.progress = paddingRight - tv_padding_top.text = paddingTop.toString() - tv_padding_bottom.text = paddingBottom.toString() - tv_padding_left.text = paddingLeft.toString() - tv_padding_right.text = paddingRight.toString() + override fun onDestroy() { + super.onDestroy() + ReadBookConfig.save() } - private fun initView() = with(ReadBookConfig.getConfig()) { - iv_padding_top_add.onClick { - seek_padding_top.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) + private fun initData() = with(ReadBookConfig.durConfig) { + //正文 + dsb_padding_top.progress = paddingTop + dsb_padding_bottom.progress = paddingBottom + dsb_padding_left.progress = paddingLeft + dsb_padding_right.progress = paddingRight + //页眉 + dsb_header_padding_top.progress = headerPaddingTop + dsb_header_padding_bottom.progress = headerPaddingBottom + dsb_header_padding_left.progress = headerPaddingLeft + dsb_header_padding_right.progress = headerPaddingRight + //页脚 + dsb_footer_padding_top.progress = footerPaddingTop + dsb_footer_padding_bottom.progress = footerPaddingBottom + dsb_footer_padding_left.progress = footerPaddingLeft + dsb_footer_padding_right.progress = footerPaddingRight + } + + private fun initView() = with(ReadBookConfig.durConfig) { + //正文 + dsb_padding_top.onChanged = { + paddingTop = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_top_remove.onClick { - seek_padding_top.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_padding_bottom.onChanged = { + paddingBottom = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_bottom_add.onClick { - seek_padding_bottom.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) + dsb_padding_left.onChanged = { + paddingLeft = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_bottom_remove.onClick { - seek_padding_bottom.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_padding_right.onChanged = { + paddingRight = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_left_add.onClick { - seek_padding_left.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) + //页眉 + dsb_header_padding_top.onChanged = { + headerPaddingTop = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_left_remove.onClick { - seek_padding_left.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_header_padding_bottom.onChanged = { + headerPaddingBottom = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_right_add.onClick { - seek_padding_right.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) + dsb_header_padding_left.onChanged = { + headerPaddingLeft = it + postEvent(EventBus.UP_CONFIG, true) } - iv_padding_right_remove.onClick { - seek_padding_right.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_header_padding_right.onChanged = { + headerPaddingRight = it + postEvent(EventBus.UP_CONFIG, true) + } + //页脚 + dsb_footer_padding_top.onChanged = { + footerPaddingTop = it + postEvent(EventBus.UP_CONFIG, true) + } + dsb_footer_padding_bottom.onChanged = { + footerPaddingBottom = it + postEvent(EventBus.UP_CONFIG, true) + } + dsb_footer_padding_left.onChanged = { + footerPaddingLeft = it + postEvent(EventBus.UP_CONFIG, true) + } + dsb_footer_padding_right.onChanged = { + footerPaddingRight = it + postEvent(EventBus.UP_CONFIG, true) } - - seek_padding_top.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - paddingTop = progress - tv_padding_top.text = paddingTop.toString() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - seek_padding_bottom.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - paddingBottom = progress - tv_padding_bottom.text = paddingBottom.toString() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - seek_padding_left.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - paddingLeft = progress - tv_padding_left.text = paddingLeft.toString() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - seek_padding_right.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - paddingRight = progress - tv_padding_right.text = paddingRight.toString() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) } } 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 0d066dd54..294a1c127 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 @@ -12,12 +12,13 @@ import androidx.preference.ListPreference import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.R -import io.legado.app.constant.Bus +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.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.ui.book.read.Help -import io.legado.app.utils.getPrefString import io.legado.app.utils.postEvent class ReadAloudConfigDialog : DialogFragment() { @@ -58,17 +59,19 @@ class ReadAloudConfigDialog : DialogFragment() { } class ReadAloudPreferenceFragment : PreferenceFragmentCompat(), - SharedPreferences.OnSharedPreferenceChangeListener, - Preference.OnPreferenceChangeListener { + SharedPreferences.OnSharedPreferenceChangeListener { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_aloud) + upPreferenceSummary( + findPreference(PreferKey.ttsSpeechPer), + AppConfig.ttsSpeechPer + ) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) ATH.applyEdgeEffectColor(listView) - bindPreferenceSummaryToValue(findPreference("ttsSpeechPer")) } override fun onResume() { @@ -86,42 +89,35 @@ class ReadAloudConfigDialog : DialogFragment() { key: String? ) { when (key) { - "readAloudByPage" -> { + PreferKey.readAloudByPage -> { if (BaseReadAloudService.isRun) { - postEvent(Bus.MEDIA_BUTTON, false) + postEvent(EventBus.MEDIA_BUTTON, false) } } - "readAloudOnLine" -> { - if (BaseReadAloudService.isRun) { - ReadAloud.stop(requireContext()) - ReadAloud.aloudClass = ReadAloud.getReadAloudClass() - } + PreferKey.readAloudOnLine -> { + ReadAloud.stop(requireContext()) + ReadAloud.aloudClass = ReadAloud.getReadAloudClass() + } + PreferKey.ttsSpeechPer -> { + upPreferenceSummary( + findPreference(PreferKey.ttsSpeechPer), + AppConfig.ttsSpeechPer + ) + ReadAloud.upTtsSpeechRate(requireContext()) } - "ttsSpeechPer" -> ReadAloud.upTtsSpeechRate(requireContext()) - } - } - - override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - val stringValue = newValue.toString() - - if (preference is ListPreference) { - val index = preference.findIndexOfValue(stringValue) - // Set the summary to reflect the new value. - preference.setSummary(if (index >= 0) preference.entries[index] else null) - } else { - // For all other preferences, set the summary to the value's - preference?.summary = stringValue } - return true } - private fun bindPreferenceSummaryToValue(preference: Preference?) { - preference?.apply { - onPreferenceChangeListener = this@ReadAloudPreferenceFragment - onPreferenceChange( - this, - context.getPrefString(key) - ) + private fun upPreferenceSummary(preference: Preference?, value: String) { + when (preference) { + is ListPreference -> { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } + else -> { + preference?.summary = value + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt index a20ac2efe..6040c0880 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/config/ReadAloudDialog.kt @@ -9,12 +9,15 @@ import android.view.ViewGroup import android.widget.SeekBar import androidx.fragment.app.DialogFragment import io.legado.app.R -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus +import io.legado.app.help.AppConfig import io.legado.app.service.BaseReadAloudService import io.legado.app.service.help.ReadAloud import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.Help -import io.legado.app.utils.* +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.observeEvent +import io.legado.app.utils.putPrefBoolean import kotlinx.android.synthetic.main.dialog_read_aloud.* import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick @@ -57,15 +60,15 @@ class ReadAloudDialog : DialogFragment() { } private fun initData() { - observeEvent(Bus.ALOUD_STATE) { upPlayState() } - observeEvent(Bus.TTS_DS) { seek_timer.progress = it } + observeEvent(EventBus.ALOUD_STATE) { upPlayState() } + observeEvent(EventBus.TTS_DS) { seek_timer.progress = it } upPlayState() seek_timer.progress = BaseReadAloudService.timeMinute tv_timer.text = requireContext().getString(R.string.timer_m, BaseReadAloudService.timeMinute) cb_tts_follow_sys.isChecked = requireContext().getPrefBoolean("ttsFollowSys", true) seek_tts_SpeechRate.isEnabled = !cb_tts_follow_sys.isChecked - seek_tts_SpeechRate.progress = requireContext().getPrefInt("ttsSpeechRate", 5) + seek_tts_SpeechRate.progress = AppConfig.ttsSpeechRate } private fun initOnChange() { @@ -83,7 +86,7 @@ class ReadAloudDialog : DialogFragment() { override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit override fun onStopTrackingTouch(seekBar: SeekBar?) { - requireContext().putPrefInt("ttsSpeechRate", seek_tts_SpeechRate.progress) + AppConfig.ttsSpeechRate = seek_tts_SpeechRate.progress upTtsSpeechRate() } }) @@ -101,7 +104,7 @@ class ReadAloudDialog : DialogFragment() { } private fun initOnClick() { - iv_menu.onClick { callBack?.showMenu(); dismiss() } + iv_menu.onClick { callBack?.showMenuBar(); dismiss() } iv_other_config.onClick { ReadAloudConfigDialog().show(childFragmentManager, "readAloudConfigDialog") } @@ -135,7 +138,7 @@ class ReadAloudDialog : DialogFragment() { } interface CallBack { - fun showMenu() + fun showMenuBar() fun openChapterList() fun onClickReadAloud() fun finish() 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 3bc5060b0..b1f80b354 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 @@ -6,11 +6,10 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.SeekBar import androidx.core.view.get import androidx.fragment.app.DialogFragment import io.legado.app.R -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.help.ImageLoader import io.legado.app.help.ReadBookConfig @@ -37,8 +36,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { it.windowManager?.defaultDisplay?.getMetrics(dm) } dialog?.window?.let { - it.setBackgroundDrawableResource(R.color.transparent) - it.decorView.setPadding(0, 0, 0, 0) + it.setBackgroundDrawableResource(R.color.background) + it.decorView.setPadding(0, 5, 0, 0) val attr = it.attributes attr.dimAmount = 0.0f attr.gravity = Gravity.BOTTOM @@ -57,8 +56,9 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + initView() initData() - initOnClick() + initViewEvent() } override fun onDestroy() { @@ -66,7 +66,17 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { ReadBookConfig.save() } + private fun initView() { + dsb_text_size.valueFormat = { + (it + 5).toString() + } + dsb_text_letter_spacing.valueFormat = { + ((it - 50) / 100f).toString() + } + } + private fun initData() { + cb_share_layout.isChecked = getPrefBoolean(PreferKey.shareLayout) requireContext().getPrefInt(PreferKey.pageAnim).let { if (it >= 0 && it < rg_page_anim.childCount) { rg_page_anim.check(rg_page_anim[it].id) @@ -77,13 +87,23 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { upBg() } - private fun initOnClick() { + private fun initViewEvent() { + chinese_converter.onChanged { + postEvent(EventBus.UP_CONFIG, true) + } + tv_title_center.onClick { + ReadBookConfig.durConfig.apply { + titleCenter = !titleCenter + tv_title_center.isSelected = titleCenter + } + postEvent(EventBus.UP_CONFIG, true) + } tv_text_bold.onClick { - with(ReadBookConfig.getConfig()) { + ReadBookConfig.durConfig.apply { textBold = !textBold tv_text_bold.isSelected = textBold } - postEvent(Bus.UP_CONFIG, false) + postEvent(EventBus.UP_CONFIG, false) } tv_text_font.onClick { FontSelectDialog().show(childFragmentManager, "fontSelectDialog") @@ -93,8 +113,8 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { title = getString(R.string.text_indent), items = resources.getStringArray(R.array.indent).toList() ) { _, index -> - putPrefInt("textIndent", index) - postEvent(Bus.UP_CONFIG, true) + putPrefInt(PreferKey.bodyIndent, index) + postEvent(EventBus.UP_CONFIG, true) } } tv_padding.onClick { @@ -104,68 +124,21 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { activity.showPaddingConfig() } } - seek_text_size.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - ReadBookConfig.getConfig().textSize = progress + 5 - tv_text_size.text = ReadBookConfig.getConfig().textSize.toString() - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - iv_text_size_add.onClick { - seek_text_size.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) - } - iv_text_size_remove.onClick { - seek_text_size.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_text_size.onChanged = { + ReadBookConfig.durConfig.textSize = it + 5 + postEvent(EventBus.UP_CONFIG, true) } - seek_text_letter_spacing.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - with(ReadBookConfig.getConfig()) { - letterSpacing = (seek_text_letter_spacing.progress - 5) / 10f - tv_text_letter_spacing.text = letterSpacing.toString() - } - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - iv_text_letter_spacing_add.onClick { - seek_text_letter_spacing.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) + dsb_text_letter_spacing.onChanged = { + ReadBookConfig.durConfig.letterSpacing = (it - 50) / 100f + postEvent(EventBus.UP_CONFIG, true) } - iv_text_letter_spacing_remove.onClick { - seek_text_letter_spacing.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_line_size.onChanged = { + ReadBookConfig.durConfig.lineSpacingExtra = it + postEvent(EventBus.UP_CONFIG, true) } - seek_line_size.setOnSeekBarChangeListener(object : - SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - with(ReadBookConfig.getConfig()) { - lineSpacingExtra = seek_line_size.progress - tv_line_size.text = lineSpacingExtra.toString() - } - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) = Unit - override fun onStopTrackingTouch(seekBar: SeekBar?) { - postEvent(Bus.UP_CONFIG, true) - } - }) - iv_line_size_add.onClick { - seek_line_size.progressAdd(1) - postEvent(Bus.UP_CONFIG, true) - } - iv_line_size_remove.onClick { - seek_line_size.progressAdd(-1) - postEvent(Bus.UP_CONFIG, true) + dsb_paragraph_spacing.onChanged = { + ReadBookConfig.durConfig.paragraphSpacing = it + postEvent(EventBus.UP_CONFIG, true) } rg_page_anim.onCheckedChange { _, checkedId -> for (i in 0 until rg_page_anim.childCount) { @@ -179,16 +152,21 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { } } } - tv_bg0.onClick { changeBg(0) } - tv_bg0.onLongClick { showBgTextConfig(0) } - tv_bg1.onClick { changeBg(1) } - tv_bg1.onLongClick { showBgTextConfig(1) } - tv_bg2.onClick { changeBg(2) } - tv_bg2.onLongClick { showBgTextConfig(2) } - tv_bg3.onClick { changeBg(3) } - tv_bg3.onLongClick { showBgTextConfig(3) } - tv_bg4.onClick { changeBg(4) } - tv_bg4.onLongClick { showBgTextConfig(4) } + cb_share_layout.onCheckedChangeListener = { checkBox, isChecked -> + if (checkBox.isPressed) { + putPrefBoolean(PreferKey.shareLayout, isChecked) + } + } + bg0.onClick { changeBg(0) } + bg0.onLongClick { showBgTextConfig(0) } + bg1.onClick { changeBg(1) } + bg1.onLongClick { showBgTextConfig(1) } + bg2.onClick { changeBg(2) } + bg2.onLongClick { showBgTextConfig(2) } + bg3.onClick { changeBg(3) } + bg3.onLongClick { showBgTextConfig(3) } + bg4.onClick { changeBg(4) } + bg4.onLongClick { showBgTextConfig(4) } } private fun changeBg(index: Int) { @@ -197,7 +175,7 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { ReadBookConfig.upBg() upStyle() upBg() - postEvent(Bus.UP_CONFIG, true) + postEvent(EventBus.UP_CONFIG, true) } } @@ -212,23 +190,22 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { } private fun upStyle() { - ReadBookConfig.getConfig().let { + ReadBookConfig.durConfig.let { + tv_title_center.isSelected = it.titleCenter tv_text_bold.isSelected = it.textBold - seek_text_size.progress = it.textSize - 5 - tv_text_size.text = it.textSize.toString() - seek_text_letter_spacing.progress = (it.letterSpacing * 10).toInt() + 5 - tv_text_letter_spacing.text = it.letterSpacing.toString() - seek_line_size.progress = it.lineSpacingExtra - tv_line_size.text = it.lineSpacingExtra.toString() + dsb_text_size.progress = it.textSize - 5 + dsb_text_letter_spacing.progress = (it.letterSpacing * 100).toInt() + 50 + dsb_line_size.progress = it.lineSpacingExtra + dsb_paragraph_spacing.progress = it.paragraphSpacing } } private fun setBg() { - tv_bg0.setTextColor(ReadBookConfig.getConfig(0).textColor()) - tv_bg1.setTextColor(ReadBookConfig.getConfig(1).textColor()) - tv_bg2.setTextColor(ReadBookConfig.getConfig(2).textColor()) - tv_bg3.setTextColor(ReadBookConfig.getConfig(3).textColor()) - tv_bg4.setTextColor(ReadBookConfig.getConfig(4).textColor()) + bg0.setTextColor(ReadBookConfig.getConfig(0).textColor()) + bg1.setTextColor(ReadBookConfig.getConfig(1).textColor()) + bg2.setTextColor(ReadBookConfig.getConfig(2).textColor()) + bg3.setTextColor(ReadBookConfig.getConfig(3).textColor()) + bg4.setTextColor(ReadBookConfig.getConfig(4).textColor()) for (i in 0..4) { val iv = when (i) { 1 -> bg1 @@ -266,6 +243,6 @@ class ReadStyleDialog : DialogFragment(), FontSelectDialog.CallBack { override fun selectFile(path: String) { requireContext().putPrefString(PreferKey.readBookFont, path) - postEvent(Bus.UP_CONFIG, true) + 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 new file mode 100644 index 000000000..ba9b23a44 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/config/TocRegexDialog.kt @@ -0,0 +1,260 @@ +package io.legado.app.ui.book.read.config + +import android.annotation.SuppressLint +import android.content.Context +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.MenuItem +import android.view.View +import android.view.ViewGroup +import androidx.appcompat.widget.Toolbar +import androidx.fragment.app.FragmentManager +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.ItemTouchHelper +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.BaseDialogFragment +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.constant.Theme +import io.legado.app.data.entities.TxtTocRule +import io.legado.app.help.ItemTouchCallback +import io.legado.app.lib.dialogs.alert +import io.legado.app.lib.dialogs.cancelButton +import io.legado.app.lib.dialogs.customView +import io.legado.app.lib.dialogs.okButton +import io.legado.app.model.localBook.AnalyzeTxtFile +import io.legado.app.utils.applyTint +import io.legado.app.utils.getVerticalDivider +import kotlinx.android.synthetic.main.dialog_toc_regex.* +import kotlinx.android.synthetic.main.dialog_toc_regex_edit.* +import kotlinx.android.synthetic.main.dialog_toc_regex_edit.view.* +import kotlinx.android.synthetic.main.item_toc_regex.view.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.launch +import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* + + +class TocRegexDialog : BaseDialogFragment(), Toolbar.OnMenuItemClickListener { + + private lateinit var adapter: TocRegexAdapter + private var tocRegexLiveData: LiveData>? = null + var selectedName: String? = null + private var durRegex: String? = null + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.windowManager?.defaultDisplay?.getMetrics(dm) + dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.8).toInt()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_toc_regex, container) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + durRegex = arguments?.getString("tocRegex") + tool_bar.setTitle(R.string.txt_toc_regex) + tool_bar.inflateMenu(R.menu.txt_toc_regex) + tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) + tool_bar.setOnMenuItemClickListener(this) + initView() + initData() + } + + private fun initView() { + adapter = TocRegexAdapter(requireContext()) + recycler_view.layoutManager = LinearLayoutManager(requireContext()) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) + recycler_view.adapter = adapter + val itemTouchCallback = ItemTouchCallback() + itemTouchCallback.onItemTouchCallbackListener = adapter + itemTouchCallback.isCanDrag = true + ItemTouchHelper(itemTouchCallback).attachToRecyclerView(recycler_view) + tv_cancel.onClick { + dismiss() + } + tv_ok.onClick { + adapter.getItems().forEach { tocRule -> + if (selectedName == tocRule.name) { + val callBack = activity as? CallBack + callBack?.onTocRegexDialogResult(tocRule.rule) + dismiss() + return@onClick + } + } + } + } + + private fun initData() { + tocRegexLiveData?.removeObservers(viewLifecycleOwner) + tocRegexLiveData = App.db.txtTocRule().observeAll() + tocRegexLiveData?.observe(viewLifecycleOwner, Observer { tocRules -> + initSelectedName(tocRules) + adapter.setItems(tocRules) + }) + } + + private fun initSelectedName(tocRules: List) { + if (selectedName == null && durRegex != null) { + tocRules.forEach { + if (durRegex == it.rule) { + selectedName = it.name + return@forEach + } + } + if (selectedName == null) { + selectedName = "" + } + } + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_add -> editRule() + R.id.menu_default -> importDefault() + } + return false + } + + private fun importDefault() { + launch(IO) { + AnalyzeTxtFile.getDefaultRules().let { + App.db.txtTocRule().insert(*it.toTypedArray()) + } + } + } + + @SuppressLint("InflateParams") + private fun editRule(rule: TxtTocRule? = null) { + val tocRule = rule?.copy() ?: TxtTocRule() + requireContext().alert(titleResource = R.string.txt_toc_regex) { + var rootView: View? = null + customView { + LayoutInflater.from(requireContext()) + .inflate(R.layout.dialog_toc_regex_edit, null).apply { + rootView = this + tv_rule_name.setText(tocRule.name) + tv_rule_regex.setText(tocRule.rule) + } + } + okButton { + rootView?.let { + tocRule.name = tv_rule_name.text.toString() + tocRule.rule = tv_rule_regex.text.toString() + saveRule(tocRule, rule) + } + } + cancelButton() + }.show().applyTint() + } + + private fun saveRule(rule: TxtTocRule, oldRule: TxtTocRule? = null) { + launch(IO) { + if (rule.serialNumber < 0) { + rule.serialNumber = adapter.getItems().lastOrNull()?.serialNumber ?: 0 + 1 + } + oldRule?.let { + App.db.txtTocRule().delete(oldRule) + } + App.db.txtTocRule().insert(rule) + } + } + + inner class TocRegexAdapter(context: Context) : + SimpleRecyclerAdapter(context, R.layout.item_toc_regex), + ItemTouchCallback.OnItemTouchCallbackListener { + + override fun convert(holder: ItemViewHolder, item: TxtTocRule, payloads: MutableList) { + holder.itemView.apply { + if (payloads.isEmpty()) { + rb_regex_name.text = item.name + rb_regex_name.isChecked = item.name == selectedName + swt_enabled.isChecked = item.enable + } else { + rb_regex_name.isChecked = item.name == selectedName + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + rb_regex_name.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed && isChecked) { + selectedName = getItem(holder.layoutPosition)?.name + updateItems(0, itemCount - 1, true) + } + } + swt_enabled.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + getItem(holder.layoutPosition)?.let { + it.enable = isChecked + launch(IO) { + App.db.txtTocRule().update(it) + } + } + } + } + iv_edit.onClick { + editRule(getItem(holder.layoutPosition)) + } + iv_delete.onClick { + getItem(holder.layoutPosition)?.let { item -> + launch(IO) { + App.db.txtTocRule().delete(item) + } + } + } + } + } + + private var isMoved = false + + override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) + isMoved = true + return super.onMove(srcPosition, targetPosition) + } + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + super.clearView(recyclerView, viewHolder) + if (isMoved) { + for ((index, item) in getItems().withIndex()) { + item.serialNumber = index + 1 + } + launch(IO) { + App.db.txtTocRule().update(*getItems().toTypedArray()) + } + } + isMoved = false + } + } + + + companion object { + fun show(fragmentManager: FragmentManager, tocRegex: String? = null) { + val dialog = TocRegexDialog() + val bundle = Bundle() + bundle.putString("tocRegex", tocRegex) + dialog.arguments = bundle + dialog.show(fragmentManager, "tocRegexDialog") + } + } + + interface CallBack { + fun onTocRegexDialogResult(tocRegex: String) {} + } + +} \ No newline at end of file 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 63cc6748d..8a4a46e06 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,104 +1,344 @@ package io.legado.app.ui.book.read.page -import android.text.Spannable -import android.text.SpannableStringBuilder -import android.text.style.ForegroundColorSpan -import android.text.style.RelativeSizeSpan -import androidx.core.text.HtmlCompat -import androidx.core.text.HtmlCompat.FROM_HTML_MODE_COMPACT +import android.graphics.Typeface +import android.text.Layout +import android.text.StaticLayout +import android.text.TextPaint +import android.text.TextUtils import io.legado.app.App +import io.legado.app.constant.PreferKey import io.legado.app.data.entities.BookChapter -import io.legado.app.lib.theme.accentColor +import io.legado.app.help.BookHelp +import io.legado.app.help.ReadBookConfig +import io.legado.app.ui.book.read.page.entities.* +import io.legado.app.utils.dp +import io.legado.app.utils.getPrefString +import io.legado.app.utils.removePref +@Suppress("DEPRECATION") object ChapterProvider { - var readAloudSpan = ForegroundColorSpan(App.INSTANCE.accentColor) - private val titleSpan = RelativeSizeSpan(1.2f) + var viewWidth = 0 + var viewHeight = 0 + private var visibleWidth = 0 + private var visibleHeight = 0 + private var paddingLeft = 0 + private var paddingTop = 0 + private var lineSpacingExtra = 0f + private var paragraphSpacing = 0 + var typeface: Typeface = Typeface.SANS_SERIF + var titlePaint = TextPaint() + var contentPaint = TextPaint() + private var bodyIndent = BookHelp.bodyIndent - var textView: ContentTextView? = null + init { + upStyle(ReadBookConfig.durConfig) + } + /** + * 获取拆分完的章节数据 + */ fun getTextChapter( bookChapter: BookChapter, content: String, - chapterSize: Int, - isHtml: Boolean = false + chapterSize: Int ): TextChapter { - textView?.let { - val textPages = arrayListOf() - val pageLines = arrayListOf() - val pageLengths = arrayListOf() - var surplusText = content - var pageIndex = 0 - while (surplusText.isNotEmpty()) { - val spannableStringBuilder = - if (isHtml) { - HtmlCompat.fromHtml( - surplusText, - FROM_HTML_MODE_COMPACT - ) as SpannableStringBuilder - } else { - SpannableStringBuilder(surplusText) - } - if (pageIndex == 0) { - val end = surplusText.indexOf("\n") - if (end > 0) { - spannableStringBuilder.setSpan( - titleSpan, - 0, - end, - Spannable.SPAN_INCLUSIVE_EXCLUSIVE - ) - } + val textPages = arrayListOf() + val pageLines = arrayListOf() + val pageLengths = arrayListOf() + val stringBuilder = StringBuilder() + var surplusText = content + var durY = 0 + textPages.add(TextPage()) + while (surplusText.isNotEmpty()) { + if (textPages.first().textLines.isEmpty()) { + //title + val end = surplusText.indexOf("\n") + if (end > 0) { + val title = surplusText.substring(0, end) + surplusText = surplusText.substring(end + 1) + durY = joinTitle(title, durY, textPages, pageLines, pageLengths, stringBuilder) } - it.text = spannableStringBuilder - val lastLine = it.getLineNum() - val lastCharNum = it.getCharNum(lastLine) - if (lastCharNum == 0) { - break + } else { + //正文 + val end = surplusText.indexOf("\n") + val text: String + if (end >= 0) { + text = surplusText.substring(0, end) + surplusText = surplusText.substring(end + 1) } else { - pageLines.add(lastLine) - pageLengths.add(lastCharNum) - textPages.add( - TextPage( - index = pageIndex, - text = spannableStringBuilder.delete( - lastCharNum, - spannableStringBuilder.length - ), - title = bookChapter.title, - chapterSize = chapterSize, - chapterIndex = bookChapter.index - ) - ) - surplusText = surplusText.substring(lastCharNum) - pageIndex++ + text = surplusText + surplusText = "" } + durY = joinBody(text, durY, textPages, pageLines, pageLengths, stringBuilder) } - for (item in textPages) { - item.pageSize = textPages.size - } - return TextChapter( - bookChapter.index, - bookChapter.title, - bookChapter.url, - textPages, - pageLines, - pageLengths, - chapterSize - ) - } ?: return TextChapter( + } + textPages.last().height = durY + textPages.last().text = stringBuilder.toString() + if (pageLines.size < textPages.size) { + pageLines.add(textPages.last().textLines.size) + } + if (pageLengths.size < textPages.size) { + pageLengths.add(textPages.last().text.length) + } + for ((index, item) in textPages.withIndex()) { + item.index = index + item.pageSize = textPages.size + item.chapterIndex = bookChapter.index + item.chapterSize = chapterSize + item.title = bookChapter.title + } + return TextChapter( bookChapter.index, bookChapter.title, bookChapter.url, - arrayListOf(), - arrayListOf(), - arrayListOf(), + textPages, + pageLines, + pageLengths, chapterSize ) + } + /** + * 标题 + */ + private fun joinTitle( + title: String, + y: Int, + textPages: ArrayList, + pageLines: ArrayList, + pageLengths: ArrayList, + stringBuilder: StringBuilder + ): Int { + var durY = y + val layout = StaticLayout( + title, titlePaint, visibleWidth, + Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true + ) + for (lineIndex in 0 until layout.lineCount) { + durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) + val textLine = TextLine(isTitle = true) + if (durY < visibleHeight) { + textPages.last().textLines.add(textLine) + } else { + textPages.last().height = durY + textPages.last().text = stringBuilder.toString() + stringBuilder.clear() + pageLines.add(textPages.last().textLines.size) + pageLengths.add(textPages.last().text.length) + //新页面 + durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) + textPages.add(TextPage()) + textPages.last().textLines.add(textLine) + } + textLine.lineBottom = (paddingTop + durY - + (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat() + textLine.lineTop = (paddingTop + durY - + (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat() + val words = + title.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + stringBuilder.append(words) + textLine.text = words + val desiredWidth = layout.getLineMax(lineIndex) + if (lineIndex != layout.lineCount - 1) { + val gapCount: Int = words.length - 1 + val d = (visibleWidth - desiredWidth) / gapCount + var x = 0f + for (i in words.indices) { + val char = words[i].toString() + val cw = StaticLayout.getDesiredWidth(char, titlePaint) + val x1 = if (i != words.lastIndex) (x + cw + d) else (x + cw) + val textChar = TextChar( + charData = char, + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar) + x = x1 + } + } else { + //最后一行 + textLine.text = "$words\n" + stringBuilder.append("\n") + var x = if (ReadBookConfig.durConfig.titleCenter) + (visibleWidth - layout.getLineMax(lineIndex)) / 2 + else 0f + for (i in words.indices) { + val char = words[i].toString() + val cw = StaticLayout.getDesiredWidth(char, titlePaint) + val x1 = x + cw + val textChar = TextChar( + charData = char, + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar) + x = x1 + } + } + textLine.lineBottom = textLine.lineBottom + titlePaint.fontMetrics.descent + } + durY += paragraphSpacing + return durY } - fun upReadAloudSpan() { - readAloudSpan = ForegroundColorSpan(App.INSTANCE.accentColor) + /** + * 正文 + */ + private fun joinBody( + text: String, + y: Int, + textPages: ArrayList, + pageLines: ArrayList, + pageLengths: ArrayList, + stringBuilder: StringBuilder + ): Int { + var durY = y + val layout = StaticLayout( + text, contentPaint, visibleWidth, + Layout.Alignment.ALIGN_NORMAL, 1f, lineSpacingExtra, true + ) + for (lineIndex in 0 until layout.lineCount) { + val textLine = TextLine(isTitle = false) + durY = durY + layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) + if (durY < visibleHeight) { + textPages.last().textLines.add(textLine) + } else { + textPages.last().height = durY + textPages.last().text = stringBuilder.toString() + stringBuilder.clear() + pageLines.add(textPages.last().textLines.size) + pageLengths.add(textPages.last().text.length) + //新页面 + durY = layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex) + textPages.add(TextPage()) + textPages.last().textLines.add(textLine) + } + textLine.lineBottom = (paddingTop + durY - + (layout.getLineBottom(lineIndex) - layout.getLineBaseline(lineIndex))).toFloat() + textLine.lineTop = (paddingTop + durY - + (layout.getLineBottom(lineIndex) - layout.getLineTop(lineIndex))).toFloat() + var words = + text.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + stringBuilder.append(words) + textLine.text = words + val desiredWidth = layout.getLineMax(lineIndex) + if (lineIndex == 0 && layout.lineCount > 1) { + //第一行 + var x = 0f + val icw = StaticLayout.getDesiredWidth(bodyIndent, contentPaint) / bodyIndent.length + for (i in 0..bodyIndent.lastIndex) { + val x1 = x + icw + val textChar = TextChar( + charData = bodyIndent[i].toString(), + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar) + x = x1 + } + words = words.replaceFirst(bodyIndent, "") + val gapCount: Int = words.length - 1 + val d = (visibleWidth - desiredWidth) / gapCount + for (i in words.indices) { + val char = words[i].toString() + val cw = StaticLayout.getDesiredWidth(char, contentPaint) + val x1 = if (i != words.lastIndex) x + cw + d else x + cw + val textChar1 = TextChar( + charData = char, + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar1) + x = x1 + } + } else if (lineIndex == layout.lineCount - 1) { + //最后一行 + stringBuilder.append("\n") + textLine.text = "$words\n" + var x = 0f + for (i in words.indices) { + val char = words[i].toString() + val cw = StaticLayout.getDesiredWidth(char, contentPaint) + val x1 = x + cw + val textChar = TextChar( + charData = char, + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar) + x = x1 + } + } else { + //中间行 + val gapCount: Int = words.length - 1 + val d = (visibleWidth - desiredWidth) / gapCount + var x = 0f + for (i in words.indices) { + val char = words[i].toString() + val cw = StaticLayout.getDesiredWidth(char, contentPaint) + val x1 = if (i != words.lastIndex) x + cw + d else x + cw + val textChar = TextChar( + charData = char, + leftBottomPosition = TextPoint(paddingLeft + x, textLine.lineBottom), + rightTopPosition = TextPoint(paddingLeft + x1, textLine.lineTop) + ) + textLine.textChars.add(textChar) + x = x1 + } + } + textLine.lineBottom = textLine.lineBottom + contentPaint.fontMetrics.descent + } + durY += paragraphSpacing + return durY } + + + /** + * 更新样式 + */ + fun upStyle(config: ReadBookConfig.Config) { + typeface = try { + val fontPath = App.INSTANCE.getPrefString(PreferKey.readBookFont) + if (!TextUtils.isEmpty(fontPath)) { + Typeface.createFromFile(fontPath) + } else { + Typeface.SANS_SERIF + } + } catch (e: Exception) { + App.INSTANCE.removePref(PreferKey.readBookFont) + Typeface.SANS_SERIF + } + //标题 + titlePaint.isAntiAlias = true + titlePaint.color = config.textColor() + titlePaint.letterSpacing = config.letterSpacing + titlePaint.typeface = Typeface.create(typeface, Typeface.BOLD) + //正文 + contentPaint.isAntiAlias = true + contentPaint.color = config.textColor() + contentPaint.letterSpacing = config.letterSpacing + val bold = if (config.textBold) Typeface.BOLD else Typeface.NORMAL + contentPaint.typeface = Typeface.create(typeface, bold) + //间距 + lineSpacingExtra = config.lineSpacingExtra.dp.toFloat() + paragraphSpacing = config.paragraphSpacing.dp + titlePaint.textSize = (config.textSize + 2).dp.toFloat() + contentPaint.textSize = config.textSize.dp.toFloat() + + bodyIndent = BookHelp.bodyIndent + + upSize(config) + } + + /** + * 更新View尺寸 + */ + fun upSize(config: ReadBookConfig.Config) { + paddingLeft = config.paddingLeft.dp + paddingTop = config.paddingTop.dp + visibleWidth = viewWidth - paddingLeft - config.paddingRight.dp + visibleHeight = viewHeight - paddingTop - config.paddingBottom.dp + } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt b/app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt deleted file mode 100644 index cfce5f27b..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/ContentSelectActionCallback.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.legado.app.ui.book.read.page - -import android.view.ActionMode -import android.view.Menu -import android.view.MenuItem -import android.widget.TextView - -import io.legado.app.R -import io.legado.app.constant.Bus -import io.legado.app.utils.postEvent - -class ContentSelectActionCallback(private val textView: TextView) : ActionMode.Callback { - - override fun onActionItemClicked(mode: ActionMode?, item: MenuItem?): Boolean { - when (item?.itemId) { - R.id.menu_replace -> { - val text = textView.text.substring(textView.selectionStart, textView.selectionEnd) - postEvent(Bus.REPLACE, text) - mode?.finish() - return true - } - } - return false - } - - override fun onCreateActionMode(mode: ActionMode?, menu: Menu?): Boolean { - mode?.menuInflater?.inflate(R.menu.content_select_action, menu) - return true - } - - override fun onPrepareActionMode(mode: ActionMode?, menu: Menu?): Boolean { - return false - } - - override fun onDestroyActionMode(mode: ActionMode?) { - - } - -} \ No newline at end of file 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 190e31ac9..eeae9d33b 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 @@ -1,245 +1,300 @@ package io.legado.app.ui.book.read.page -import android.annotation.SuppressLint import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.text.Layout +import android.text.StaticLayout import android.util.AttributeSet -import android.view.MotionEvent -import android.view.VelocityTracker -import android.view.ViewConfiguration -import android.view.animation.Interpolator -import android.widget.OverScroller -import androidx.appcompat.widget.AppCompatTextView -import androidx.core.view.ViewCompat -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min - - -class ContentTextView : AppCompatTextView { - constructor(context: Context) : super(context) - - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) - - constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) - : super(context, attrs, defStyleAttr) - - private val scrollStateIdle = 0 - private val scrollStateDragging = 1 - val scrollStateSettling = 2 - - private val mViewFling: ViewFling by lazy { ViewFling() } - private var velocityTracker: VelocityTracker? = null - private var mScrollState = scrollStateIdle - private var mLastTouchY: Int = 0 - private var mTouchSlop: Int = 0 - private var mMinFlingVelocity: Int = 0 - private var mMaxFlingVelocity: Int = 0 - - //滑动距离的最大边界 - private var mOffsetHeight: Int = 0 - - //f(x) = (x-1)^5 + 1 - private val sQuinticInterpolator = Interpolator { - var t = it - t -= 1.0f - t * t * t * t * t + 1.0f +import android.view.View +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.help.ReadBookConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.read.page.entities.TextPage +import io.legado.app.utils.activity +import io.legado.app.utils.getCompatColor +import io.legado.app.utils.getPrefBoolean + + +class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) { + private val selectedPaint by lazy { + Paint().apply { + color = context.getCompatColor(R.color.btn_bg_press_2) + style = Paint.Style.FILL + } } + private var callBack: CallBack + var selectAble = context.getPrefBoolean(PreferKey.textSelectAble) + private var selectLineStart = 0 + private var selectCharStart = 0 + private var selectLineEnd = 0 + private var selectCharEnd = 0 + private var textPage: TextPage = TextPage() + //滚动参数 + private val pageFactory: TextPageFactory get() = callBack.pageFactory + private val maxScrollOffset = 100f + private var pageOffset = 0f + private var linePos = 0 + private var isLastPage = false init { - val vc = ViewConfiguration.get(context) - mTouchSlop = vc.scaledTouchSlop - mMinFlingVelocity = vc.scaledMinimumFlingVelocity - mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + callBack = activity as CallBack + contentDescription = textPage.text } - fun atTop(): Boolean { - return scrollY <= 0 + fun setContent(textPage: TextPage) { + this.textPage = textPage + contentDescription = textPage.text + invalidate() } - fun atBottom(): Boolean { - return scrollY >= mOffsetHeight + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + ReadBookConfig.durConfig.let { + ChapterProvider.viewWidth = w + ChapterProvider.viewHeight = h + ChapterProvider.upSize(ReadBookConfig.durConfig) + } } - /** - * 获取当前页总字数 - */ - fun getCharNum(lineNum: Int = getLineNum()): Int { - return layout?.getLineEnd(lineNum) ?: 0 + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + if (textPage.textLines.isEmpty()) { + drawMsg(canvas, textPage.text) + } else { + drawHorizontalPage(canvas) + } } - /** - * 获取当前页总行数 - */ - fun getLineNum(): Int { - val topOfLastLine = height - paddingTop - paddingBottom - lineHeight - return layout?.getLineForVertical(topOfLastLine) ?: 0 + @Suppress("DEPRECATION") + private fun drawMsg(canvas: Canvas, msg: String) { + val layout = StaticLayout( + msg, ChapterProvider.contentPaint, width, + Layout.Alignment.ALIGN_NORMAL, 1f, 0f, false + ) + val y = (height - layout.height) / 2f + for (lineIndex in 0 until layout.lineCount) { + val x = (width - layout.getLineMax(lineIndex)) / 2 + val words = + msg.substring(layout.getLineStart(lineIndex), layout.getLineEnd(lineIndex)) + canvas.drawText(words, x, y, ChapterProvider.contentPaint) + } } - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - initOffsetHeight() + private fun drawHorizontalPage(canvas: Canvas) { + textPage.textLines.forEach { textLine -> + val textPaint = if (textLine.isTitle) { + ChapterProvider.titlePaint + } else { + ChapterProvider.contentPaint + } + textPaint.color = if (textLine.isReadAloud) { + context.accentColor + } else { + ReadBookConfig.durConfig.textColor() + } + textLine.textChars.forEach { + canvas.drawText( + it.charData, + it.leftBottomPosition.x, + it.leftBottomPosition.y, + textPaint + ) + if (it.selected) { + canvas.drawRect( + it.leftBottomPosition.x, + textLine.lineTop, + it.rightTopPosition.x, + textLine.lineBottom, + selectedPaint + ) + } + } + } } - override fun onTextChanged( - text: CharSequence?, - start: Int, - lengthBefore: Int, - lengthAfter: Int - ) { - super.onTextChanged(text, start, lengthBefore, lengthAfter) - initOffsetHeight() - } + fun onScroll(mOffset: Float) { + var offset = mOffset + if (offset > maxScrollOffset) { + offset = maxScrollOffset + } else if (offset < -maxScrollOffset) { + offset = -maxScrollOffset + } - private fun initOffsetHeight() { - val mLayoutHeight: Int + if (!isLastPage || offset < 0) { + pageOffset += offset + isLastPage = false + } + // 首页 + if (pageOffset < 0 && !pageFactory.hasPrev()) { + pageOffset = 0f + } - //获得内容面板 - val mLayout = layout ?: return - //获得内容面板的高度 - mLayoutHeight = mLayout.height + val cHeight = if (textPage.height > 0) textPage.height else height + if (offset > 0 && pageOffset > cHeight) { - //计算滑动距离的边界 - mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight + } } - override fun scrollTo(x: Int, y: Int) { - super.scrollTo(x, min(y, mOffsetHeight)) + fun resetPageOffset() { + pageOffset = 0f + linePos = 0 + isLastPage = false } - @SuppressLint("ClickableViewAccessibility") - override fun onTouchEvent(event: MotionEvent?): Boolean { - event?.let { - if (velocityTracker == null) { - velocityTracker = VelocityTracker.obtain() + private fun switchToPageOffset(offset: Int) { + when (offset) { + 1 -> { + } - velocityTracker?.addMovement(it) - when (event.action) { - MotionEvent.ACTION_DOWN -> { - setScrollState(scrollStateIdle) - mLastTouchY = (event.y + 0.5f).toInt() - } - MotionEvent.ACTION_MOVE -> { - val y = (event.y + 0.5f).toInt() - var dy = mLastTouchY - y - if (mScrollState != scrollStateDragging) { - var startScroll = false - - if (abs(dy) > mTouchSlop) { - if (dy > 0) { - dy -= mTouchSlop - } else { - dy += mTouchSlop - } - startScroll = true - } - if (startScroll) { - setScrollState(scrollStateDragging) - } - } - if (mScrollState == scrollStateDragging) { - mLastTouchY = y - } - } - MotionEvent.ACTION_UP -> { - velocityTracker?.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat()) - val yVelocity = velocityTracker?.yVelocity ?: 0f - if (abs(yVelocity) > mMinFlingVelocity) { - mViewFling.fling(-yVelocity.toInt()) - } else { - setScrollState(scrollStateIdle) - } - resetTouch() - } - MotionEvent.ACTION_CANCEL -> { - resetTouch() - } + -1 -> { + } } - return super.onTouchEvent(event) } - private fun resetTouch() { - velocityTracker?.clear() + fun selectText(x: Float, y: Float): Boolean { + for ((lineIndex, textLine) in textPage.textLines.withIndex()) { + if (y > textLine.lineTop && y < textLine.lineBottom) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { + textChar.selected = true + invalidate() + selectLineStart = lineIndex + selectCharStart = charIndex + selectLineEnd = lineIndex + selectCharEnd = charIndex + upSelectedStart( + textChar.leftBottomPosition.x, + textChar.leftBottomPosition.y + ) + upSelectedEnd( + textChar.rightTopPosition.x, + textChar.leftBottomPosition.y + ) + return true + } + } + break + } + } + return false } - private fun setScrollState(state: Int) { - if (state == mScrollState) { - return - } - mScrollState = state - if (state != scrollStateSettling) { - mViewFling.stop() + fun selectStartMove(x: Float, y: Float) { + for ((lineIndex, textLine) in textPage.textLines.withIndex()) { + if (y > textLine.lineTop && y < textLine.lineBottom) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { + if (selectLineStart != lineIndex || selectCharStart != charIndex) { + selectLineStart = lineIndex + selectCharStart = charIndex + upSelectedStart( + textChar.leftBottomPosition.x, + textChar.leftBottomPosition.y + ) + upSelectChars(textPage) + } + break + } + } + break + } } } - /** - * 惯性滚动 - */ - private inner class ViewFling : Runnable { - - private var mLastFlingY = 0 - private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator) - private var mEatRunOnAnimationRequest = false - private var mReSchedulePostAnimationCallback = false - - override fun run() { - disableRunOnAnimationRequests() - val scroller = mScroller - if (scroller.computeScrollOffset()) { - val y = scroller.currY - val dy = y - mLastFlingY - mLastFlingY = y - if (dy < 0 && scrollY > 0) { - scrollBy(0, max(dy, -scrollY)) - } else if (dy > 0 && scrollY < mOffsetHeight) { - scrollBy(0, min(dy, mOffsetHeight - scrollY)) + fun selectEndMove(x: Float, y: Float) { + for ((lineIndex, textLine) in textPage.textLines.withIndex()) { + if (y > textLine.lineTop && y < textLine.lineBottom) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + if (x > textChar.leftBottomPosition.x && x < textChar.rightTopPosition.x) { + if (selectLineEnd != lineIndex || selectCharEnd != charIndex) { + selectLineEnd = lineIndex + selectCharEnd = charIndex + upSelectedEnd( + textChar.rightTopPosition.x, + textChar.leftBottomPosition.y + ) + upSelectChars(textPage) + } + break + } } - postOnAnimation() + break } - enableRunOnAnimationRequests() } + } - fun fling(velocityY: Int) { - mLastFlingY = 0 - setScrollState(scrollStateSettling) - mScroller.fling( - 0, - 0, - 0, - velocityY, - Integer.MIN_VALUE, - Integer.MAX_VALUE, - Integer.MIN_VALUE, - Integer.MAX_VALUE - ) - postOnAnimation() + private fun upSelectChars(textPage: TextPage) { + for ((lineIndex, textLine) in textPage.textLines.withIndex()) { + for ((charIndex, textChar) in textLine.textChars.withIndex()) { + textChar.selected = + if (lineIndex == selectLineStart && lineIndex == selectLineEnd) { + charIndex in selectCharStart..selectCharEnd + } else if (lineIndex == selectLineStart) { + charIndex >= selectCharStart + } else if (lineIndex == selectLineEnd) { + charIndex <= selectCharEnd + } else { + lineIndex in (selectLineStart + 1) until selectLineEnd + } + } } + invalidate() + } - fun stop() { - removeCallbacks(this) - mScroller.abortAnimation() - } + private fun upSelectedStart(x: Float, y: Float) { + callBack.upSelectedStart(x, y + callBack.headerHeight) + } - private fun disableRunOnAnimationRequests() { - mReSchedulePostAnimationCallback = false - mEatRunOnAnimationRequest = true - } + private fun upSelectedEnd(x: Float, y: Float) { + callBack.upSelectedEnd(x, y + callBack.headerHeight) + } - private fun enableRunOnAnimationRequests() { - mEatRunOnAnimationRequest = false - if (mReSchedulePostAnimationCallback) { - postOnAnimation() + fun cancelSelect() { + textPage.textLines.forEach { textLine -> + textLine.textChars.forEach { + it.selected = false } } + invalidate() + callBack.onCancelSelect() + } - internal fun postOnAnimation() { - if (mEatRunOnAnimationRequest) { - mReSchedulePostAnimationCallback = true - } else { - removeCallbacks(this) - ViewCompat.postOnAnimation(this@ContentTextView, this) + val selectedText: String + get() { + val stringBuilder = StringBuilder() + for (lineIndex in selectLineStart..selectLineEnd) { + if (lineIndex == selectLineStart && lineIndex == selectLineEnd) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring( + selectCharStart, + selectCharEnd + 1 + ) + ) + } else if (lineIndex == selectLineStart) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring( + selectCharStart + ) + ) + } else if (lineIndex == selectLineEnd) { + stringBuilder.append( + textPage.textLines[lineIndex].text.substring(0, selectCharEnd + 1) + ) + } else { + stringBuilder.append(textPage.textLines[lineIndex].text) + } } + return stringBuilder.toString() } - } + interface CallBack { + fun upSelectedStart(x: Float, y: Float) + fun upSelectedEnd(x: Float, y: Float) + fun onCancelSelect() + val headerHeight: Int + val pageFactory: TextPageFactory + } } 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 cc9e9e666..abb2b82c4 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 @@ -2,102 +2,85 @@ package io.legado.app.ui.book.read.page import android.annotation.SuppressLint import android.content.Context -import android.graphics.Typeface import android.graphics.drawable.Drawable -import android.util.AttributeSet -import android.view.Gravity +import android.view.MotionEvent import android.widget.FrameLayout -import android.widget.ImageView -import androidx.appcompat.widget.AppCompatImageView import io.legado.app.R import io.legado.app.constant.AppConst.TIME_FORMAT import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig +import io.legado.app.ui.book.read.page.entities.TextPage import io.legado.app.utils.* import kotlinx.android.synthetic.main.view_book_page.view.* -import org.jetbrains.anko.matchParent -import org.jetbrains.anko.sdk27.listeners.onScrollChange -import java.io.File import java.util.* -class ContentView : FrameLayout { - var callBack: CallBack? = null - private var isScroll: Boolean = false +class ContentView(context: Context) : FrameLayout(context) { private var pageSize: Int = 0 - private val bgImage: AppCompatImageView = AppCompatImageView(context) - .apply { - scaleType = ImageView.ScaleType.CENTER_CROP - } - - constructor(context: Context) : super(context) { - this.isScroll = true - init() - } - constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) { - init() - } - - fun init() { + init { //设置背景防止切换背景时文字重叠 setBackgroundColor(context.getCompatColor(R.color.background)) - addView(bgImage, LayoutParams(matchParent, matchParent)) inflate(context, R.layout.view_book_page, this) - top_bar.layoutParams.height = context.getStatusBarHeight() + upStyle() upTime() - content_text_view.customSelectionActionModeCallback = - ContentSelectActionCallback(content_text_view) - content_text_view.onScrollChange { _, _, scrollY, _, _ -> - content_text_view.layout?.getLineForVertical(scrollY)?.let { line -> - callBack?.scrollToLine(line) - } - if (content_text_view.atBottom()) { - callBack?.scrollToLast() - } - } } fun upStyle() { - ReadBookConfig.getConfig().apply { - val pt = if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { - top_bar.visible() - 0 + ReadBookConfig.durConfig.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 + //显示状态栏时隐藏header + if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { + ll_header.layoutParams = + ll_header.layoutParams.apply { height = context.getStatusBarHeight() } + ll_header.setPadding( + headerPaddingLeft.dp, + headerPaddingTop.dp, + headerPaddingRight.dp, + headerPaddingBottom.dp + ) + ll_header.visible() + page_panel.setPadding(0, 0, 0, 0) } else { - top_bar.gone() - context.getStatusBarHeight() + ll_header.gone() + page_panel.setPadding(0, context.getStatusBarHeight(), 0, 0) } - page_panel.setPadding(paddingLeft.dp, pt, paddingRight.dp, 0) - content_text_view.setPadding(0, paddingTop.dp, 0, paddingBottom.dp) - content_text_view.textSize = textSize.toFloat() - content_text_view.setLineSpacing(lineSpacingExtra.toFloat(), lineSpacingMultiplier) - content_text_view.letterSpacing = letterSpacing - content_text_view.paint.isFakeBoldText = textBold + content_text_view.setPadding( + paddingLeft.dp, + paddingTop.dp, + paddingRight.dp, + paddingBottom.dp + ) + ll_footer.setPadding( + footerPaddingLeft.dp, + footerPaddingTop.dp, + footerPaddingRight.dp, + footerPaddingBottom.dp + ) textColor().let { - content_text_view.setTextColor(it) tv_top_left.setTextColor(it) tv_top_right.setTextColor(it) tv_bottom_left.setTextColor(it) tv_bottom_right.setTextColor(it) } } - context.getPrefString(PreferKey.readBookFont)?.let { - if (it.isNotEmpty()) { - val file = File(it) - if (file.exists()) { - content_text_view.typeface = Typeface.createFromFile(it) - return@let - } else { - context.putPrefString(PreferKey.readBookFont, "") - } + } + + val headerHeight: Int + get() { + return if (context.getPrefBoolean(PreferKey.hideStatusBar, false)) { + ll_header.height + } else { + context.getStatusBarHeight() } - content_text_view.typeface = Typeface.DEFAULT } - } fun setBg(bg: Drawable?) { - bgImage.background = bg + page_panel.background = bg } fun upTime() { @@ -110,14 +93,10 @@ class ContentView : FrameLayout { fun setContent(textPage: TextPage?) { if (textPage != null) { - content_text_view.gravity = Gravity.START - content_text_view.text = textPage.text + content_text_view.setContent(textPage) tv_bottom_left.text = textPage.title pageSize = textPage.pageSize setPageIndex(textPage.index) - } else { - content_text_view.gravity = Gravity.CENTER - content_text_view.setText(R.string.data_loading) } } @@ -128,35 +107,31 @@ class ContentView : FrameLayout { } } - fun isTextSelected(): Boolean { - return content_text_view.selectionEnd - content_text_view.selectionStart != 0 + fun onScroll(offset: Float) { + content_text_view.onScroll(offset) } - fun contentTextView(): ContentTextView? { - return content_text_view + fun upSelectAble(selectAble: Boolean) { + content_text_view.selectAble = selectAble } - fun scrollTo(pos: Int?) { - if (pos != null) { - content_text_view.post { - if (content_text_view.layout.lineCount >= pos) { - content_text_view.scrollTo(0, content_text_view.layout.getLineTop(pos)) - } - } - } + fun selectText(e: MotionEvent): Boolean { + val y = e.y - headerHeight + return content_text_view.selectText(e.x, y) } - fun scrollToBottom() { - content_text_view.post { - content_text_view.scrollTo( - 0, - content_text_view.layout.getLineTop(content_text_view.lineCount) - ) - } + fun selectStartMove(x: Float, y: Float) { + content_text_view.selectStartMove(x, y - headerHeight) + } + + fun selectEndMove(x: Float, y: Float) { + content_text_view.selectEndMove(x, y - headerHeight) } - interface CallBack { - fun scrollToLine(line: Int) - fun scrollToLast() + fun cancelSelect() { + content_text_view.cancelSelect() } + + val selectedText: String get() = content_text_view.selectedText + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt b/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt index 7c93e52c2..ed223a4cf 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/DataSource.kt @@ -1,5 +1,7 @@ package io.legado.app.ui.book.read.page +import io.legado.app.ui.book.read.page.entities.TextChapter + interface DataSource { val isScrollDelegate: Boolean 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 84dafcaa6..a0c6c962a 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 @@ -10,24 +10,24 @@ import io.legado.app.constant.PreferKey import io.legado.app.help.ReadBookConfig import io.legado.app.service.help.ReadBook import io.legado.app.ui.book.read.page.delegate.* +import io.legado.app.ui.book.read.page.entities.TextChapter import io.legado.app.utils.activity import io.legado.app.utils.getPrefInt class PageView(context: Context, attrs: AttributeSet) : FrameLayout(context, attrs), - ContentView.CallBack, DataSource { - var callBack: CallBack? = null - var pageFactory: TextPageFactory? = null - private var pageDelegate: PageDelegate? = null + var callBack: CallBack + var pageFactory: TextPageFactory + var pageDelegate: PageDelegate? = null - var prevPage: ContentView? = null - var curPage: ContentView? = null - var nextPage: ContentView? = null + var prevPage: ContentView + var curPage: ContentView + var nextPage: ContentView init { - callBack = activity as? CallBack + callBack = activity as CallBack prevPage = ContentView(context) addView(prevPage) nextPage = ContentView(context) @@ -38,7 +38,6 @@ class PageView(context: Context, attrs: AttributeSet) : setWillNotDraw(false) pageFactory = TextPageFactory(this) upPageAnim(context.getPrefInt(PreferKey.pageAnim)) - curPage?.callBack = this } override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { @@ -65,30 +64,31 @@ class PageView(context: Context, attrs: AttributeSet) : @SuppressLint("ClickableViewAccessibility") override fun onTouchEvent(event: MotionEvent): Boolean { + callBack.screenOffTimerStart() return pageDelegate?.onTouch(event) ?: super.onTouchEvent(event) } + fun onDestroy() { + pageDelegate?.onDestroy() + curPage.cancelSelect() + } + fun fillPage(direction: PageDelegate.Direction) { when (direction) { PageDelegate.Direction.PREV -> { - pageFactory?.moveToPrevious() + pageFactory.moveToPrevious() upContent() - if (isScrollDelegate) { - curPage?.scrollToBottom() - } } PageDelegate.Direction.NEXT -> { - pageFactory?.moveToNext() + pageFactory.moveToNext() upContent() - if (isScrollDelegate) { - curPage?.scrollTo(0) - } } else -> Unit } } fun upPageAnim(pageAnim: Int) { + pageDelegate?.onDestroy() pageDelegate = null pageDelegate = when (pageAnim) { 0 -> CoverPageDelegate(this) @@ -101,75 +101,66 @@ class PageView(context: Context, attrs: AttributeSet) : } fun upContent(position: Int = 0) { - pageFactory?.let { + pageFactory.let { when (position) { - -1 -> prevPage?.setContent(it.previousPage()) - 1 -> nextPage?.setContent(it.nextPage()) + -1 -> prevPage.setContent(it.previousPage()) + 1 -> nextPage.setContent(it.nextPage()) else -> { - curPage?.setContent(it.currentPage()) - nextPage?.setContent(it.nextPage()) - prevPage?.setContent(it.previousPage()) - if (isScrollDelegate) { - curPage?.scrollTo(ReadBook.textChapter()?.getStartLine(ReadBook.durChapterPos())) - } + curPage.setContent(it.currentPage()) + nextPage.setContent(it.nextPage()) + prevPage.setContent(it.previousPage()) } } - if (isScrollDelegate) { - prevPage?.scrollToBottom() - } } - pageDelegate?.onPageUp() + callBack.screenOffTimerStart() } fun moveToPrevPage(noAnim: Boolean = true) { if (noAnim) { - if (isScrollDelegate) { - ReadBook.textChapter()?.let { - curPage?.scrollTo(it.getStartLine(pageIndex - 1)) - } - } else { - fillPage(PageDelegate.Direction.PREV) - } + fillPage(PageDelegate.Direction.PREV) + } else { + pageDelegate?.start(PageDelegate.Direction.PREV) } } fun moveToNextPage(noAnim: Boolean = true) { if (noAnim) { - if (isScrollDelegate) { - ReadBook.textChapter()?.let { - curPage?.scrollTo(it.getStartLine(pageIndex + 1)) - } - } else { - fillPage(PageDelegate.Direction.NEXT) - } + fillPage(PageDelegate.Direction.NEXT) + } else { + pageDelegate?.start(PageDelegate.Direction.NEXT) } } + fun upSelectAble(selectAble: Boolean) { + pageDelegate?.upSelectAble(selectAble) + curPage.upSelectAble(selectAble) + } + fun upStyle() { - curPage?.upStyle() - prevPage?.upStyle() - nextPage?.upStyle() + curPage.upStyle() + prevPage.upStyle() + nextPage.upStyle() } fun upBg() { ReadBookConfig.bg ?: let { ReadBookConfig.upBg() } - curPage?.setBg(ReadBookConfig.bg) - prevPage?.setBg(ReadBookConfig.bg) - nextPage?.setBg(ReadBookConfig.bg) + curPage.setBg(ReadBookConfig.bg) + prevPage.setBg(ReadBookConfig.bg) + nextPage.setBg(ReadBookConfig.bg) } fun upTime() { - curPage?.upTime() - prevPage?.upTime() - nextPage?.upTime() + curPage.upTime() + prevPage.upTime() + nextPage.upTime() } fun upBattery(battery: Int) { - curPage?.upBattery(battery) - prevPage?.upBattery(battery) - nextPage?.upBattery(battery) + curPage.upBattery(battery) + prevPage.upBattery(battery) + nextPage.upBattery(battery) } override val isScrollDelegate: Boolean @@ -179,7 +170,7 @@ class PageView(context: Context, attrs: AttributeSet) : get() = ReadBook.durChapterPos() override fun setPageIndex(pageIndex: Int) { - callBack?.setPageIndex(pageIndex) + callBack.setPageIndex(pageIndex) } override fun getChapterPosition(): Int { @@ -187,15 +178,15 @@ class PageView(context: Context, attrs: AttributeSet) : } override fun getCurrentChapter(): TextChapter? { - return if (callBack?.isInitFinish == true) ReadBook.textChapter(0) else null + return if (callBack.isInitFinish) ReadBook.textChapter(0) else null } override fun getNextChapter(): TextChapter? { - return if (callBack?.isInitFinish == true) ReadBook.textChapter(1) else null + return if (callBack.isInitFinish) ReadBook.textChapter(1) else null } override fun getPreviousChapter(): TextChapter? { - return if (callBack?.isInitFinish == true) ReadBook.textChapter(-1) else null + return if (callBack.isInitFinish) ReadBook.textChapter(-1) else null } override fun hasNextChapter(): Boolean { @@ -203,33 +194,13 @@ class PageView(context: Context, attrs: AttributeSet) : } override fun hasPrevChapter(): Boolean { - callBack?.let { - return ReadBook.durChapterIndex > 0 - } - return false - } - - override fun scrollToLine(line: Int) { - if (isScrollDelegate) { - ReadBook.textChapter()?.let { - val pageIndex = it.getPageIndex(line) - curPage?.setPageIndex(pageIndex) - callBack?.setPageIndex(pageIndex) - } - } - } - - override fun scrollToLast() { - if (isScrollDelegate) { - ReadBook.textChapter()?.let { - callBack?.setPageIndex(it.lastIndex()) - curPage?.setPageIndex(it.lastIndex()) - } - } + return ReadBook.durChapterIndex > 0 } interface CallBack { + val isInitFinish: Boolean + /** * 保存页数 */ @@ -240,6 +211,6 @@ class PageView(context: Context, attrs: AttributeSet) : */ fun clickCenter() - val isInitFinish: Boolean + fun screenOffTimerStart() } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/TextPage.kt b/app/src/main/java/io/legado/app/ui/book/read/page/TextPage.kt deleted file mode 100644 index 4950bf68a..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/TextPage.kt +++ /dev/null @@ -1,39 +0,0 @@ -package io.legado.app.ui.book.read.page - -import android.text.Spannable -import android.text.SpannableStringBuilder -import io.legado.app.App -import io.legado.app.R - -data class TextPage( - val index: Int, - val text: CharSequence = App.INSTANCE.getString(R.string.data_loading), - val title: String, - var pageSize: Int = 0, - var chapterSize: Int = 0, - var chapterIndex: Int = 0 -) { - - fun removePageAloudSpan(): TextPage { - if (text is SpannableStringBuilder) { - text.removeSpan(ChapterProvider.readAloudSpan) - } - return this - } - - fun upPageAloudSpan(pageStart: Int) { - if (text is SpannableStringBuilder) { - text.removeSpan(ChapterProvider.readAloudSpan) - var end = text.indexOf("\n", pageStart) - if (end == -1) end = text.length - var start = text.lastIndexOf("\n", pageStart) - if (start == -1) start = 0 - text.setSpan( - ChapterProvider.readAloudSpan, - start, - end, - Spannable.SPAN_INCLUSIVE_EXCLUSIVE - ) - } - } -} 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 8423d04ab..018f75bab 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 @@ -1,24 +1,16 @@ package io.legado.app.ui.book.read.page import io.legado.app.service.help.ReadBook +import io.legado.app.ui.book.read.page.entities.TextPage class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource) { override fun hasPrev(): Boolean = with(dataSource) { - return if (isScrollDelegate) { - hasPrevChapter() - } else { - hasPrevChapter() || pageIndex > 0 - } + return hasPrevChapter() || pageIndex > 0 } override fun hasNext(): Boolean = with(dataSource) { - return if (isScrollDelegate) { - hasNextChapter() - } else { - hasNextChapter() - || getCurrentChapter()?.isLastIndex(pageIndex) != true - } + return hasNextChapter() || getCurrentChapter()?.isLastIndex(pageIndex) != true } override fun moveToFirst() { @@ -38,7 +30,6 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource override fun moveToNext(): Boolean = with(dataSource) { return if (hasNext()) { if (getCurrentChapter()?.isLastIndex(pageIndex) == true - || isScrollDelegate ) { ReadBook.moveToNextChapter(false) } else { @@ -51,7 +42,7 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource override fun moveToPrevious(): Boolean = with(dataSource) { return if (hasPrev()) { - if (pageIndex <= 0 || isScrollDelegate) { + if (pageIndex <= 0) { ReadBook.moveToPrevChapter(false) } else { setPageIndex(pageIndex.minus(1)) @@ -62,17 +53,10 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource } override fun currentPage(): TextPage? = with(dataSource) { - return if (isScrollDelegate) { - getCurrentChapter()?.scrollPage() - } else { - getCurrentChapter()?.page(pageIndex) - } + return getCurrentChapter()?.page(pageIndex) } override fun nextPage(): TextPage? = with(dataSource) { - if (isScrollDelegate) { - return getNextChapter()?.scrollPage() - } getCurrentChapter()?.let { if (pageIndex < it.pageSize() - 1) { return getCurrentChapter()?.page(pageIndex + 1)?.removePageAloudSpan() @@ -82,9 +66,6 @@ class TextPageFactory(dataSource: DataSource) : PageFactory(dataSource } override fun previousPage(): TextPage? = with(dataSource) { - if (isScrollDelegate) { - return getPreviousChapter()?.scrollPage() - } if (pageIndex > 0) { return getCurrentChapter()?.page(pageIndex - 1)?.removePageAloudSpan() } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt index c30affba3..21ffcee77 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/CoverPageDelegate.kt @@ -18,9 +18,9 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { shadowDrawableR.gradientType = GradientDrawable.LINEAR_GRADIENT } - override fun onScrollStart() { + override fun onAnimStart() { val distanceX: Float - when (direction) { + when (mDirection) { Direction.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX @@ -42,27 +42,27 @@ class CoverPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { startScroll(touchX.toInt(), 0, distanceX.toInt(), 0) } - override fun onScrollStop() { - curPage?.x = 0.toFloat() + override fun onAnimStop() { + curPage.x = 0.toFloat() if (!isCancel) { - pageView.fillPage(direction) + pageView.fillPage(mDirection) } } override fun onDraw(canvas: Canvas) { val offsetX = touchX - startX - if ((direction == Direction.NEXT && offsetX > 0) - || (direction == Direction.PREV && offsetX < 0) + if ((mDirection == Direction.NEXT && offsetX > 0) + || (mDirection == Direction.PREV && offsetX < 0) ) return val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth bitmap?.let { - if (direction == Direction.PREV) { + if (mDirection == Direction.PREV) { bitmapMatrix.setTranslate(distanceX, 0.toFloat()) canvas.drawBitmap(it, bitmapMatrix, null) - } else if (direction == Direction.NEXT) { - curPage?.translationX = offsetX + } else if (mDirection == Direction.NEXT) { + curPage.translationX = offsetX } addShadow(distanceX.toInt(), canvas) } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt deleted file mode 100644 index 05d6b28a3..000000000 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/EventExtensions.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.legado.app.ui.book.read.page.delegate - -import android.view.MotionEvent - -fun MotionEvent.toAction(action: Int): MotionEvent { - return MotionEvent.obtain( - downTime, - eventTime, - action, - x, - y, - pressure, - size, - metaState, - xPrecision, - yPrecision, - deviceId, - edgeFlags - ) -} \ No newline at end of file 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 ebb76100c..f129d41ff 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 @@ -2,7 +2,6 @@ package io.legado.app.ui.book.read.page.delegate import android.view.MotionEvent import io.legado.app.ui.book.read.page.PageView -import io.legado.app.utils.screenshot import kotlin.math.abs abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageView) { @@ -14,9 +13,6 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie distanceY: Float ): Boolean { if (!isMoved) { - val event = e1.toAction(MotionEvent.ACTION_UP) - curPage?.dispatchTouchEvent(event) - event.recycle() if (abs(distanceX) > abs(distanceY)) { if (distanceX < 0) { //如果上一页不存在 @@ -24,26 +20,27 @@ abstract class HorizontalPageDelegate(pageView: PageView) : PageDelegate(pageVie noNext = true return true } - //上一页截图 - bitmap = prevPage?.screenshot() + setDirection(Direction.PREV) + setBitmap() } else { //如果不存在表示没有下一页了 if (!hasNext()) { noNext = true return true } - //下一页截图 - bitmap = nextPage?.screenshot() + setDirection(Direction.NEXT) + setBitmap() } isMoved = true } } if (isMoved) { - isCancel = if (direction == Direction.NEXT) distanceX < 0 else distanceX > 0 + isCancel = if (mDirection == Direction.NEXT) distanceX < 0 else distanceX > 0 isRunning = true //设置触摸点 setTouchPoint(e2.x, e2.y) } return isMoved } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt index 8837698d0..52dc2aeeb 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/NoAnimPageDelegate.kt @@ -4,16 +4,16 @@ import android.graphics.Canvas import io.legado.app.ui.book.read.page.PageView class NoAnimPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { - override fun onScrollStart() { + override fun onAnimStart() { startScroll(touchX.toInt(), 0, 0, 0) } override fun onDraw(canvas: Canvas) { } - override fun onScrollStop() { + override fun onAnimStop() { if (!isCancel) { - pageView.fillPage(direction) + pageView.fillPage(mDirection) } } } \ No newline at end of file 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 db1250c3d..2a90c79f1 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 @@ -1,5 +1,6 @@ package io.legado.app.ui.book.read.page.delegate +import android.content.Context import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.RectF @@ -10,32 +11,36 @@ import androidx.annotation.CallSuper import androidx.interpolator.view.animation.FastOutLinearInInterpolator import com.google.android.material.snackbar.Snackbar import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig import io.legado.app.ui.book.read.page.ContentView import io.legado.app.ui.book.read.page.PageView import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.screenshot -import io.legado.app.utils.snackbar import kotlin.math.abs -abstract class PageDelegate(protected val pageView: PageView) { - val centerRectF = RectF( +abstract class PageDelegate(protected val pageView: PageView) : + GestureDetector.SimpleOnGestureListener() { + private val centerRectF = RectF( pageView.width * 0.33f, pageView.height * 0.33f, pageView.width * 0.66f, pageView.height * 0.66f ) + protected val context: Context = pageView.context //起始点 - protected var startX: Float = 0.toFloat() - protected var startY: Float = 0.toFloat() + protected var startX: Float = 0f + protected var startY: Float = 0f + //上一个触碰点 + protected var lastY: Float = 0f //触碰点 - protected var touchX: Float = 0.toFloat() - protected var touchY: Float = 0.toFloat() + protected var touchX: Float = 0f + protected var touchY: Float = 0f - protected val nextPage: ContentView? + protected val nextPage: ContentView get() = pageView.nextPage - protected val curPage: ContentView? + protected val curPage: ContentView get() = pageView.curPage - protected val prevPage: ContentView? + protected val prevPage: ContentView get() = pageView.prevPage protected var bitmap: Bitmap? = null @@ -46,34 +51,36 @@ abstract class PageDelegate(protected val pageView: PageView) { protected var atTop: Boolean = false protected var atBottom: Boolean = false - private var snackbar: Snackbar? = null + private val snackBar: Snackbar by lazy { + Snackbar.make(pageView, "", Snackbar.LENGTH_SHORT) + } private val scroller: Scroller by lazy { - Scroller( - pageView.context, - FastOutLinearInInterpolator() - ) + Scroller(pageView.context, FastOutLinearInInterpolator()) } private val detector: GestureDetector by lazy { - GestureDetector( - pageView.context, - GestureListener() - ) + GestureDetector(pageView.context, this).apply { + setIsLongpressEnabled(context.getPrefBoolean(PreferKey.textSelectAble)) + } } var isMoved = false var noNext = true //移动方向 - var direction = Direction.NONE + var mDirection = Direction.NONE var isCancel = false var isRunning = false var isStarted = false + var isTextSelected = false open fun setStartPoint(x: Float, y: Float, invalidate: Boolean = true) { startX = x startY = y + lastY = y + touchX = x + touchY = y if (invalidate) { invalidate() @@ -81,6 +88,7 @@ abstract class PageDelegate(protected val pageView: PageView) { } open fun setTouchPoint(x: Float, y: Float, invalidate: Boolean = true) { + lastY = touchY touchX = x touchY = y @@ -91,10 +99,24 @@ abstract class PageDelegate(protected val pageView: PageView) { onScroll() } + fun upSelectAble(selectAble: Boolean) { + detector.setIsLongpressEnabled(selectAble) + } + protected fun invalidate() { pageView.invalidate() } + open fun fling( + startX: Int, startY: Int, velocityX: Int, velocityY: Int, + minX: Int, maxX: Int, minY: Int, maxY: Int + ) { + scroller.fling(startX, startY, velocityX, velocityY, minX, maxX, minY, maxY) + isRunning = true + isStarted = true + invalidate() + } + protected fun startScroll(startX: Int, startY: Int, dx: Int, dy: Int) { scroller.startScroll( startX, @@ -108,19 +130,12 @@ abstract class PageDelegate(protected val pageView: PageView) { invalidate() } - protected fun stopScroll() { + private fun stopScroll() { isRunning = false isStarted = false invalidate() - if (pageView.isScrollDelegate) { - pageView.postDelayed({ - bitmap?.recycle() - bitmap = null - }, 100) - } else { - bitmap?.recycle() - bitmap = null - } + bitmap?.recycle() + bitmap = null } fun setViewSize(width: Int, height: Int) { @@ -138,7 +153,7 @@ abstract class PageDelegate(protected val pageView: PageView) { setTouchPoint(scroller.currX.toFloat(), scroller.currY.toFloat()) } else if (isStarted) { setTouchPoint(scroller.finalX.toFloat(), scroller.finalY.toFloat(), false) - onScrollStop() + onAnimStop() stopScroll() } } @@ -174,7 +189,29 @@ abstract class PageDelegate(protected val pageView: PageView) { return } } - onScrollStart() + onAnimStart() + } + + abstract fun onAnimStart()//scroller start + + abstract fun onDraw(canvas: Canvas)//绘制 + + abstract fun onAnimStop()//scroller finish + + open fun onScroll() {//移动contentView, slidePage + } + + @CallSuper + open fun setDirection(direction: Direction) { + mDirection = direction + } + + open fun setBitmap() { + bitmap = when (mDirection) { + Direction.NEXT -> nextPage.screenshot() + Direction.PREV -> prevPage.screenshot() + else -> null + } } /** @@ -183,145 +220,117 @@ abstract class PageDelegate(protected val pageView: PageView) { @CallSuper open fun onTouch(event: MotionEvent): Boolean { if (isStarted) return false - if (curPage?.isTextSelected() == true) { - curPage?.dispatchTouchEvent(event) - return true - } - if (event.action == MotionEvent.ACTION_DOWN) { - curPage?.let { - it.contentTextView()?.let { contentTextView -> - atTop = contentTextView.atTop() - atBottom = contentTextView.atBottom() + if (!detector.onTouchEvent(event)) { + //GestureDetector.onFling小幅移动不会触发,所以要自己判断 + if (event.action == MotionEvent.ACTION_UP && isMoved) { + if (isTextSelected) { + isTextSelected = false } - it.dispatchTouchEvent(event) - } - } else if (event.action == MotionEvent.ACTION_UP) { - curPage?.dispatchTouchEvent(event) - if (isMoved) { - // 开启翻页效果 - if (!noNext) onScrollStart() - return true + if (!noNext) onAnimStart() } } - return detector.onTouchEvent(event) + return true } - abstract fun onScrollStart()//scroller start - - abstract fun onDraw(canvas: Canvas)//绘制 - - abstract fun onScrollStop()//scroller finish - - open fun onScroll() {//移动contentView, slidePage - } - - open fun onPageUp() { - } - - abstract fun onScroll( - e1: MotionEvent, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean - - enum class Direction { - NONE, PREV, NEXT + /** + * 按下 + */ + override fun onDown(e: MotionEvent): Boolean { + if (isTextSelected) { + curPage.cancelSelect() + } + //是否移动 + isMoved = false + //是否存在下一章 + noNext = false + //是否正在执行动画 + isRunning = false + //取消 + isCancel = false + //是下一章还是前一章 + setDirection(Direction.NONE) + //设置起始位置的触摸点 + setStartPoint(e.x, e.y) + return true } /** - * 触摸事件处理 + * 单击 */ - private inner class GestureListener : GestureDetector.SimpleOnGestureListener() { - - override fun onDown(e: MotionEvent): Boolean { -// abort() - //是否移动 - isMoved = false - //是否存在下一章 - noNext = false - //是否正在执行动画 - isRunning = false - //取消 - isCancel = false - //是下一章还是前一章 - direction = Direction.NONE - //设置起始位置的触摸点 - setStartPoint(e.x, e.y) + override fun onSingleTapUp(e: MotionEvent): Boolean { + if (isTextSelected) { + isTextSelected = false return true } - - override fun onSingleTapUp(e: MotionEvent): Boolean { - val x = e.x - val y = e.y - if (centerRectF.contains(x, y)) { - pageView.callBack?.clickCenter() - setTouchPoint(x, y) + val x = e.x + val y = e.y + if (centerRectF.contains(x, y)) { + pageView.callBack.clickCenter() + setTouchPoint(x, y) + } else { + if (x > viewWidth / 2 || + AppConfig.clickAllNext + ) { + //设置动画方向 + if (!hasNext()) { + return true + } + setDirection(Direction.NEXT) + setBitmap() } else { - bitmap = if (x > viewWidth / 2 || - pageView.context.getPrefBoolean(PreferKey.clickAllNext, false)) { - //设置动画方向 - if (!hasNext()) { - return true - } - //下一页截图 - nextPage?.screenshot() - } else { - if (!hasPrev()) { - return true - } - //上一页截图 - prevPage?.screenshot() + if (!hasPrev()) { + return true } - setTouchPoint(x, y) - onScrollStart() + setDirection(Direction.PREV) + setBitmap() } - return true + setTouchPoint(x, y) + onAnimStart() } + return true + } - override fun onScroll( - e1: MotionEvent, - e2: MotionEvent, - distanceX: Float, - distanceY: Float - ): Boolean { - return this@PageDelegate.onScroll(e1, e2, distanceX, distanceY) - } + /** + * 长按选择 + */ + override fun onLongPress(e: MotionEvent) { + isTextSelected = curPage.selectText(e) } + /** + * 判断是否有上一页 + */ fun hasPrev(): Boolean { - //上一页的参数配置 - direction = Direction.PREV - val hasPrev = pageView.pageFactory?.hasPrev() == true + val hasPrev = pageView.pageFactory.hasPrev() if (!hasPrev) { - snackbar ?: let { - snackbar = pageView.snackbar("没有上一页") - } - snackbar?.let { - if (!it.isShown) { - it.setText("没有上一页") - it.show() - } + if (!snackBar.isShown) { + snackBar.setText("没有上一页") + snackBar.show() } } return hasPrev } + /** + * 判断是否有下一页 + */ fun hasNext(): Boolean { - //进行下一页的配置 - direction = Direction.NEXT - val hasNext = pageView.pageFactory?.hasNext() == true + val hasNext = pageView.pageFactory.hasNext() if (!hasNext) { - snackbar ?: let { - snackbar = pageView.snackbar("没有下一页") - } - snackbar?.let { - if (!it.isShown) { - it.setText("没有下一页") - it.show() - } + if (!snackBar.isShown) { + snackBar.setText("没有下一页") + snackBar.show() } } return hasNext } -} \ No newline at end of file + + open fun onDestroy() { + bitmap?.recycle() + } + + enum class Direction { + NONE, PREV, NEXT + } + +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt index 5b25392a2..714c26f89 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/ScrollPageDelegate.kt @@ -1,115 +1,75 @@ package io.legado.app.ui.book.read.page.delegate import android.graphics.Canvas -import android.graphics.Matrix import android.view.MotionEvent +import android.view.VelocityTracker import io.legado.app.ui.book.read.page.PageView -import io.legado.app.utils.screenshot import kotlin.math.abs class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { - private val bitmapMatrix = Matrix() + // 滑动追踪的时间 + private val velocityDuration = 1000 + //速度追踪器 + private val mVelocity: VelocityTracker = VelocityTracker.obtain() - override fun onScrollStart() { - if (!atTop && !atBottom) { - stopScroll() - return - } - val distanceY: Float - when (direction) { - Direction.NEXT -> distanceY = - if (isCancel) { - var dis = viewHeight - startY + touchY - if (dis > viewHeight) { - dis = viewHeight.toFloat() - } - viewHeight - dis - } else { - -(touchY + (viewHeight - startY)) - } - else -> distanceY = - if (isCancel) { - -(touchY - startY) - } else { - viewHeight - (touchY - startY) - } - } - - startScroll(0, touchY.toInt(), 0, distanceY.toInt()) + override fun onAnimStart() { + //惯性滚动 + fling( + 0, touchY.toInt(), 0, mVelocity.yVelocity.toInt(), + 0, 0, -10 * viewHeight, 10 * viewHeight + ) } override fun onDraw(canvas: Canvas) { - if (atTop || atBottom) { - val offsetY = touchY - startY - - if ((direction == Direction.NEXT && offsetY > 0) - || (direction == Direction.PREV && offsetY < 0) - ) return - - val distanceY = if (offsetY > 0) offsetY - viewHeight else offsetY + viewHeight - if (atTop && direction == Direction.PREV) { - bitmap?.let { - bitmapMatrix.setTranslate(0.toFloat(), distanceY) - canvas.drawBitmap(it, bitmapMatrix, null) - } - } else if (atBottom && direction == Direction.NEXT) { - bitmap?.let { - bitmapMatrix.setTranslate(0.toFloat(), distanceY) - canvas.drawBitmap(it, bitmapMatrix, null) - } - } - } + curPage.onScroll(lastY - touchY) } - override fun onScrollStop() { + override fun onAnimStop() { if (!isCancel) { - pageView.fillPage(direction) + pageView.fillPage(mDirection) } } + override fun onDown(e: MotionEvent): Boolean { + abort() + mVelocity.clear() + mVelocity.addMovement(e) + return super.onDown(e) + } + override fun onScroll( e1: MotionEvent, e2: MotionEvent, distanceX: Float, distanceY: Float ): Boolean { + mVelocity.addMovement(e2) + mVelocity.computeCurrentVelocity(velocityDuration) + if (!isMoved && abs(distanceX) < abs(distanceY)) { if (distanceY < 0) { if (atTop) { - val event = e1.toAction(MotionEvent.ACTION_UP) - curPage?.dispatchTouchEvent(event) - event.recycle() //如果上一页不存在 if (!hasPrev()) { noNext = true return true } - //上一页截图 - bitmap = prevPage?.screenshot() + setDirection(Direction.PREV) } } else { if (atBottom) { - val event = e1.toAction(MotionEvent.ACTION_UP) - curPage?.dispatchTouchEvent(event) - event.recycle() //如果不存在表示没有下一页了 if (!hasNext()) { noNext = true return true } - //下一页截图 - bitmap = nextPage?.screenshot() + setDirection(Direction.NEXT) } } isMoved = true } - if ((atTop && direction != Direction.PREV) || (atBottom && direction != Direction.NEXT) || direction == Direction.NONE) { - //传递触摸事件到textView - curPage?.dispatchTouchEvent(e2) - } if (isMoved) { - isCancel = if (direction == Direction.NEXT) distanceY < 0 else distanceY > 0 isRunning = true //设置触摸点 setTouchPoint(e2.x, e2.y) @@ -117,4 +77,18 @@ class ScrollPageDelegate(pageView: PageView) : PageDelegate(pageView) { return isMoved } + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent?, + velocityX: Float, + velocityY: Float + ): Boolean { + mVelocity.addMovement(e2) + return super.onFling(e1, e2, velocityX, velocityY) + } + + override fun onDestroy() { + super.onDestroy() + mVelocity.recycle() + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt index ac3f00283..789fb61e1 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SimulationPageDelegate.kt @@ -4,12 +4,16 @@ import android.graphics.* import android.graphics.drawable.GradientDrawable import android.os.Build import io.legado.app.ui.book.read.page.PageView +import io.legado.app.utils.screenshot import kotlin.math.* +@Suppress("DEPRECATION") class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { - - private var mCornerX = 1 // 拖拽点对应的页脚 - + //不让x,y为0,否则在点计算时会有问题 + private var mTouchX = 0.1f + private var mTouchY = 0.1f + // 拖拽点对应的页脚 + private var mCornerX = 1 private var mCornerY = 1 private val mPath0: Path = Path() private val mPath1: Path = Path() @@ -23,52 +27,92 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi private var mBezierEnd1 = PointF() // 另一条贝塞尔曲线 + // 贝塞尔曲线起始点 private val mBezierStart2 = PointF() - + // 贝塞尔曲线控制点 private val mBezierControl2 = PointF() + // 贝塞尔曲线顶点 private val mBezierVertex2 = PointF() + // 贝塞尔曲线结束点 private var mBezierEnd2 = PointF() private var mMiddleX = 0f private var mMiddleY = 0f private var mDegrees = 0f private var mTouchToCornerDis = 0f - private var mColorMatrixFilter: ColorMatrixColorFilter? = null + private var mColorMatrixFilter: ColorMatrixColorFilter = ColorMatrixColorFilter( + ColorMatrix( + floatArrayOf( + 1f, 0f, 0f, 0f, 0f, + 0f, 1f, 0f, 0f, 0f, + 0f, 0f, 1f, 0f, 0f, + 0f, 0f, 0f, 1f, 0f + ) + ) + ) private val mMatrix: Matrix = Matrix() - private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1.0f) + private val mMatrixArray = floatArrayOf(0f, 0f, 0f, 0f, 0f, 0f, 0f, 0f, 1f) // 是否属于右上左下 - private var mIsRT_LB = false + private var mIsRtOrLb = false private var mMaxLength = 0f // 背面颜色组 - private var mBackShadowColors: IntArray? = null + private var mBackShadowColors: IntArray // 前面颜色组 - private var mFrontShadowColors: IntArray? = null + private var mFrontShadowColors: IntArray // 有阴影的GradientDrawable - private var mBackShadowDrawableLR: GradientDrawable? = null - private var mBackShadowDrawableRL: GradientDrawable? = null - private var mFolderShadowDrawableLR: GradientDrawable? = null - private var mFolderShadowDrawableRL: GradientDrawable? = null + private var mBackShadowDrawableLR: GradientDrawable + private var mBackShadowDrawableRL: GradientDrawable + private var mFolderShadowDrawableLR: GradientDrawable + private var mFolderShadowDrawableRL: GradientDrawable - private var mFrontShadowDrawableHBT: GradientDrawable? = null - private var mFrontShadowDrawableHTB: GradientDrawable? = null - private var mFrontShadowDrawableVLR: GradientDrawable? = null - private var mFrontShadowDrawableVRL: GradientDrawable? = null + private var mFrontShadowDrawableHBT: GradientDrawable + private var mFrontShadowDrawableHTB: GradientDrawable + private var mFrontShadowDrawableVLR: GradientDrawable + private var mFrontShadowDrawableVRL: GradientDrawable private val mPaint: Paint = Paint() + private var curBitmap: Bitmap? = null + private var prevBitmap: Bitmap? = null + private var nextBitmap: Bitmap? = null + init { - mMaxLength = hypot(pageView.width.toDouble(), pageView.height.toDouble()).toFloat() + mMaxLength = hypot(viewWidth.toDouble(), viewWidth.toDouble()).toFloat() mPaint.style = Paint.Style.FILL //设置颜色数组 - createDrawable() - val cm = ColorMatrix() - val array = floatArrayOf( - 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f, 0f, 0f, - 0f, 0f, 1f, 0f, 0f, 0f, 0f, 0f, 1f, 0f - ) - cm.set(array) - mColorMatrixFilter = ColorMatrixColorFilter(cm) + val color = intArrayOf(0x333333, -0x4fcccccd) + mFolderShadowDrawableRL = GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, color) + mFolderShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFolderShadowDrawableLR = GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, color) + mFolderShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) + mBackShadowDrawableRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors) + mBackShadowDrawableRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mBackShadowDrawableLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors) + mBackShadowDrawableLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) + mFrontShadowDrawableVLR = + GradientDrawable(GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors) + mFrontShadowDrawableVLR.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableVRL = + GradientDrawable(GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors) + mFrontShadowDrawableVRL.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHTB = + GradientDrawable(GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors) + mFrontShadowDrawableHTB.gradientType = GradientDrawable.LINEAR_GRADIENT + + mFrontShadowDrawableHBT = + GradientDrawable(GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors) + mFrontShadowDrawableHBT.gradientType = GradientDrawable.LINEAR_GRADIENT } override fun setStartPoint(x: Float, y: Float, invalidate: Boolean) { @@ -79,102 +123,111 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi override fun setTouchPoint(x: Float, y: Float, invalidate: Boolean) { super.setTouchPoint(x, y, invalidate) //触摸y中间位置吧y变成屏幕高度 - //触摸y中间位置吧y变成屏幕高度 - if (startY > pageView.height / 3.0 - && startY < pageView.height * 2 / 3.0 - || direction == Direction.PREV + if ((startY > viewHeight * 0.33 && startY < viewHeight * 0.66) + || mDirection == Direction.PREV ) { - touchY = pageView.height.toFloat() + touchY = viewHeight.toFloat() } - if (startY > pageView.height / 3.0 - && startY < pageView.height / 2.0 - && direction == Direction.NEXT + if (startY > viewHeight * 0.33 && startY < viewHeight / 2.0 + && mDirection == Direction.NEXT ) { touchY = 1f } } - override fun onScrollStart() { - val distanceX: Float + override fun setDirection(direction: Direction) { + super.setDirection(direction) when (direction) { - Direction.NEXT -> distanceX = - if (isCancel) { - var dis = viewWidth - startX + touchX - if (dis > viewWidth) { - dis = viewWidth.toFloat() - } - viewWidth - dis + Direction.PREV -> + //上一页滑动不出现对角 + if (startX > viewWidth / 2.0) { + calcCornerXY(startX, viewHeight.toFloat()) } else { - -(touchX + (viewWidth - startX)) + calcCornerXY(viewWidth - startX, viewHeight.toFloat()) } - else -> distanceX = - if (isCancel) { - -(touchX - startX) - } else { - viewWidth - (touchX - startX) + Direction.NEXT -> + if (viewWidth / 2.0 > startX) { + calcCornerXY(viewWidth - startX, startY) } + else -> Unit } - - startScroll(touchX.toInt(), 0, distanceX.toInt(), 0) } - override fun onScrollStop() { - curPage?.x = 0.toFloat() - if (!isCancel) { - pageView.fillPage(direction) + override fun setBitmap() { + when (mDirection) { + Direction.PREV -> { + prevBitmap = prevPage.screenshot() + curBitmap = curPage.screenshot() + } + Direction.NEXT -> { + nextBitmap = nextPage.screenshot() + curBitmap = curPage.screenshot() + } + else -> Unit } } - override fun onDraw(canvas: Canvas) { - bitmap?.let { - if (direction === Direction.NEXT) { - calcPoints() - drawCurrentPageArea(canvas, it, mPath0) //绘制翻页时的正面页 - drawNextPageAreaAndShadow(canvas, it) - drawCurrentPageShadow(canvas) - drawCurrentBackArea(canvas, it) + override fun onAnimStart() { + var dx: Float + val dy: Float + // dx 水平方向滑动的距离,负值会使滚动向左滚动 + // dy 垂直方向滑动的距离,负值会使滚动向上滚动 + if (isCancel) { + dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + (viewWidth - touchX) + } else { + -touchX + } + if (mDirection != Direction.NEXT) { + dx = -(viewWidth + touchX) + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) + } else { + -touchY // 防止mTouchY最终变为0 + } + } else { + dx = if (mCornerX > 0 && mDirection == Direction.NEXT) { + -(viewWidth + touchX) + } else { + (viewWidth - touchX + viewWidth) + } + dy = if (mCornerY > 0) { + (viewHeight - touchY) } else { - calcPoints() - drawCurrentPageArea(canvas, it, mPath0) - drawNextPageAreaAndShadow(canvas, it) - drawCurrentPageShadow(canvas) - drawCurrentBackArea(canvas, it) + (1 - touchY) // 防止mTouchY最终变为0 } } + startScroll(touchX.toInt(), touchY.toInt(), dx.toInt(), dy.toInt()) } - /** - * 创建阴影的GradientDrawable - */ - private fun createDrawable() { - val color = intArrayOf(0x333333, -0x4fcccccd) - mFolderShadowDrawableRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, color - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mFolderShadowDrawableLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, color - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mBackShadowColors = intArrayOf(-0xeeeeef, 0x111111) - mBackShadowDrawableRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, mBackShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mBackShadowDrawableLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, mBackShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mFrontShadowColors = intArrayOf(-0x7feeeeef, 0x111111) - mFrontShadowDrawableVLR = GradientDrawable( - GradientDrawable.Orientation.LEFT_RIGHT, mFrontShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mFrontShadowDrawableVRL = GradientDrawable( - GradientDrawable.Orientation.RIGHT_LEFT, mFrontShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mFrontShadowDrawableHTB = GradientDrawable( - GradientDrawable.Orientation.TOP_BOTTOM, mFrontShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } - mFrontShadowDrawableHBT = GradientDrawable( - GradientDrawable.Orientation.BOTTOM_TOP, mFrontShadowColors - ).apply { gradientType = GradientDrawable.LINEAR_GRADIENT } + override fun onAnimStop() { + if (!isCancel) { + pageView.fillPage(mDirection) + } + prevBitmap?.recycle() + prevBitmap = null + nextBitmap?.recycle() + nextBitmap = null + curBitmap?.recycle() + curBitmap = null + } + + override fun onDraw(canvas: Canvas) { + if (mDirection === Direction.NEXT) { + calcPoints() + drawCurrentPageArea(canvas, curBitmap, mPath0) + drawNextPageAreaAndShadow(canvas, nextBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, curBitmap) + } else { + calcPoints() + drawCurrentPageArea(canvas, prevBitmap, mPath0) + drawNextPageAreaAndShadow(canvas, curBitmap) + drawCurrentPageShadow(canvas) + drawCurrentBackArea(canvas, prevBitmap) + } } /** @@ -182,42 +235,41 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi */ private fun drawCurrentBackArea( canvas: Canvas, - bitmap: Bitmap + bitmap: Bitmap? ) { - val i = (mBezierStart1.x + mBezierControl1.x).toInt() / 2 + bitmap ?: return + val i = ((mBezierStart1.x + mBezierControl1.x) / 2).toInt() val f1 = abs(i - mBezierControl1.x) - val i1 = (mBezierStart2.y + mBezierControl2.y).toInt() / 2 + val i1 = ((mBezierStart2.y + mBezierControl2.y) / 2).toInt() val f2 = abs(i1 - mBezierControl2.y) val f3 = min(f1, f2) mPath1.reset() mPath1.moveTo(mBezierVertex2.x, mBezierVertex2.y) mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) mPath1.lineTo(mBezierEnd1.x, mBezierEnd1.y) - mPath1.lineTo(touchX, touchY) + mPath1.lineTo(mTouchX, mTouchY) mPath1.lineTo(mBezierEnd2.x, mBezierEnd2.y) mPath1.close() val mFolderShadowDrawable: GradientDrawable val left: Int val right: Int - if (mIsRT_LB) { - left = (mBezierStart1.x - 1).toInt() + if (mIsRtOrLb) { + left = mBezierStart1.x.toInt() - 1 right = (mBezierStart1.x + f3 + 1).toInt() - mFolderShadowDrawable = mFolderShadowDrawableLR!! + mFolderShadowDrawable = mFolderShadowDrawableLR } else { left = (mBezierStart1.x - f3 - 1).toInt() right = (mBezierStart1.x + 1).toInt() - mFolderShadowDrawable = mFolderShadowDrawableRL!! + mFolderShadowDrawable = mFolderShadowDrawableRL } canvas.save() - try { - canvas.clipPath(mPath0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } - } catch (ignored: Exception) { + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) } + mPaint.colorFilter = mColorMatrixFilter val dis = hypot( mCornerX - mBezierControl1.x.toDouble(), @@ -237,8 +289,8 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi mPaint.colorFilter = null canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) mFolderShadowDrawable.setBounds( - left, mBezierStart1.y.toInt(), right, - (mBezierStart1.y + mMaxLength).toInt() + left, mBezierStart1.y.toInt(), + right, (mBezierStart1.y + mMaxLength).toInt() ) mFolderShadowDrawable.draw(canvas) canvas.restore() @@ -248,109 +300,107 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi * 绘制翻起页的阴影 */ private fun drawCurrentPageShadow(canvas: Canvas) { - val degree: Double = if (mIsRT_LB) { - (Math.PI / 4 - atan2(mBezierControl1.y - touchX, touchY - mBezierControl1.x)) + val degree: Double = if (mIsRtOrLb) { + Math.PI / 4 - atan2(mBezierControl1.y - mTouchY, mTouchX - mBezierControl1.x) } else { - (Math.PI / 4 - atan2(touchY - mBezierControl1.y, touchX - mBezierControl1.x)) + Math.PI / 4 - atan2(mTouchY - mBezierControl1.y, mTouchX - mBezierControl1.x) } // 翻起页阴影顶点与touch点的距离 val d1 = 25.toFloat() * 1.414 * cos(degree) val d2 = 25.toFloat() * 1.414 * sin(degree) - val x = (touchX + d1).toFloat() - val y: Float - y = if (mIsRT_LB) { - (touchY + d2).toFloat() + val x = (mTouchX + d1).toFloat() + val y: Float = if (mIsRtOrLb) { + (mTouchY + d2).toFloat() } else { - (touchY - d2).toFloat() + (mTouchY - d2).toFloat() } mPath1.reset() mPath1.moveTo(x, y) - mPath1.lineTo(touchX, touchY) + mPath1.lineTo(mTouchX, mTouchY) mPath1.lineTo(mBezierControl1.x, mBezierControl1.y) mPath1.lineTo(mBezierStart1.x, mBezierStart1.y) mPath1.close() canvas.save() - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipOutPath(mPath0) - } else { - canvas.clipPath(mPath0, Region.Op.XOR) - } - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } catch (ignored: java.lang.Exception) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) } + canvas.clipPath(mPath1, Region.Op.INTERSECT) + var leftX: Int var rightX: Int var mCurrentPageShadow: GradientDrawable - if (mIsRT_LB) { + if (mIsRtOrLb) { leftX = mBezierControl1.x.toInt() rightX = mBezierControl1.x.toInt() + 25 - mCurrentPageShadow = mFrontShadowDrawableVLR!! + mCurrentPageShadow = mFrontShadowDrawableVLR } else { - leftX = (mBezierControl1.x - 25).toInt() + leftX = mBezierControl1.x.toInt() - 25 rightX = mBezierControl1.x.toInt() + 1 - mCurrentPageShadow = mFrontShadowDrawableVRL!! + mCurrentPageShadow = mFrontShadowDrawableVRL } var rotateDegrees: Float = - Math.toDegrees(atan2(touchX - mBezierControl1.x, mBezierControl1.y - touchY).toDouble()) - .toFloat() + Math.toDegrees( + atan2(mTouchX - mBezierControl1.x, mBezierControl1.y - mTouchY).toDouble() + ).toFloat() canvas.rotate(rotateDegrees, mBezierControl1.x, mBezierControl1.y) mCurrentPageShadow.setBounds( - leftX, - (mBezierControl1.y - mMaxLength).toInt(), rightX, - mBezierControl1.y.toInt() + leftX, (mBezierControl1.y - mMaxLength).toInt(), + rightX, mBezierControl1.y.toInt() ) mCurrentPageShadow.draw(canvas) canvas.restore() + mPath1.reset() mPath1.moveTo(x, y) - mPath1.lineTo(touchX, touchY) + mPath1.lineTo(mTouchX, mTouchY) mPath1.lineTo(mBezierControl2.x, mBezierControl2.y) mPath1.lineTo(mBezierStart2.x, mBezierStart2.y) mPath1.close() canvas.save() - try { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipOutPath(mPath0) - } else { - canvas.clipPath(mPath0, Region.Op.XOR) - } - canvas.clipPath(mPath1) - } catch (ignored: java.lang.Exception) { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipOutPath(mPath0) + } else { + canvas.clipPath(mPath0, Region.Op.XOR) } - if (mIsRT_LB) { + canvas.clipPath(mPath1) + + if (mIsRtOrLb) { leftX = mBezierControl2.y.toInt() - rightX = (mBezierControl2.y + 25).toInt() - mCurrentPageShadow = mFrontShadowDrawableHTB!! + rightX = mBezierControl2.y.toInt() + 25 + mCurrentPageShadow = mFrontShadowDrawableHTB } else { - leftX = (mBezierControl2.y - 25).toInt() - rightX = (mBezierControl2.y + 1).toInt() - mCurrentPageShadow = mFrontShadowDrawableHBT!! + leftX = mBezierControl2.y.toInt() - 25 + rightX = mBezierControl2.y.toInt() + 1 + mCurrentPageShadow = mFrontShadowDrawableHBT } rotateDegrees = Math.toDegrees( - atan2(mBezierControl2.y - touchY, mBezierControl2.x - touchX).toDouble() + atan2(mBezierControl2.y - mTouchY, mBezierControl2.x - mTouchX).toDouble() ).toFloat() canvas.rotate(rotateDegrees, mBezierControl2.x, mBezierControl2.y) val temp: Float = - if (mBezierControl2.y < 0) mBezierControl2.y - pageView.height else mBezierControl2.y + if (mBezierControl2.y < 0) mBezierControl2.y - viewHeight else mBezierControl2.y val hmg = hypot(mBezierControl2.x.toDouble(), temp.toDouble()).toInt() - if (hmg > mMaxLength) mCurrentPageShadow - .setBounds( + if (hmg > mMaxLength) + mCurrentPageShadow.setBounds( (mBezierControl2.x - 25).toInt() - hmg, leftX, - (mBezierControl2.x + mMaxLength).toInt() - hmg, - rightX - ) else mCurrentPageShadow.setBounds( - (mBezierControl2.x - mMaxLength).toInt(), leftX, - mBezierControl2.x.toInt(), rightX - ) + (mBezierControl2.x + mMaxLength).toInt() - hmg, rightX + ) + else + mCurrentPageShadow.setBounds( + (mBezierControl2.x - mMaxLength).toInt(), leftX, + mBezierControl2.x.toInt(), rightX + ) mCurrentPageShadow.draw(canvas) canvas.restore() } private fun drawNextPageAreaAndShadow( canvas: Canvas, - bitmap: Bitmap + bitmap: Bitmap? ) { + bitmap ?: return mPath1.reset() mPath1.moveTo(mBezierStart1.x, mBezierStart1.y) mPath1.lineTo(mBezierVertex1.x, mBezierVertex1.y) @@ -365,33 +415,29 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi ) ).toFloat() val leftX: Int - val rightY: Int + val rightX: Int val mBackShadowDrawable: GradientDrawable - if (mIsRT_LB) { //左下及右上 + if (mIsRtOrLb) { //左下及右上 leftX = mBezierStart1.x.toInt() - rightY = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() - mBackShadowDrawable = mBackShadowDrawableLR!! + rightX = (mBezierStart1.x + mTouchToCornerDis / 4).toInt() + mBackShadowDrawable = mBackShadowDrawableLR } else { leftX = (mBezierStart1.x - mTouchToCornerDis / 4).toInt() - rightY = mBezierStart1.x.toInt() - mBackShadowDrawable = mBackShadowDrawableRL!! + rightX = mBezierStart1.x.toInt() + mBackShadowDrawable = mBackShadowDrawableRL } canvas.save() - try { - canvas.clipPath(mPath0) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - canvas.clipPath(mPath1) - } else { - canvas.clipPath(mPath1, Region.Op.INTERSECT) - } - //canvas.clipPath(mPath1, Region.Op.INTERSECT); - } catch (ignored: java.lang.Exception) { + canvas.clipPath(mPath0) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + canvas.clipPath(mPath1) + } else { + canvas.clipPath(mPath1, Region.Op.INTERSECT) } canvas.drawBitmap(bitmap, 0f, 0f, null) canvas.rotate(mDegrees, mBezierStart1.x, mBezierStart1.y) mBackShadowDrawable.setBounds( - leftX, mBezierStart1.y.toInt(), rightY, - (mMaxLength + mBezierStart1.y).toInt() + leftX, mBezierStart1.y.toInt(), + rightX, (mMaxLength + mBezierStart1.y).toInt() ) //左上及右下角的xy坐标值,构成一个矩形 mBackShadowDrawable.draw(canvas) canvas.restore() @@ -399,21 +445,16 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi private fun drawCurrentPageArea( canvas: Canvas, - bitmap: Bitmap, + bitmap: Bitmap?, path: Path ) { + bitmap ?: return mPath0.reset() mPath0.moveTo(mBezierStart1.x, mBezierStart1.y) - mPath0.quadTo( - mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, - mBezierEnd1.y - ) - mPath0.lineTo(touchX, touchY) + mPath0.quadTo(mBezierControl1.x, mBezierControl1.y, mBezierEnd1.x, mBezierEnd1.y) + mPath0.lineTo(mTouchX, mTouchY) mPath0.lineTo(mBezierEnd2.x, mBezierEnd2.y) - mPath0.quadTo( - mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, - mBezierStart2.y - ) + mPath0.quadTo(mBezierControl2.x, mBezierControl2.y, mBezierStart2.x, mBezierStart2.y) mPath0.lineTo(mCornerX.toFloat(), mCornerY.toFloat()) mPath0.close() canvas.save() @@ -423,86 +464,72 @@ class SimulationPageDelegate(pageView: PageView) : HorizontalPageDelegate(pageVi canvas.clipPath(path, Region.Op.XOR) } canvas.drawBitmap(bitmap, 0f, 0f, null) - try { - canvas.restore() - } catch (e: java.lang.Exception) { - e.printStackTrace() - } + canvas.restore() } /** * 计算拖拽点对应的拖拽脚 */ private fun calcCornerXY(x: Float, y: Float) { - mCornerX = if (x <= pageView.width / 2.0) { - 0 - } else { - pageView.width - } - mCornerY = if (y <= pageView.height / 2.0) { - 0 - } else { - pageView.height - } - mIsRT_LB = (mCornerX == 0 && mCornerY == pageView.height - || mCornerX == pageView.width && mCornerY == 0) + mCornerX = if (x <= viewWidth / 2.0) 0 else viewWidth + mCornerY = if (y <= viewHeight / 2.0) 0 else viewHeight + mIsRtOrLb = (mCornerX == 0 && mCornerY == viewHeight) + || (mCornerY == 0 && mCornerX == viewWidth) } private fun calcPoints() { - mMiddleX = (touchX + mCornerX) / 2 - mMiddleY = (touchY + mCornerY) / 2 + mTouchX = touchX + mTouchY = touchY + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) mBezierControl1.y = mCornerY.toFloat() + mBezierControl2.x = mCornerX.toFloat() - val f4 = mCornerY - mMiddleY - if (f4 == 0f) { - mBezierControl2.y = mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + mBezierControl2.y = if ((mCornerY - mMiddleY).toInt() == 0) { + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f } else { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) } mBezierStart1.x = mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2 mBezierStart1.y = mCornerY.toFloat() - // 当mBezierStart1.x < 0或者mBezierStart1.x > 480时 - // 如果继续翻页,会出现BUG故在此限制 - if (touchX > 0 && touchX < pageView.width) { - if (mBezierStart1.x < 0 || mBezierStart1.x > pageView.width) { - if (mBezierStart1.x < 0) mBezierStart1.x = pageView.width - mBezierStart1.x - val f1: Float = abs(mCornerX - touchX) - val f2: Float = pageView.width * f1 / mBezierStart1.x - touchX = abs(mCornerX - f2) - val f3: Float = abs(mCornerX - touchX) * abs(mCornerY - touchX) / f1 - touchX = abs(mCornerY - f3) - mMiddleX = (touchX + mCornerX) / 2 - mMiddleY = (touchY + mCornerY) / 2 + //固定左边上下两个点 + if (mTouchX > 0 && mTouchX < viewWidth) { + if (mBezierStart1.x < 0 || mBezierStart1.x > viewWidth) { + if (mBezierStart1.x < 0) + mBezierStart1.x = viewWidth - mBezierStart1.x + + val f1: Float = abs(mCornerX - mTouchX) + val f2: Float = viewWidth * f1 / mBezierStart1.x + mTouchX = abs(mCornerX - f2) + val f3: Float = abs(mCornerX - mTouchX) * abs(mCornerY - mTouchY) / f1 + mTouchY = abs(mCornerY - f3) + + mMiddleX = (mTouchX + mCornerX) / 2 + mMiddleY = (mTouchY + mCornerY) / 2 mBezierControl1.x = mMiddleX - (mCornerY - mMiddleY) * (mCornerY - mMiddleY) / (mCornerX - mMiddleX) mBezierControl1.y = mCornerY.toFloat() mBezierControl2.x = mCornerX.toFloat() - val f5 = mCornerY - mMiddleY - if (f5 == 0f) { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f + mBezierControl2.y = if ((mCornerY - mMiddleY).toInt() == 0) { + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / 0.1f } else { - mBezierControl2.y = - mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) + mMiddleY - (mCornerX - mMiddleX) * (mCornerX - mMiddleX) / (mCornerY - mMiddleY) } - mBezierStart1.x = (mBezierControl1.x - - (mCornerX - mBezierControl1.x) / 2) + mBezierStart1.x = (mBezierControl1.x - (mCornerX - mBezierControl1.x) / 2) } } mBezierStart2.x = mCornerX.toFloat() mBezierStart2.y = mBezierControl2.y - (mCornerY - mBezierControl2.y) / 2 - mTouchToCornerDis = hypot(touchX - mCornerX, touchY - mCornerY) - mBezierEnd1 = getCross( - PointF(touchX, touchY), mBezierControl1, mBezierStart1, - mBezierStart2 - ) - mBezierEnd2 = getCross( - PointF(touchX, touchY), mBezierControl2, mBezierStart1, - mBezierStart2 - ) + + mTouchToCornerDis = hypot(mTouchX - mCornerX, touchY - mCornerY) + + mBezierEnd1 = + getCross(PointF(mTouchX, mTouchY), mBezierControl1, mBezierStart1, mBezierStart2) + mBezierEnd2 = + getCross(PointF(mTouchX, mTouchY), mBezierControl2, mBezierStart1, mBezierStart2) + mBezierVertex1.x = (mBezierStart1.x + 2 * mBezierControl1.x + mBezierEnd1.x) / 4 mBezierVertex1.y = (2 * mBezierControl1.y + mBezierStart1.y + mBezierEnd1.y) / 4 mBezierVertex2.x = (mBezierStart2.x + 2 * mBezierControl2.x + mBezierEnd2.x) / 4 diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt index 24c0781d7..0308d965e 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/delegate/SlidePageDelegate.kt @@ -8,9 +8,9 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { private val bitmapMatrix = Matrix() - override fun onScrollStart() { + override fun onAnimStart() { val distanceX: Float - when (direction) { + when (mDirection) { Direction.NEXT -> distanceX = if (isCancel) { var dis = viewWidth - startX + touchX @@ -35,8 +35,8 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onDraw(canvas: Canvas) { val offsetX = touchX - startX - if ((direction == Direction.NEXT && offsetX > 0) - || (direction == Direction.PREV && offsetX < 0) + if ((mDirection == Direction.NEXT && offsetX > 0) + || (mDirection == Direction.PREV && offsetX < 0) ) return val distanceX = if (offsetX > 0) offsetX - viewWidth else offsetX + viewWidth @@ -49,18 +49,18 @@ class SlidePageDelegate(pageView: PageView) : HorizontalPageDelegate(pageView) { override fun onScroll() { val offsetX = touchX - startX - if ((direction == Direction.NEXT && offsetX > 0) - || (direction == Direction.PREV && offsetX < 0) + if ((mDirection == Direction.NEXT && offsetX > 0) + || (mDirection == Direction.PREV && offsetX < 0) ) return - curPage?.translationX = offsetX + curPage.translationX = offsetX } - override fun onScrollStop() { - curPage?.x = 0.toFloat() + override fun onAnimStop() { + curPage.x = 0.toFloat() if (!isCancel) { - pageView.fillPage(direction) + pageView.fillPage(mDirection) } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/TextChapter.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt similarity index 81% rename from app/src/main/java/io/legado/app/ui/book/read/page/TextChapter.kt rename to app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt index a4dacac60..e2cbaafde 100644 --- a/app/src/main/java/io/legado/app/ui/book/read/page/TextChapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt @@ -1,6 +1,5 @@ -package io.legado.app.ui.book.read.page +package io.legado.app.ui.book.read.page.entities -import android.text.SpannableStringBuilder import kotlin.math.min data class TextChapter( @@ -28,12 +27,12 @@ data class TextChapter( fun scrollPage(): TextPage? { if (pages.isNotEmpty()) { - val spannableStringBuilder = SpannableStringBuilder() + val stringBuilder = StringBuilder() pages.forEach { - spannableStringBuilder.append(it.text) + stringBuilder.append(it.text) } return TextPage( - index = 0, text = spannableStringBuilder, title = title, + index = 0, text = stringBuilder.toString(), title = title, pageSize = pages.size, chapterSize = chaptersSize, chapterIndex = position ) } @@ -54,7 +53,7 @@ data class TextChapter( fun getReadLength(pageIndex: Int): Int { var length = 0 - val maxIndex = min(pageIndex, pageLines.size) + val maxIndex = min(pageIndex, pages.size) for (index in 0 until maxIndex) { length += pageLengths[index] } @@ -92,5 +91,13 @@ data class TextChapter( } return 0 } + + fun getContent(): String { + val stringBuilder = StringBuilder() + pages.forEach { + stringBuilder.append(it.text) + } + return stringBuilder.toString() + } } diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt new file mode 100644 index 000000000..9883e6dab --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt @@ -0,0 +1,8 @@ +package io.legado.app.ui.book.read.page.entities + +data class TextChar( + val charData: String, + var selected: Boolean = false, + val leftBottomPosition: TextPoint, + val rightTopPosition: TextPoint +) \ No newline at end of file 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 new file mode 100644 index 000000000..00ae4bff6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt @@ -0,0 +1,10 @@ +package io.legado.app.ui.book.read.page.entities + +data class TextLine( + var text: String = "", + val textChars: ArrayList = arrayListOf(), + var lineTop: Float = 0f, + var lineBottom: Float = 0f, + val isTitle: Boolean = false, + var isReadAloud: Boolean = false +) \ 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 new file mode 100644 index 000000000..92d91b1e5 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt @@ -0,0 +1,49 @@ +package io.legado.app.ui.book.read.page.entities + +import io.legado.app.App +import io.legado.app.R + +data class TextPage( + var index: Int = 0, + var text: String = App.INSTANCE.getString(R.string.data_loading), + var title: String = "", + val textLines: ArrayList = arrayListOf(), + var pageSize: Int = 0, + var chapterSize: Int = 0, + var chapterIndex: Int = 0, + var height: Int = 0 +) { + + fun removePageAloudSpan(): TextPage { + textLines.forEach { textLine -> + textLine.isReadAloud = false + } + return this + } + + fun upPageAloudSpan(pageStart: Int) { + removePageAloudSpan() + var lineStart = 0 + for ((index, textLine) in textLines.withIndex()) { + if (pageStart > lineStart && pageStart < lineStart + textLine.text.length) { + for (i in index - 1 downTo 0) { + if (textLines[i].text.endsWith("\n")) { + break + } else { + textLines[i].isReadAloud = true + } + } + for (i in index until textLines.size) { + if (textLines[i].text.endsWith("\n")) { + textLines[i].isReadAloud = true + break + } else { + textLines[i].isReadAloud = true + } + } + break + } + lineStart += textLine.text.length + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPoint.kt b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPoint.kt new file mode 100644 index 000000000..07bb142b6 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPoint.kt @@ -0,0 +1,6 @@ +package io.legado.app.ui.book.read.page.entities + +data class TextPoint( + val x: Float, + val y: Float +) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt index 4165e5c23..05ea3c782 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/BookAdapter.kt @@ -5,16 +5,25 @@ import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.Book -import kotlinx.android.synthetic.main.item_text.view.* +import kotlinx.android.synthetic.main.item_fillet_text.view.* import org.jetbrains.anko.sdk27.listeners.onClick class BookAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_text) { + SimpleRecyclerAdapter(context, R.layout.item_fillet_text) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { with(holder.itemView) { text_view.text = item.name - onClick { callBack.showBookInfo(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it) + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt index 37158ea5b..8db633cf8 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/DiffCallBack.kt @@ -1,30 +1,76 @@ package io.legado.app.ui.book.search +import android.os.Bundle import androidx.recyclerview.widget.DiffUtil import io.legado.app.data.entities.SearchBook -class DiffCallBack : DiffUtil.ItemCallback() { - override fun areItemsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { - return oldItem.name == newItem.name - && oldItem.author == newItem.author +class DiffCallBack(private val oldItems: List, private val newItems: List) : + DiffUtil.Callback() { + + override fun getNewListSize(): Int { + return newItems.size + } + + override fun getOldListSize(): Int { + return oldItems.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return true } - override fun areContentsTheSame(oldItem: SearchBook, newItem: SearchBook): Boolean { - return oldItem.origins?.size == newItem.origins?.size - && (oldItem.coverUrl == newItem.coverUrl || !oldItem.coverUrl.isNullOrEmpty()) - && (oldItem.kind == newItem.kind || !oldItem.kind.isNullOrEmpty()) - && (oldItem.latestChapterTitle == newItem.latestChapterTitle || !oldItem.kind.isNullOrEmpty()) - && oldItem.intro?.length ?: 0 > newItem.intro?.length ?: 0 + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.author != newItem.author) { + return false + } + if (oldItem.origins?.size != newItem.origins?.size) { + return false + } + if (oldItem.coverUrl != newItem.coverUrl) { + return false + } + if (oldItem.kind != newItem.kind) { + return false + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + return false + } + if (oldItem.intro != newItem.intro) { + return false + } + return true } - override fun getChangePayload(oldItem: SearchBook, newItem: SearchBook): Any? { - return when { - oldItem.origins?.size != newItem.origins?.size -> 1 - oldItem.coverUrl != newItem.coverUrl -> 2 - oldItem.kind != newItem.kind -> 3 - oldItem.latestChapterTitle != newItem.latestChapterTitle -> 4 - oldItem.intro != newItem.intro -> 5 - else -> null + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putString("name", newItem.name) + } + if (oldItem.author != newItem.author) { + payload.putString("author", newItem.author) + } + if (oldItem.origins?.size != newItem.origins?.size) { + payload.putInt("origins", newItem.origins?.size ?: 1) + } + if (oldItem.coverUrl != newItem.coverUrl) { + payload.putString("group", newItem.coverUrl) + } + if (oldItem.kind != newItem.kind) { + payload.putString("enabled", newItem.kind) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + payload.putString("enabled", newItem.latestChapterTitle) + } + if (oldItem.intro != newItem.intro) { + payload.putString("enabled", newItem.intro) } + return payload } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt index 213157e90..e59605495 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/HistoryKeyAdapter.kt @@ -1,13 +1,12 @@ package io.legado.app.ui.book.search -import android.content.Context import io.legado.app.App import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchKeyword import io.legado.app.ui.widget.anima.explosion_field.ExplosionField -import kotlinx.android.synthetic.main.item_text.view.* +import kotlinx.android.synthetic.main.item_fillet_text.view.* import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch @@ -15,21 +14,32 @@ import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick -class HistoryKeyAdapter(context: Context, val callBack: CallBack) : - SimpleRecyclerAdapter(context, R.layout.item_text) { +class HistoryKeyAdapter(activity: SearchActivity, val callBack: CallBack) : + SimpleRecyclerAdapter(activity, R.layout.item_fillet_text) { + + private val explosionField = ExplosionField.attach2Window(activity) override fun convert(holder: ItemViewHolder, item: SearchKeyword, payloads: MutableList) { with(holder.itemView) { text_view.text = item.word + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { onClick { - callBack.searchHistory(item.word) + getItem(holder.layoutPosition)?.let { + callBack.searchHistory(it.word) + } } onLongClick { it?.let { - ExplosionField(context).explode(it, true) + explosionField.explode(it, true) } - GlobalScope.launch(IO) { - App.db.searchKeywordDao().delete(item) + getItem(holder.layoutPosition)?.let { + GlobalScope.launch(IO) { + App.db.searchKeywordDao().delete(it) + } } true } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt index ae44ddaad..1227f4232 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchActivity.kt @@ -8,6 +8,7 @@ import android.view.View.VISIBLE import androidx.appcompat.widget.SearchView import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.App @@ -15,12 +16,13 @@ import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book +import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.book.info.BookInfoActivity import io.legado.app.ui.book.source.manage.BookSourceActivity -import io.legado.app.ui.widget.LoadMoreView +import io.legado.app.ui.widget.recycler.LoadMoreView import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_book_search.* import kotlinx.android.synthetic.main.view_search.* @@ -32,7 +34,6 @@ import org.jetbrains.anko.startActivity class SearchActivity : VMBaseActivity(R.layout.activity_book_search), - SearchViewModel.CallBack, BookAdapter.CallBack, HistoryKeyAdapter.CallBack, SearchAdapter.CallBack { @@ -40,7 +41,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se override val viewModel: SearchViewModel get() = getViewModel(SearchViewModel::class.java) - override lateinit var adapter: SearchAdapter + lateinit var adapter: SearchAdapter private lateinit var bookAdapter: BookAdapter private lateinit var historyKeyAdapter: HistoryKeyAdapter private lateinit var loadMoreView: LoadMoreView @@ -49,13 +50,13 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se private var menu: Menu? = null private var precisionSearchMenuItem: MenuItem? = null private var groups = hashSetOf() + private var refreshTime = System.currentTimeMillis() override fun onActivityCreated(savedInstanceState: Bundle?) { - viewModel.callBack = this initRecyclerView() initSearchView() initOtherView() - initData() + initLiveData() initIntent() } @@ -76,7 +77,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se !getPrefBoolean(PreferKey.precisionSearch) ) precisionSearchMenuItem?.isChecked = getPrefBoolean(PreferKey.precisionSearch) - search_view.query.toString().trim()?.let { + search_view.query?.toString()?.trim()?.let { search_view.setQuery(it, true) } } @@ -88,7 +89,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } else { putPrefString("searchGroup", item.title.toString()) } - search_view.query.toString().trim()?.let { + search_view.query?.toString()?.trim()?.let { search_view.setQuery(it, true) } } @@ -141,6 +142,14 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se adapter = SearchAdapter(this, this) recycler_view.layoutManager = LinearLayoutManager(this) recycler_view.adapter = adapter + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0) { + recycler_view.scrollToPosition(0) + } + } + }) loadMoreView = LoadMoreView(this) recycler_view.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { @@ -160,7 +169,7 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } } - private fun initData() { + private fun initLiveData() { App.db.bookSourceDao().liveGroupEnabled().observe(this, Observer { groups.clear() it.map { group -> @@ -168,6 +177,17 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } upGroupMenu() }) + viewModel.searchBookLiveData.observe(this, Observer { + upSearchItems(it, false) + }) + viewModel.isSearchLiveData.observe(this, Observer { + if (it) { + startSearch() + } else { + searchFinally() + upSearchItems(viewModel.searchBooks, true) + } + }) } private fun initIntent() { @@ -178,12 +198,18 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } } + /** + * 滚动到底部事件 + */ private fun scrollToBottom() { if (!viewModel.isLoading && viewModel.searchKey.isNotEmpty() && loadMoreView.hasMore) { viewModel.search("") } } + /** + * 打开关闭历史界面 + */ private fun openOrCloseHistory(open: Boolean) { if (open) { upHistory(search_view.query.toString()) @@ -193,6 +219,9 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } } + /** + * 更新分组菜单 + */ private fun upGroupMenu() { val selectedGroup = getPrefString("searchGroup") ?: "" menu?.removeGroup(R.id.source_group) @@ -209,6 +238,9 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se menu?.setGroupCheckable(R.id.source_group, true, true) } + /** + * 更新搜索历史 + */ private fun upHistory(key: String? = null) { bookData?.removeObservers(this) if (key.isNullOrBlank()) { @@ -243,18 +275,39 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se }) } - override fun startSearch() { + /** + * 更新搜索结果 + */ + @Synchronized + private fun upSearchItems(items: List, isMandatoryUpdate: Boolean) { + if (!isMandatoryUpdate && System.currentTimeMillis() - refreshTime < 1000) { + return + } + refreshTime = System.currentTimeMillis() + val diffResult = DiffUtil.calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), items)) + adapter.setItems(items, diffResult) + } + + /** + * 开始搜索 + */ + private fun startSearch() { refresh_progress_bar.isAutoLoading = true - initData() fb_stop.visible() } - override fun searchFinally() { + /** + * 搜索结束 + */ + private fun searchFinally() { refresh_progress_bar.isAutoLoading = false loadMoreView.startLoad() fb_stop.invisible() } + /** + * 显示书籍详情 + */ override fun showBookInfo(name: String, author: String) { viewModel.getSearchBook(name, author) { searchBook -> searchBook?.let { @@ -263,18 +316,30 @@ class SearchActivity : VMBaseActivity(R.layout.activity_book_se } } + /** + * 显示书籍详情 + */ override fun showBookInfo(book: Book) { startActivity( Pair("bookUrl", book.bookUrl) ) } + /** + * 点击历史关键字 + */ override fun searchHistory(key: String) { launch { - if (withContext(IO) { App.db.bookDao().findByName(key).isEmpty() }) { - search_view.setQuery(key, true) - } else { - search_view.setQuery(key, false) + when { + search_view.query.toString() == key -> { + search_view.setQuery(key, true) + } + withContext(IO) { App.db.bookDao().findByName(key).isEmpty() } -> { + search_view.setQuery(key, true) + } + else -> { + search_view.setQuery(key, false) + } } } } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt index 04d2e46f8..fbdcb99cf 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchAdapter.kt @@ -1,12 +1,12 @@ package io.legado.app.ui.book.search import android.content.Context +import android.os.Bundle import android.view.View import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchBook -import io.legado.app.help.ImageLoader import io.legado.app.utils.gone import io.legado.app.utils.visible import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover @@ -18,10 +18,21 @@ class SearchAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_search) { override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { - if (payloads.isEmpty()) { + val bundle = payloads.getOrNull(0) as? Bundle + if (bundle == null) { bind(holder.itemView, item) } else { - bindChange(holder.itemView, item, payloads) + bindChange(holder.itemView, item, bundle) + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it.name, it.author) + } + } } } @@ -30,118 +41,60 @@ class SearchAdapter(context: Context, val callBack: CallBack) : tv_name.text = searchBook.name tv_author.text = context.getString(R.string.author_show, searchBook.author) bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1) - if (searchBook.latestChapterTitle.isNullOrEmpty()) { - tv_lasted.gone() - } else { - tv_lasted.text = context.getString(R.string.lasted_show, searchBook.latestChapterTitle) - tv_lasted.visible() - } + upLasted(itemView, searchBook.latestChapterTitle) tv_introduce.text = context.getString(R.string.intro_show, searchBook.intro) - val kinds = searchBook.getKindList() - if (kinds.isEmpty()) { - ll_kind.gone() - } else { - ll_kind.visible() - for (index in 0..2) { - if (kinds.size > index) { - when (index) { - 0 -> { - tv_kind.text = kinds[index] - tv_kind.visible() - } - 1 -> { - tv_kind_1.text = kinds[index] - tv_kind_1.visible() - } - 2 -> { - tv_kind_2.text = kinds[index] - tv_kind_2.visible() - } - } - } else { - when (index) { - 0 -> tv_kind.gone() - 1 -> tv_kind_1.gone() - 2 -> tv_kind_2.gone() - } - } + upKind(itemView, searchBook.getKindList()) + iv_cover.load(searchBook.coverUrl, searchBook.name, searchBook.author) + + } + } + + private fun bindChange(itemView: View, searchBook: SearchBook, bundle: Bundle) { + with(itemView) { + bundle.keySet().map { + when (it) { + "name" -> tv_name.text = searchBook.name + "author" -> tv_author.text = + context.getString(R.string.author_show, searchBook.author) + "originCount" -> bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1) + "lasted" -> upLasted(itemView, searchBook.latestChapterTitle) + "introduce" -> tv_introduce.text = + context.getString(R.string.intro_show, searchBook.intro) + "kind" -> upKind(itemView, searchBook.getKindList()) + "cover" -> iv_cover.load( + searchBook.coverUrl, + searchBook.name, + searchBook.author + ) } } - searchBook.coverUrl.let { - ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.image_cover_default) - .error(R.drawable.image_cover_default) - .centerCrop() - .into(iv_cover) - } - onClick { - callBack.showBookInfo(searchBook.name, searchBook.author) - } } } - private fun bindChange(itemView: View, searchBook: SearchBook, payloads: MutableList) { + private fun upLasted(itemView: View, latestChapterTitle: String?) { with(itemView) { - when (payloads[0]) { - 1 -> bv_originCount.setBadgeCount(searchBook.origins?.size ?: 1) - 2 -> searchBook.coverUrl.let { - ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.image_cover_default) - .error(R.drawable.image_cover_default) - .centerCrop() - .into(iv_cover) - } - 3 -> { - val kinds = searchBook.getKindList() - if (kinds.isEmpty()) { - ll_kind.gone() - } else { - ll_kind.visible() - for (index in 0..2) { - if (kinds.size > index) { - when (index) { - 0 -> { - tv_kind.text = kinds[index] - tv_kind.visible() - } - 1 -> { - tv_kind_1.text = kinds[index] - tv_kind_1.visible() - } - 2 -> { - tv_kind_2.text = kinds[index] - tv_kind_2.visible() - } - } - } else { - when (index) { - 0 -> tv_kind.gone() - 1 -> tv_kind_1.gone() - 2 -> tv_kind_2.gone() - } - } - } - } - } - 4 -> { - if (searchBook.latestChapterTitle.isNullOrEmpty()) { - tv_lasted.gone() - } else { - tv_lasted.text = context.getString( - R.string.lasted_show, - searchBook.latestChapterTitle - ) - tv_lasted.visible() - } - } - 5 -> tv_introduce.text = - context.getString(R.string.intro_show, searchBook.intro) - else -> { - } + if (latestChapterTitle.isNullOrEmpty()) { + tv_lasted.gone() + } else { + tv_lasted.text = + context.getString( + R.string.lasted_show, + latestChapterTitle + ) + tv_lasted.visible() } } } + private fun upKind(itemView: View, kinds: List) = with(itemView) { + if (kinds.isEmpty()) { + ll_kind.gone() + } else { + ll_kind.visible() + ll_kind.setLabels(kinds) + } + } + interface CallBack { fun showBookInfo(name: String, author: String) } diff --git a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt index 1a695cccd..df104de88 100644 --- a/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/book/search/SearchViewModel.kt @@ -1,29 +1,35 @@ package io.legado.app.ui.book.search import android.app.Application +import androidx.lifecycle.MutableLiveData import io.legado.app.App import io.legado.app.base.BaseViewModel import io.legado.app.constant.PreferKey import io.legado.app.data.entities.SearchBook import io.legado.app.data.entities.SearchKeyword +import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook import io.legado.app.utils.getPrefBoolean import io.legado.app.utils.getPrefString -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.launch import java.util.concurrent.Executors class SearchViewModel(application: Application) : BaseViewModel(application) { - private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() + private var searchPool = + Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() private var task: Coroutine<*>? = null - var callBack: CallBack? = null + var isSearchLiveData = MutableLiveData() + var searchBookLiveData = MutableLiveData>() var searchKey: String = "" var searchPage = 1 var isLoading = false - private var searchBooks = arrayListOf() + var searchBooks = arrayListOf() + /** + * 开始搜索 + */ fun search(key: String) { task?.cancel() if (key.isEmpty() && searchKey.isEmpty()) { @@ -37,7 +43,7 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { searchKey = key searchBooks.clear() } - callBack?.startSearch() + isSearchLiveData.postValue(true) task = execute { val searchGroup = context.getPrefString("searchGroup") ?: "" val bookSourceList = if (searchGroup.isBlank()) { @@ -54,36 +60,44 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { context = searchPool ) .timeout(30000L) - .onSuccess(Dispatchers.IO) { + .onSuccess(IO) { it?.let { list -> - searchSuccess(list) + if (context.getPrefBoolean(PreferKey.precisionSearch)) { + precisionSearch(list) + } else { + App.db.searchBookDao().insert(*list.toTypedArray()) + mergeItems(list) + } } } } } task?.invokeOnCompletion { - callBack?.searchFinally() + isSearchLiveData.postValue(false) isLoading = false } } - private fun searchSuccess(searchBooks: List) { + /** + * 精确搜索处理 + */ + private fun precisionSearch(searchBooks: List) { val books = arrayListOf() searchBooks.forEach { searchBook -> - if (context.getPrefBoolean(PreferKey.precisionSearch)) { - if (searchBook.name.equals(searchKey, true) - || searchBook.author.equals(searchKey, true) - ) books.add(searchBook) - } else - books.add(searchBook) + if (searchBook.name.equals(searchKey, true) + || searchBook.author.equals(searchKey, true) + ) books.add(searchBook) } App.db.searchBookDao().insert(*books.toTypedArray()) - addToAdapter(books) + mergeItems(books) } + /** + * 合并搜索结果并排序 + */ @Synchronized - private fun addToAdapter(newDataS: List) { + private fun mergeItems(newDataS: List) { if (newDataS.isNotEmpty()) { val copyDataS = ArrayList(searchBooks) val searchBooksAdd = ArrayList() @@ -91,56 +105,57 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { copyDataS.addAll(newDataS) } else { //存在 - for (temp in newDataS) { + newDataS.forEach { item -> var hasSame = false - for (i in copyDataS.indices) { - val searchBook = copyDataS[i] - if (temp.name == searchBook.name - && temp.author == searchBook.author + for (searchBook in copyDataS) { + if (item.name == searchBook.name + && item.author == searchBook.author ) { hasSame = true - searchBook.addOrigin(temp.bookUrl) + searchBook.addOrigin(item.bookUrl) break } } if (!hasSame) { - searchBooksAdd.add(temp) + searchBooksAdd.add(item) } } //添加 - for (temp in searchBooksAdd) { - if (searchKey == temp.name) { - for (i in copyDataS.indices) { - val searchBook = copyDataS[i] + searchBooksAdd.forEach { item -> + if (searchKey == item.name) { + for ((index, searchBook) in copyDataS.withIndex()) { if (searchKey != searchBook.name) { - copyDataS.add(i, temp) + copyDataS.add(index, item) break } } - } else if (searchKey == temp.author) { - for (i in copyDataS.indices) { - val searchBook = copyDataS[i] + } else if (searchKey == item.author) { + for ((i, searchBook) in copyDataS.withIndex()) { if (searchKey != searchBook.name && searchKey == searchBook.author) { - copyDataS.add(i, temp) + copyDataS.add(i, item) break } } } else { - copyDataS.add(temp) + copyDataS.add(item) } } } - launch { - searchBooks = copyDataS - callBack?.adapter?.setItems(searchBooks) - } + searchBooks = copyDataS + searchBookLiveData.postValue(copyDataS) } } + /** + * 停止搜索 + */ fun stop() { task?.cancel() } + /** + * 按书名和作者获取书源排序最前的搜索结果 + */ fun getSearchBook(name: String, author: String, success: ((searchBook: SearchBook?) -> Unit)?) { execute { val searchBook = App.db.searchBookDao().getFirstByNameAuthor(name, author) @@ -148,6 +163,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { } } + /** + * 保存搜索关键字 + */ fun saveSearchKey(key: String) { execute { App.db.searchKeywordDao().get(key)?.let { @@ -157,6 +175,9 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { } } + /** + * 清楚搜索关键字 + */ fun clearHistory() { execute { App.db.searchKeywordDao().deleteAll() @@ -168,9 +189,4 @@ class SearchViewModel(application: Application) : BaseViewModel(application) { searchPool.close() } - interface CallBack { - var adapter: SearchAdapter - fun startSearch() - fun searchFinally() - } } diff --git a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt index 7fd07fbda..718b0a231 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/debug/BookSourceDebugAdapter.kt @@ -26,4 +26,8 @@ class BookSourceDebugAdapter(context: Context) : text_view.text = item } } + + override fun registerListener(holder: ItemViewHolder) { + //nothing + } } \ No newline at end of file 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 cc63c93d4..f693c07ed 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 @@ -20,7 +20,6 @@ import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst import io.legado.app.data.entities.BookSource -import io.legado.app.data.entities.EditEntity import io.legado.app.data.entities.rule.* import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.ATH @@ -354,18 +353,16 @@ class BookSourceEditActivity : } private fun showKeyboardTopPopupWindow() { - mSoftKeyboardTool?.isShowing?.let { if (it) return } - if (!isFinishing) { - mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + mSoftKeyboardTool?.let { + if (it.isShowing) return + if (!isFinishing) { + it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + } } } private fun closePopupWindow() { - mSoftKeyboardTool?.let { - if (it.isShowing) { - it.dismiss() - } - } + mSoftKeyboardTool?.dismiss() } private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt index 37939e803..163d66584 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditAdapter.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.legado.app.R -import io.legado.app.data.entities.EditEntity import kotlinx.android.synthetic.main.item_source_edit.view.* class BookSourceEditAdapter : RecyclerView.Adapter() { diff --git a/app/src/main/java/io/legado/app/ui/book/source/edit/EditEntity.kt b/app/src/main/java/io/legado/app/ui/book/source/edit/EditEntity.kt new file mode 100644 index 000000000..fb98c3c4e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/book/source/edit/EditEntity.kt @@ -0,0 +1,3 @@ +package io.legado.app.ui.book.source.edit + +data class EditEntity(var key: String, var value: String?, var hint: Int) \ No newline at end of file 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 52b433a32..5b375b4cf 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 @@ -7,12 +7,11 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.SubMenu +import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView -import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -23,39 +22,37 @@ import io.legado.app.data.entities.BookSource import io.legado.app.help.ItemTouchCallback 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.cancelButton -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.dialogs.* import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.lib.theme.view.ATEAutoCompleteTextView -import io.legado.app.service.CheckSourceService +import io.legado.app.service.help.CheckSource import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.qrcode.QrCodeActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_book_source.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.view_search.* import org.jetbrains.anko.startActivity import org.jetbrains.anko.startActivityForResult -import org.jetbrains.anko.startService import org.jetbrains.anko.toast import java.io.FileNotFoundException class BookSourceActivity : VMBaseActivity(R.layout.activity_book_source), + PopupMenu.OnMenuItemClickListener, BookSourceAdapter.CallBack, FileChooserDialog.CallBack, SearchView.OnQueryTextListener { override val viewModel: BookSourceViewModel get() = getViewModel(BookSourceViewModel::class.java) - + private val importRecordKey = "bookSourceRecordKey" private val qrRequestCode = 101 private val importSource = 132 private lateinit var adapter: BookSourceAdapter private var bookSourceLiveDate: LiveData>? = null - private var groups = hashSetOf() + private var groups = linkedSetOf() private var groupMenu: SubMenu? = null override fun onActivityCreated(savedInstanceState: Bundle?) { @@ -64,6 +61,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity initSearchView() initLiveDataBookSource() initLiveDataGroup() + initSelectActionBar() } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -81,21 +79,9 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity when (item.itemId) { R.id.menu_add_book_source -> startActivity() R.id.menu_import_source_qr -> startActivityForResult(qrRequestCode) - R.id.menu_group_manage -> GroupManageDialog().show( - supportFragmentManager, - "groupManage" - ) + R.id.menu_group_manage -> + GroupManageDialog().show(supportFragmentManager, "groupManage") R.id.menu_import_source_local -> selectFileSys() - R.id.menu_select_all -> adapter.selectAll() - R.id.menu_revert_selection -> adapter.revertSelection() - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds()) - R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelectionIds()) - R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelectionIds()) - R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds()) - R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelectionIds()) - R.id.menu_check_source -> - startService(Pair("data", adapter.getSelectionIds())) R.id.menu_import_source_onLine -> showImportDialog() } if (item.groupId == R.id.source_group) { @@ -123,12 +109,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity private fun initRecyclerView() { ATH.applyEdgeEffectColor(recycler_view) recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration( - DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { - ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let { - this.setDrawable(it) - } - }) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) adapter = BookSourceAdapter(this, this) recycler_view.adapter = adapter val itemTouchCallback = ItemTouchCallback() @@ -153,11 +134,10 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity App.db.bookSourceDao().liveDataSearch("%$searchKey%") } bookSourceLiveDate?.observe(this, Observer { - search_view.queryHint = getString(R.string.search_book_source_num, it.size) val diffResult = DiffUtil - .calculateDiff(DiffCallBack(adapter.getItems(), it)) - adapter.setItems(it, false) - diffResult.dispatchUpdatesTo(adapter) + .calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), it)) + adapter.setItems(it, diffResult) + upCountView() }) } @@ -171,6 +151,47 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity }) } + private fun initSelectActionBar() { + select_action_bar.setMainActionText(R.string.delete) + select_action_bar.inflateMenu(R.menu.book_source_sel) + select_action_bar.setOnMenuItemClickListener(this) + select_action_bar.setCallBack(object : SelectActionBar.CallBack { + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + this@BookSourceActivity + .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.getSelection()) } + noButton { } + } + .show().applyTint() + } + }) + + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) + R.id.menu_enable_explore -> viewModel.enableSelectExplore(adapter.getSelection()) + R.id.menu_disable_explore -> viewModel.disableSelectExplore(adapter.getSelection()) + R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelection()) + R.id.menu_check_source -> CheckSource.start(this, adapter.getSelection()) + } + return true + } + private fun upGroupMenu() { groupMenu?.removeGroup(R.id.source_group) groups.map { @@ -182,17 +203,17 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity private fun showImportDialog() { val aCache = ACache.get(this, cacheDir = false) val cacheUrls: MutableList = aCache - .getAsString("sourceUrl") + .getAsString(importRecordKey) ?.splitNotBlank(",") ?.toMutableList() ?: mutableListOf() alert(titleResource = R.string.import_book_source_on_line) { - var editText: ATEAutoCompleteTextView? = null + var editText: AutoCompleteTextView? = null customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view edit_view.setFilterValues(cacheUrls) { cacheUrls.remove(it) - aCache.put("sourceUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } } } @@ -201,7 +222,7 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) - aCache.put("sourceUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() viewModel.importSource(it) { msg -> @@ -241,6 +262,10 @@ class BookSourceActivity : VMBaseActivity(R.layout.activity ) } + override fun upCountView() { + select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) + } + override fun onFilePicked(requestCode: Int, currentPath: String) { if (requestCode == importSource) { Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt index c7c432e16..5d5872638 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/BookSourceAdapter.kt @@ -1,74 +1,65 @@ package io.legado.app.ui.book.source.manage import android.content.Context -import android.view.Menu +import android.graphics.Color +import android.os.Bundle +import android.view.View +import android.widget.ImageView import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.BookSource import io.legado.app.help.ItemTouchCallback.OnItemTouchCallbackListener import io.legado.app.lib.theme.backgroundColor +import io.legado.app.utils.invisible +import io.legado.app.utils.visible import kotlinx.android.synthetic.main.item_book_source.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* class BookSourceAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_book_source), OnItemTouchCallbackListener { - private val selectedIds = linkedSetOf() + private val selected = linkedSetOf() fun selectAll() { getItems().forEach { - selectedIds.add(it.bookSourceUrl) + selected.add(it) } - notifyItemRangeChanged(0, itemCount, 1) + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() } fun revertSelection() { getItems().forEach { - if (selectedIds.contains(it.bookSourceUrl)) { - selectedIds.remove(it.bookSourceUrl) + if (selected.contains(it)) { + selected.remove(it) } else { - selectedIds.add(it.bookSourceUrl) + selected.add(it) } } - notifyItemRangeChanged(0, itemCount, 1) + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() } - fun getSelectionIds(): LinkedHashSet { - val selection = linkedSetOf() + fun getSelection(): LinkedHashSet { + val selection = linkedSetOf() getItems().map { - if (selectedIds.contains(it.bookSourceUrl)) { - selection.add(it.bookSourceUrl) + if (selected.contains(it)) { + selection.add(it) } } return selection } - override fun onSwiped(adapterPosition: Int) { - - } - - override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { - val srcItem = getItem(srcPosition) - val targetItem = getItem(targetPosition) - if (srcItem != null && targetItem != null) { - if (srcItem.customOrder == targetItem.customOrder) { - callBack.upOrder() - } else { - val srcOrder = srcItem.customOrder - srcItem.customOrder = targetItem.customOrder - targetItem.customOrder = srcOrder - callBack.update(srcItem, targetItem) - } - } - return true - } - override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList) { with(holder.itemView) { - if (payloads.isEmpty()) { + val payload = payloads.getOrNull(0) as? Bundle + if (payload == null) { this.setBackgroundColor(context.backgroundColor) if (item.bookSourceGroup.isNullOrEmpty()) { cb_book_source.text = item.bookSourceName @@ -77,38 +68,127 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : String.format("%s (%s)", item.bookSourceName, item.bookSourceGroup) } swt_enabled.isChecked = item.enabled - swt_enabled.onClick { - item.enabled = swt_enabled.isChecked - callBack.update(item) + cb_book_source.isChecked = selected.contains(item) + upShowExplore(iv_explore, item) + } else { + payload.keySet().map { + when (it) { + "selected" -> cb_book_source.isChecked = selected.contains(item) + "name", "group" -> if (item.bookSourceGroup.isNullOrEmpty()) { + cb_book_source.text = item.bookSourceName + } else { + cb_book_source.text = + String.format("%s (%s)", item.bookSourceName, item.bookSourceGroup) + } + "enabled" -> swt_enabled.isChecked = payload.getBoolean(it) + "showExplore" -> upShowExplore(iv_explore, item) + } } - cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl) - cb_book_source.setOnClickListener { - if (cb_book_source.isChecked) { - selectedIds.add(item.bookSourceUrl) - } else { - selectedIds.remove(item.bookSourceUrl) + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + swt_enabled.setOnCheckedChangeListener { view, checked -> + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + it.enabled = checked + callBack.update(it) } } - iv_edit.onClick { callBack.edit(item) } - iv_menu_more.onClick { - val popupMenu = PopupMenu(context, it) - popupMenu.menu.add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top) - popupMenu.menu.add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete) - popupMenu.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.menu_top -> callBack.toTop(item) - R.id.menu_del -> callBack.del(item) + } + cb_book_source.setOnCheckedChangeListener { view, checked -> + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + if (checked) { + selected.add(it) + } else { + selected.remove(it) } - true + callBack.upCountView() } - popupMenu.show() } + } + iv_edit.onClick { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) + } + } + iv_menu_more.onClick { + showMenu(iv_menu_more, holder.layoutPosition) + } + } + } + + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.book_source_item) + val qyMenu = popupMenu.menu.findItem(R.id.menu_enable_explore) + if (source.exploreUrl.isNullOrEmpty()) { + qyMenu.isVisible = false + } else { + if (source.enabledExplore) { + qyMenu.setTitle(R.string.disable_explore) } else { - when (payloads[0]) { - 1 -> cb_book_source.isChecked = selectedIds.contains(item.bookSourceUrl) - 2 -> swt_enabled.isChecked = item.enabled + qyMenu.setTitle(R.string.enable_explore) + } + } + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(source) + R.id.menu_del -> callBack.del(source) + R.id.menu_enable_explore -> { + callBack.update(source.copy(enabledExplore = !source.enabledExplore)) } } + true + } + popupMenu.show() + } + + private fun upShowExplore(iv: ImageView, source: BookSource) { + when { + source.exploreUrl.isNullOrEmpty() -> { + iv.invisible() + } + source.enabledExplore -> { + iv.setColorFilter(Color.GREEN) + iv.visible() + } + else -> { + iv.setColorFilter(Color.RED) + iv.visible() + } + } + } + + override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { + val srcItem = getItem(srcPosition) + val targetItem = getItem(targetPosition) + if (srcItem != null && targetItem != null) { + if (srcItem.customOrder == targetItem.customOrder) { + callBack.upOrder() + } else { + val srcOrder = srcItem.customOrder + srcItem.customOrder = targetItem.customOrder + targetItem.customOrder = srcOrder + movedItems.add(srcItem) + movedItems.add(targetItem) + } + } + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) + return true + } + + private val movedItems = hashSetOf() + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() } } @@ -118,5 +198,6 @@ class BookSourceAdapter(context: Context, val callBack: CallBack) : fun update(vararg bookSource: BookSource) fun toTop(bookSource: BookSource) fun upOrder() + fun upCountView() } } \ No newline at end of file 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 6ae3bd977..ec5c50020 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 @@ -5,9 +5,7 @@ import android.text.TextUtils import com.jayway.jsonpath.JsonPath import io.legado.app.App import io.legado.app.base.BaseViewModel -import io.legado.app.data.api.IHttpGetApi import io.legado.app.data.entities.BookSource -import io.legado.app.help.FileHelp import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.Backup import io.legado.app.help.storage.OldRule @@ -45,56 +43,58 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } } - fun enableSelection(ids: LinkedHashSet) { + fun enableSelection(sources: LinkedHashSet) { execute { - ids.forEach { - App.db.bookSourceDao().enableSection(it) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabled = true)) } + App.db.bookSourceDao().update(*list.toTypedArray()) } } - fun disableSelection(ids: LinkedHashSet) { + fun disableSelection(sources: LinkedHashSet) { execute { - ids.forEach { - App.db.bookSourceDao().disableSection(it) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabled = false)) } + App.db.bookSourceDao().update(*list.toTypedArray()) } } - fun enableSelectExplore(ids: LinkedHashSet) { + fun enableSelectExplore(sources: LinkedHashSet) { execute { - ids.forEach { - App.db.bookSourceDao().enableSectionExplore(it) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabledExplore = true)) } + App.db.bookSourceDao().update(*list.toTypedArray()) } } - fun disableSelectExplore(ids: LinkedHashSet) { + fun disableSelectExplore(sources: LinkedHashSet) { execute { - ids.forEach { - App.db.bookSourceDao().disableSectionExplore(it) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabledExplore = false)) } + App.db.bookSourceDao().update(*list.toTypedArray()) } } - fun delSelection(ids: LinkedHashSet) { + fun delSelection(sources: LinkedHashSet) { execute { - ids.forEach { - App.db.bookSourceDao().delSection(it) - } + App.db.bookSourceDao().delete(*sources.toTypedArray()) } } - fun exportSelection(ids: LinkedHashSet) { + fun exportSelection(sources: LinkedHashSet) { execute { - ids.map { - App.db.bookSourceDao().getBookSource(it) - }.let { - val json = GSON.toJson(it) - val file = - FileHelp.getFile(Backup.exportPath + File.separator + "exportBookSource.json") - file.writeText(json) - } + val json = GSON.toJson(sources) + val file = + FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportBookSource.json") + file.writeText(json) }.onSuccess { context.toast("成功导出至\n${Backup.exportPath}") }.onError { @@ -203,20 +203,17 @@ class BookSourceViewModel(application: Application) : BaseViewModel(application) } private fun importSourceUrl(url: String): Int { - NetworkUtils.getBaseUrl(url)?.let { - val response = HttpHelper.getApiService(it).get(url, mapOf()).execute() - response.body()?.let { body -> - val bookSources = mutableListOf() - val items: List> = jsonPath.parse(body).read("$") - for (item in items) { - val jsonItem = jsonPath.parse(item) - OldRule.jsonToBookSource(jsonItem.jsonString())?.let { source -> - bookSources.add(source) - } + HttpHelper.simpleGet(url)?.let { body -> + val bookSources = mutableListOf() + val items: List> = jsonPath.parse(body).read("$") + for (item in items) { + val jsonItem = jsonPath.parse(item) + OldRule.jsonToBookSource(jsonItem.jsonString())?.let { source -> + bookSources.add(source) } - App.db.bookSourceDao().insert(*bookSources.toTypedArray()) - return bookSources.size } + App.db.bookSourceDao().insert(*bookSources.toTypedArray()) + return bookSources.size } return 0 } diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt index 0e8d9bead..8810d2dd9 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/DiffCallBack.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.book.source.manage +import android.os.Bundle import androidx.recyclerview.widget.DiffUtil import io.legado.app.data.entities.BookSource @@ -25,19 +26,41 @@ class DiffCallBack( override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return oldItem.bookSourceName == newItem.bookSourceName - && oldItem.bookSourceGroup == newItem.bookSourceGroup - && oldItem.enabled == newItem.enabled + if (oldItem.bookSourceName != newItem.bookSourceName) + return false + if (oldItem.bookSourceGroup != newItem.bookSourceGroup) + return false + if (oldItem.enabled != newItem.enabled) + return false + if (oldItem.enabledExplore != newItem.enabledExplore + || oldItem.exploreUrl != newItem.exploreUrl + ) { + return false + } + return true } override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return when { - oldItem.bookSourceName == newItem.bookSourceName - && oldItem.bookSourceGroup == newItem.bookSourceGroup - && oldItem.enabled != newItem.enabled -> 2 - else -> null + val payload = Bundle() + if (oldItem.bookSourceName != newItem.bookSourceName) { + payload.putString("name", newItem.bookSourceName) + } + if (oldItem.bookSourceGroup != newItem.bookSourceGroup) { + payload.putString("group", newItem.bookSourceGroup) + } + if (oldItem.enabled != newItem.enabled) { + payload.putBoolean("enabled", newItem.enabled) + } + if (oldItem.enabledExplore != newItem.enabledExplore + || oldItem.exploreUrl != newItem.exploreUrl + ) { + payload.putBoolean("showExplore", true) + } + if (payload.isEmpty) { + return null } + return payload } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt index 068f6f56f..314b45d08 100644 --- a/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/book/source/manage/GroupManageDialog.kt @@ -12,9 +12,7 @@ import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration 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.adapter.ItemViewHolder @@ -24,10 +22,7 @@ import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.requestInputMethod -import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.item_group_manage.view.* @@ -65,9 +60,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter App.db.bookSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { val groups = linkedSetOf() @@ -132,8 +125,19 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { with(holder.itemView) { tv_group.text = item - tv_edit.onClick { editGroup(item) } - tv_del.onClick { viewModel.delGroup(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + tv_edit.onClick { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + tv_del.onClick { + getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } + } } } } diff --git a/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverDialog.kt b/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverDialog.kt index f566cbbbb..d1c081f76 100644 --- a/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverDialog.kt +++ b/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverDialog.kt @@ -7,13 +7,16 @@ import android.view.View import android.view.ViewGroup import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager +import androidx.lifecycle.Observer import androidx.recyclerview.widget.GridLayoutManager import io.legado.app.R import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.dialog_change_source.* -class ChangeCoverDialog : DialogFragment() { +class ChangeCoverDialog : DialogFragment(), + ChangeCoverViewModel.CallBack, + CoverAdapter.CallBack { companion object { const val tag = "changeCoverDialog" @@ -32,7 +35,7 @@ class ChangeCoverDialog : DialogFragment() { private var callBack: CallBack? = null private lateinit var viewModel: ChangeCoverViewModel - private lateinit var adapter: CoverAdapter + override lateinit var adapter: CoverAdapter override fun onStart() { super.onStart() @@ -46,13 +49,17 @@ class ChangeCoverDialog : DialogFragment() { container: ViewGroup?, savedInstanceState: Bundle? ): View? { + callBack = activity as? CallBack viewModel = getViewModel(ChangeCoverViewModel::class.java) + viewModel.callBack = this return inflater.inflate(R.layout.dialog_change_source, container) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - callBack = activity as? CallBack + viewModel.searchStateData.observe(this, Observer { + refresh_progress_bar.isAutoLoading = it + }) tool_bar.setTitle(R.string.change_cover_source) arguments?.let { bundle -> bundle.getString("name")?.let { @@ -63,8 +70,14 @@ class ChangeCoverDialog : DialogFragment() { } } recycler_view.layoutManager = GridLayoutManager(requireContext(), 3) - adapter = CoverAdapter(requireContext()) + adapter = CoverAdapter(requireContext(), this) recycler_view.adapter = adapter + viewModel.initData() + } + + override fun changeTo(coverUrl: String) { + callBack?.coverChangeTo(coverUrl) + dismiss() } interface CallBack { diff --git a/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverViewModel.kt b/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverViewModel.kt index 5f12c56b2..d8ed01d9f 100644 --- a/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/changecover/ChangeCoverViewModel.kt @@ -1,12 +1,77 @@ package io.legado.app.ui.changecover import android.app.Application +import androidx.lifecycle.MutableLiveData +import io.legado.app.App import io.legado.app.base.BaseViewModel +import io.legado.app.help.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.model.WebBook +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.asCoroutineDispatcher +import kotlinx.coroutines.withContext +import java.util.concurrent.Executors class ChangeCoverViewModel(application: Application) : BaseViewModel(application) { - + private var searchPool = + Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() + var callBack: CallBack? = null var name: String = "" var author: String = "" + private var task: Coroutine<*>? = null + val searchStateData = MutableLiveData() + + fun initData() { + execute { + App.db.searchBookDao().getEnableHasCover(name, author) + }.onSuccess { + it?.let { + callBack?.adapter?.setItems(it) + } + }.onFinally { + search() + } + } + + fun search() { + task = execute { + searchStateData.postValue(true) + val bookSourceList = App.db.bookSourceDao().allEnabled + for (item in bookSourceList) { + //task取消时自动取消 by (scope = this@execute) + WebBook(item).searchBook(name, scope = this@execute, context = searchPool) + .timeout(30000L) + .onSuccess(Dispatchers.IO) { + if (it != null && it.isNotEmpty()) { + val searchBook = it[0] + if (searchBook.name == name && searchBook.author == author + && !searchBook.coverUrl.isNullOrEmpty() + ) { + App.db.searchBookDao().insert(searchBook) + callBack?.adapter?.let { adapter -> + if (!adapter.getItems().contains(searchBook)) { + withContext(Dispatchers.Main) { + adapter.addItem(searchBook) + } + } + } + } + } + } + } + } + + task?.invokeOnCompletion { + searchStateData.postValue(false) + } + } + override fun onCleared() { + super.onCleared() + searchPool.close() + } + interface CallBack { + var adapter: CoverAdapter + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/changecover/CoverAdapter.kt b/app/src/main/java/io/legado/app/ui/changecover/CoverAdapter.kt index 408acbeb4..eae791b2c 100644 --- a/app/src/main/java/io/legado/app/ui/changecover/CoverAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/changecover/CoverAdapter.kt @@ -5,21 +5,30 @@ import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.SearchBook -import io.legado.app.help.ImageLoader import kotlinx.android.synthetic.main.item_cover.view.* +import org.jetbrains.anko.sdk27.listeners.onClick -class CoverAdapter(context: Context) : +class CoverAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_cover) { override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { with(holder.itemView) { - item.coverUrl?.let { - ImageLoader.load(context, it) - .centerCrop() - .into(iv_cover) - } + iv_cover.load(item.coverUrl, item.name, item.author) tv_source.text = item.originName } } + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack.changeTo(it.coverUrl ?: "") + } + } + } + } + + interface CallBack { + fun changeTo(coverUrl: String) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceAdapter.kt index b9292cace..1fa9284c1 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceAdapter.kt @@ -1,6 +1,7 @@ package io.legado.app.ui.changesource import android.content.Context +import android.os.Bundle import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -15,19 +16,31 @@ class ChangeSourceAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_change_source) { override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + val bundle = payloads.getOrNull(0) as? Bundle holder.itemView.apply { - if (payloads.isEmpty()) { - this.onClick { callBack.changeTo(item) } + if (bundle == null) { tv_origin.text = item.originName - tv_last.text = item.latestChapterTitle + tv_last.text = item.getDisplayLastChapterTitle() if (callBack.bookUrl == item.bookUrl) { iv_checked.visible() } else { iv_checked.invisible() } } else { - tv_origin.text = item.originName - tv_last.text = item.latestChapterTitle + bundle.keySet().map { + when (it) { + "name" -> tv_origin.text = item.originName + "latest" -> tv_last.text = item.getDisplayLastChapterTitle() + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.changeTo(it) } } } diff --git a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt index edf3057a3..4b129acea 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceDialog.kt @@ -3,24 +3,30 @@ package io.legado.app.ui.changesource import android.os.Bundle import android.util.DisplayMetrics import android.view.LayoutInflater +import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout import androidx.appcompat.widget.SearchView +import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.LinearLayoutManager +import androidx.recyclerview.widget.RecyclerView import io.legado.app.R +import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook +import io.legado.app.utils.getPrefBoolean +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModel +import io.legado.app.utils.putPrefBoolean import kotlinx.android.synthetic.main.dialog_change_source.* class ChangeSourceDialog : DialogFragment(), - ChangeSourceViewModel.CallBack, + Toolbar.OnMenuItemClickListener, ChangeSourceAdapter.CallBack { companion object { @@ -40,7 +46,7 @@ class ChangeSourceDialog : DialogFragment(), private var callBack: CallBack? = null private lateinit var viewModel: ChangeSourceViewModel - private lateinit var changeSourceAdapter: ChangeSourceAdapter + lateinit var adapter: ChangeSourceAdapter override fun onStart() { super.onStart() @@ -54,29 +60,22 @@ class ChangeSourceDialog : DialogFragment(), container: ViewGroup?, savedInstanceState: Bundle? ): View? { + callBack = activity as? CallBack viewModel = getViewModel(ChangeSourceViewModel::class.java) return inflater.inflate(R.layout.dialog_change_source, container) } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - callBack = activity as? CallBack - viewModel.searchStateData.observe(viewLifecycleOwner, Observer { - refresh_progress_bar.isAutoLoading = it - }) - arguments?.let { bundle -> - bundle.getString("name")?.let { - viewModel.name = it - } - bundle.getString("author")?.let { - viewModel.author = it - } - } - tool_bar.inflateMenu(R.menu.search_view) + viewModel.initData(arguments) showTitle() + tool_bar.inflateMenu(R.menu.change_source) + tool_bar.setOnMenuItemClickListener(this) initRecyclerView() + initMenu() initSearchView() - viewModel.initData() + initLiveData() + viewModel.loadDbSearchBook() viewModel.search() } @@ -85,14 +84,29 @@ class ChangeSourceDialog : DialogFragment(), tool_bar.subtitle = getString(R.string.author_show, viewModel.author) } + private fun initMenu() { + tool_bar.menu.findItem(R.id.menu_load_toc)?.isChecked = + getPrefBoolean(PreferKey.changeSourceLoadToc) + } + private fun initRecyclerView() { - changeSourceAdapter = ChangeSourceAdapter(requireContext(), this) + adapter = ChangeSourceAdapter(requireContext(), this) recycler_view.layoutManager = LinearLayoutManager(context) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), LinearLayout.VERTICAL) - ) - recycler_view.adapter = changeSourceAdapter - viewModel.callBack = this + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) + recycler_view.adapter = adapter + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + if (positionStart == 0) { + recycler_view.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + if (toPosition == 0) { + recycler_view.scrollToPosition(0) + } + } + }) } private fun initSearchView() { @@ -118,6 +132,27 @@ class ChangeSourceDialog : DialogFragment(), }) } + private fun initLiveData() { + viewModel.searchStateData.observe(viewLifecycleOwner, Observer { + refresh_progress_bar.isAutoLoading = it + }) + viewModel.searchBooksLiveData.observe(viewLifecycleOwner, Observer { + val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) + adapter.setItems(it) + diffResult.dispatchUpdatesTo(adapter) + }) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_load_toc -> { + putPrefBoolean(PreferKey.changeSourceLoadToc, !item.isChecked) + item.isChecked = !item.isChecked + } + } + return false + } + override fun changeTo(searchBook: SearchBook) { val book = searchBook.toBook() callBack?.oldBook?.let { oldBook -> @@ -139,10 +174,6 @@ class ChangeSourceDialog : DialogFragment(), override val bookUrl: String? get() = callBack?.oldBook?.bookUrl - override fun adapter(): ChangeSourceAdapter { - return changeSourceAdapter - } - interface CallBack { val oldBook: Book? fun changeTo(book: Book) diff --git a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt index 2a81ae398..13b145a1e 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/ChangeSourceViewModel.kt @@ -1,33 +1,46 @@ package io.legado.app.ui.changesource import android.app.Application +import android.os.Bundle import androidx.lifecycle.MutableLiveData -import androidx.recyclerview.widget.DiffUtil import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook +import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.Coroutine import io.legado.app.model.WebBook +import io.legado.app.utils.getPrefBoolean import kotlinx.coroutines.Dispatchers.IO -import kotlinx.coroutines.Dispatchers.Main import kotlinx.coroutines.asCoroutineDispatcher -import kotlinx.coroutines.withContext import org.jetbrains.anko.debug import java.util.concurrent.Executors class ChangeSourceViewModel(application: Application) : BaseViewModel(application) { - private var searchPool = Executors.newFixedThreadPool(16).asCoroutineDispatcher() - var callBack: CallBack? = null + private var searchPool = + Executors.newFixedThreadPool(AppConfig.threadCount).asCoroutineDispatcher() val searchStateData = MutableLiveData() + val searchBooksLiveData = MutableLiveData>() var name: String = "" var author: String = "" private var task: Coroutine<*>? = null private var screenKey: String = "" - private val searchBooks = linkedSetOf() + private val searchBooks = hashSetOf() - fun initData() { + fun initData(arguments: Bundle?) { + arguments?.let { bundle -> + bundle.getString("name")?.let { + name = it + } + bundle.getString("author")?.let { + author = it + } + } + } + + fun loadDbSearchBook() { execute { App.db.searchBookDao().getByNameAuthorEnable(name, author).let { searchBooks.addAll(it) @@ -37,19 +50,18 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio } private fun upAdapter() { - execute { - callBack?.adapter()?.let { - val books = searchBooks.toList() - books.sorted() - val diffResult = DiffUtil.calculateDiff(DiffCallBack(it.getItems(), books)) - withContext(Main) { - synchronized(this) { - it.setItems(books, false) - diffResult.dispatchUpdatesTo(it) - } - } - } + val books = searchBooks.toList() + searchBooksLiveData.postValue(books.sortedBy { it.originOrder }) + } + + private fun searchFinish(searchBook: SearchBook) { + App.db.searchBookDao().insert(searchBook) + if (screenKey.isEmpty()) { + searchBooks.add(searchBook) + } else if (searchBook.originName.contains(screenKey)) { + searchBooks.add(searchBook) } + upAdapter() } fun search() { @@ -63,10 +75,14 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio .onSuccess(IO) { it?.forEach { searchBook -> if (searchBook.name == name && searchBook.author == author) { - if (searchBook.tocUrl.isEmpty()) { - loadBookInfo(searchBook.toBook()) + if (context.getPrefBoolean(PreferKey.changeSourceLoadToc)) { + if (searchBook.tocUrl.isEmpty()) { + loadBookInfo(searchBook.toBook()) + } else { + loadChapter(searchBook.toBook()) + } } else { - loadChapter(searchBook.toBook()) + searchFinish(searchBook) } return@onSuccess } @@ -101,10 +117,8 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio it?.let { chapters -> if (chapters.isNotEmpty()) { book.latestChapterTitle = chapters.last().title - val searchBook = book.toSearchBook() - App.db.searchBookDao().insert(searchBook) - searchBooks.add(searchBook) - upAdapter() + val searchBook: SearchBook = book.toSearchBook() + searchFinish(searchBook) } } }.onError { @@ -121,19 +135,19 @@ class ChangeSourceViewModel(application: Application) : BaseViewModel(applicatio execute { screenKey = key ?: "" if (key.isNullOrEmpty()) { - initData() + loadDbSearchBook() } else { - App.db.searchBookDao() + val items = App.db.searchBookDao().getChangeSourceSearch(name, author, screenKey) + searchBooks.clear() + searchBooks.addAll(items) + upAdapter() } } } - interface CallBack { - fun adapter(): ChangeSourceAdapter - } - override fun onCleared() { super.onCleared() searchPool.close() } + } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/changesource/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/changesource/DiffCallBack.kt index 2cb911204..91e640ac3 100644 --- a/app/src/main/java/io/legado/app/ui/changesource/DiffCallBack.kt +++ b/app/src/main/java/io/legado/app/ui/changesource/DiffCallBack.kt @@ -1,16 +1,12 @@ package io.legado.app.ui.changesource +import android.os.Bundle import androidx.recyclerview.widget.DiffUtil import io.legado.app.data.entities.SearchBook class DiffCallBack(private val oldItems: List, private val newItems: List) : DiffUtil.Callback() { - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition].bookUrl == newItems[newItemPosition].bookUrl - } - override fun getOldListSize(): Int { return oldItems.size } @@ -19,18 +15,38 @@ class DiffCallBack(private val oldItems: List, private val newItems: return newItems.size } + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + return oldItem.bookUrl == newItem.bookUrl + } + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition].originName == newItems[newItemPosition].originName - && oldItems[oldItemPosition].latestChapterTitle == newItems[newItemPosition].latestChapterTitle + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + if (oldItem.originName != newItem.originName) { + return false + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + return false + } + return true } override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - if (oldItem.originName != newItem.originName || oldItem.latestChapterTitle != newItem.latestChapterTitle) { - return true + val payload = Bundle() + if (oldItem.originName != newItem.originName) { + payload.putString("name", newItem.originName) + } + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) { + payload.putString("latest", newItem.latestChapterTitle) + } + if (payload.isEmpty) { + return null } - return null + return payload } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt b/app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt index 803fe2e11..c32c8bc42 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/BookmarkFragment.kt @@ -4,18 +4,17 @@ import android.app.Activity import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.LinearLayout import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.paging.LivePagedListBuilder import androidx.paging.PagedList -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment import io.legado.app.data.entities.Bookmark import io.legado.app.lib.theme.ATH +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModelOfActivity import kotlinx.android.synthetic.main.fragment_bookmark.* @@ -29,8 +28,7 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ private lateinit var adapter: BookmarkAdapter private var bookmarkLiveData: LiveData>? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { viewModel.bookMarkCallBack = this initRecyclerView() initData() @@ -40,18 +38,14 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ ATH.applyEdgeEffectColor(recycler_view) adapter = BookmarkAdapter(this) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration( - requireContext(), - LinearLayout.VERTICAL - ) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter } private fun initData() { bookmarkLiveData?.removeObservers(viewLifecycleOwner) - bookmarkLiveData = LivePagedListBuilder(App.db.bookmarkDao().observeByBook(viewModel.bookUrl ?: ""), 20).build() + bookmarkLiveData = + LivePagedListBuilder(App.db.bookmarkDao().observeByBook(viewModel.bookUrl), 20).build() bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) } @@ -60,7 +54,12 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ initData() } else { bookmarkLiveData?.removeObservers(viewLifecycleOwner) - bookmarkLiveData = LivePagedListBuilder(App.db.bookmarkDao().liveDataSearch(viewModel.bookUrl ?: "", newText), 20).build() + bookmarkLiveData = LivePagedListBuilder( + App.db.bookmarkDao().liveDataSearch( + viewModel.bookUrl, + newText + ), 20 + ).build() bookmarkLiveData?.observe(viewLifecycleOwner, Observer { adapter.submitList(it) }) } } @@ -74,8 +73,6 @@ class BookmarkFragment : VMBaseFragment(R.layout.fragment_ } override fun delBookmark(bookmark: Bookmark) { - bookmark?.let { - App.db.bookmarkDao().delByBookmark(it.bookUrl, it.chapterName) - } + App.db.bookmarkDao().delByBookmark(bookmark.bookUrl, bookmark.chapterName) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListActivity.kt b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListActivity.kt index 920b0dd24..a0d401a5e 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListActivity.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListActivity.kt @@ -28,11 +28,9 @@ class ChapterListActivity : VMBaseActivity(R.layout.activi override fun onActivityCreated(savedInstanceState: Bundle?) { tab_layout.isTabIndicatorFullWidth = false tab_layout.setSelectedTabIndicatorColor(accentColor) - viewModel.bookUrl = intent.getStringExtra("bookUrl") - viewModel.loadBook { - view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager) - tab_layout.setupWithViewPager(view_pager) - } + viewModel.bookUrl = intent.getStringExtra("bookUrl") ?: "" + view_pager.adapter = TabFragmentPageAdapter(supportFragmentManager) + tab_layout.setupWithViewPager(view_pager) } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListAdapter.kt b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListAdapter.kt index 05b70e1d3..08b222970 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListAdapter.kt @@ -1,10 +1,10 @@ package io.legado.app.ui.chapterlist import android.content.Context +import android.widget.TextView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -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.accentColor @@ -17,29 +17,47 @@ import org.jetbrains.anko.sdk27.listeners.onClick class ChapterListAdapter(context: Context, val callback: Callback) : SimpleRecyclerAdapter(context, R.layout.item_chapter_list) { + val cacheFileNames = hashSetOf() + override fun convert(holder: ItemViewHolder, item: BookChapter, payloads: MutableList) { with(holder.itemView) { - if (callback.durChapterIndex() == item.index) { - tv_chapter_name.setTextColor(context.accentColor) + if (payloads.isEmpty()) { + if (callback.durChapterIndex() == item.index) { + tv_chapter_name.setTextColor(context.accentColor) + } else { + tv_chapter_name.setTextColor(context.getCompatColor(R.color.tv_text_default)) + } + tv_chapter_name.text = item.title + if (!item.tag.isNullOrEmpty()) { + tv_tag.text = item.tag + tv_tag.visible() + } + upHasCache( + tv_chapter_name, + cacheFileNames.contains(BookHelp.formatChapterName(item)) + ) } else { - tv_chapter_name.setTextColor(context.getCompatColor(R.color.tv_text_default)) - } - tv_chapter_name.text = item.title - if (!item.tag.isNullOrEmpty()) { - tv_tag.text = item.tag - tv_tag.visible() + upHasCache( + tv_chapter_name, + cacheFileNames.contains(BookHelp.formatChapterName(item)) + ) } - this.onClick { - callback.openChapter(item) - } - callback.book()?.let { - tv_chapter_name.paint.isFakeBoldText = BookHelp.hasContent(it, item) + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callback.openChapter(it) } } } + private fun upHasCache(textView: TextView, contains: Boolean) { + textView.paint.isFakeBoldText = contains + } + interface Callback { - fun book(): Book? fun openChapter(bookChapter: BookChapter) fun durChapterIndex(): Int } diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt index 22092493a..0d9234e36 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListFragment.kt @@ -4,18 +4,25 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.os.Bundle import android.view.View -import android.widget.LinearLayout +import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment +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.ui.widget.recycler.UpLinearLayoutManager +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModelOfActivity +import io.legado.app.utils.observeEvent import kotlinx.android.synthetic.main.fragment_chapter_list.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import org.jetbrains.anko.sdk27.listeners.onClick class ChapterListFragment : VMBaseFragment(R.layout.fragment_chapter_list), @@ -25,43 +32,26 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme get() = getViewModelOfActivity(ChapterListViewModel::class.java) lateinit var adapter: ChapterListAdapter + private var book: Book? = null private var durChapterIndex = 0 private lateinit var mLayoutManager: UpLinearLayoutManager + private var tocLiveData: LiveData>? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { viewModel.chapterCallBack = this initRecyclerView() initView() - initData() + initBook() } private fun initRecyclerView() { adapter = ChapterListAdapter(requireContext(), this) mLayoutManager = UpLinearLayoutManager(requireContext()) recycler_view.layoutManager = mLayoutManager - recycler_view.addItemDecoration( - DividerItemDecoration( - requireContext(), - LinearLayout.VERTICAL - ) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter } - private fun initData() { - viewModel.bookUrl?.let { bookUrl -> - App.db.bookChapterDao().observeByBook(bookUrl).observe(viewLifecycleOwner, Observer { - adapter.setItems(it) - viewModel.book?.let { book -> - durChapterIndex = book.durChapterIndex - tv_current_chapter_info.text = it[durChapterIndex()].title - mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) - } - }) - } - } - private fun initView() { ll_chapter_base_info.setBackgroundColor(backgroundColor) iv_chapter_top.onClick { mLayoutManager.scrollToPositionWithOffset(0, 0) } @@ -71,19 +61,62 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme } } tv_current_chapter_info.onClick { - viewModel.book?.let { - mLayoutManager.scrollToPositionWithOffset(it.durChapterIndex, 0) + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + } + } + + private fun initBook() { + launch { + withContext(IO) { + book = App.db.bookDao().getBook(viewModel.bookUrl) + } + initDoc() + book?.let { + durChapterIndex = it.durChapterIndex + tv_current_chapter_info.text = it.durChapterTitle + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + initCacheFileNames(it) + } + } + } + + private fun initDoc() { + tocLiveData?.removeObservers(this@ChapterListFragment) + tocLiveData = App.db.bookChapterDao().observeByBook(viewModel.bookUrl) + tocLiveData?.observe(viewLifecycleOwner, Observer { + adapter.setItems(it) + mLayoutManager.scrollToPositionWithOffset(durChapterIndex, 0) + }) + } + + private fun initCacheFileNames(book: Book) { + launch(IO) { + adapter.cacheFileNames.addAll(BookHelp.getChapterFiles(book)) + withContext(Main) { + adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) + } + } + } + + override fun observeLiveBus() { + observeEvent(EventBus.SAVE_CONTENT) { chapter -> + book?.bookUrl?.let { bookUrl -> + if (chapter.bookUrl == bookUrl) { + adapter.cacheFileNames.add(BookHelp.formatChapterName(chapter)) + adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) + } } } } override fun startChapterListSearch(newText: String?) { if (newText.isNullOrBlank()) { - initData() + initDoc() } else { - App.db.bookChapterDao().liveDataSearch(viewModel.bookUrl ?: "", newText).observe(viewLifecycleOwner, Observer { + tocLiveData?.removeObservers(this) + tocLiveData = App.db.bookChapterDao().liveDataSearch(viewModel.bookUrl, newText) + tocLiveData?.observe(viewLifecycleOwner, Observer { adapter.setItems(it) - mLayoutManager.scrollToPositionWithOffset(0, 0) }) } } @@ -97,7 +130,4 @@ class ChapterListFragment : VMBaseFragment(R.layout.fragme activity?.finish() } - override fun book(): Book? { - return viewModel.book - } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListViewModel.kt b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListViewModel.kt index 5ac2e68d9..4156c4ac9 100644 --- a/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/chapterlist/ChapterListViewModel.kt @@ -2,26 +2,13 @@ package io.legado.app.ui.chapterlist import android.app.Application -import io.legado.app.App import io.legado.app.base.BaseViewModel -import io.legado.app.data.entities.Book class ChapterListViewModel(application: Application) : BaseViewModel(application) { - var bookUrl: String? = null - var book: Book? = null + var bookUrl: String = "" var chapterCallBack: ChapterListCallBack? = null var bookMarkCallBack: BookmarkCallBack? = null - fun loadBook(success: () -> Unit) { - execute { - bookUrl?.let { - book = App.db.bookDao().getBook(it) - } - }.onSuccess { - success() - } - } - fun startChapterListSearch(newText: String?) { chapterCallBack?.startChapterListSearch(newText) } diff --git a/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt new file mode 100644 index 000000000..9df96e3f2 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/BackupConfigFragment.kt @@ -0,0 +1,122 @@ +package io.legado.app.ui.config + +import android.content.Intent +import android.content.SharedPreferences +import android.os.Bundle +import android.text.InputType +import android.view.View +import androidx.preference.EditTextPreference +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.lib.theme.ATH +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.filechooser.FileChooserDialog +import io.legado.app.utils.getPrefString + +class BackupConfigFragment : PreferenceFragmentCompat(), + SharedPreferences.OnSharedPreferenceChangeListener, + FileChooserDialog.CallBack { + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + addPreferencesFromResource(R.xml.pref_config_backup) + findPreference(PreferKey.webDavUrl)?.let { + it.setOnBindEditTextListener { editText -> + ATH.setTint(editText, requireContext().accentColor) + } + + } + findPreference(PreferKey.webDavAccount)?.let { + it.setOnBindEditTextListener { editText -> + ATH.setTint(editText, requireContext().accentColor) + } + } + findPreference(PreferKey.webDavPassword)?.let { + it.setOnBindEditTextListener { editText -> + ATH.setTint(editText, requireContext().accentColor) + editText.inputType = + InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT + } + } + upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl)) + upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount)) + upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword)) + upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath)) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + ATH.applyEdgeEffectColor(listView) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.webDavUrl, + PreferKey.webDavAccount, + PreferKey.webDavPassword, + PreferKey.backupPath -> { + upPreferenceSummary(key, getPrefString(key)) + } + } + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.webDavUrl -> + if (value == null) { + preference.summary = getString(R.string.web_dav_url_s) + } else { + preference.summary = value.toString() + } + PreferKey.webDavAccount -> + if (value == null) { + preference.summary = getString(R.string.web_dav_account_s) + } else { + preference.summary = value.toString() + } + PreferKey.webDavPassword -> + if (value == null) { + preference.summary = getString(R.string.web_dav_pw_s) + } else { + preference.summary = "*".repeat(value.toString().length) + } + else -> { + if (preference is ListPreference) { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } else { + preference.summary = value + } + } + } + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { + PreferKey.backupPath -> BackupRestoreUi.selectBackupFolder(this) + "web_dav_backup" -> BackupRestoreUi.backup(this) + "web_dav_restore" -> BackupRestoreUi.restore(this) + "import_old" -> BackupRestoreUi.importOldData(this) + } + return super.onPreferenceTreeClick(preference) + } + + override fun onFilePicked(requestCode: Int, currentPath: String) { + BackupRestoreUi.onFilePicked(requestCode, currentPath) + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + BackupRestoreUi.onActivityResult(requestCode, resultCode, data) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt new file mode 100644 index 000000000..16fd3941b --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/BackupRestoreUi.kt @@ -0,0 +1,274 @@ +package io.legado.app.ui.config + +import android.app.Activity.RESULT_OK +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.documentfile.provider.DocumentFile +import androidx.fragment.app.Fragment +import io.legado.app.App +import io.legado.app.R +import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig +import io.legado.app.help.coroutine.Coroutine +import io.legado.app.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat +import io.legado.app.help.storage.Backup +import io.legado.app.help.storage.ImportOldData +import io.legado.app.help.storage.Restore +import io.legado.app.help.storage.WebDavHelp +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.filechooser.FileChooserDialog +import io.legado.app.utils.getPrefString +import io.legado.app.utils.isContentPath +import io.legado.app.utils.toast +import kotlinx.coroutines.Dispatchers.Main +import org.jetbrains.anko.toast + +object BackupRestoreUi { + + private const val backupSelectRequestCode = 22 + private const val restoreSelectRequestCode = 33 + private const val oldDataRequestCode = 11 + + fun backup(fragment: Fragment) { + val backupPath = AppConfig.backupPath + if (backupPath.isNullOrEmpty()) { + selectBackupFolder(fragment) + } else { + if (backupPath.isContentPath()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) + if (doc?.canWrite() == true) { + Coroutine.async { + Backup.backup(fragment.requireContext(), backupPath) + }.onSuccess { + fragment.toast(R.string.backup_success) + } + } else { + selectBackupFolder(fragment) + } + } else { + backupUsePermission(fragment) + } + } + } + + private fun backupUsePermission(fragment: Fragment, path: String = Backup.legadoPath) { + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = Backup.legadoPath + Backup.backup(fragment.requireContext(), path) + }.onSuccess { + fragment.toast(R.string.backup_success) + } + } + .request() + } + + fun selectBackupFolder(fragment: Fragment) { + fragment.alert { + titleResource = R.string.select_folder + items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> backupUsePermission(fragment) + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + fragment.startActivityForResult(intent, backupSelectRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + fragment.toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + FileChooserDialog.show( + fragment.childFragmentManager, + backupSelectRequestCode, + mode = FileChooserDialog.DIRECTORY + ) + } + } + } + }.show() + } + + fun restore(fragment: Fragment) { + Coroutine.async(context = Main) { + if (!WebDavHelp.showRestoreDialog(fragment.requireContext()) { + fragment.toast(R.string.restore_success) + }) { + val backupPath = fragment.getPrefString(PreferKey.backupPath) + if (backupPath?.isNotEmpty() == true) { + if (backupPath.isContentPath()) { + val uri = Uri.parse(backupPath) + val doc = DocumentFile.fromTreeUri(fragment.requireContext(), uri) + if (doc?.canWrite() == true) { + Restore.restore(fragment.requireContext(), backupPath) + fragment.toast(R.string.restore_success) + } else { + selectRestoreFolder(fragment) + } + } else { + restoreUsePermission(fragment, backupPath) + } + } else { + selectRestoreFolder(fragment) + } + } + } + } + + private fun restoreUsePermission(fragment: Fragment, path: String = Backup.legadoPath) { + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + Coroutine.async { + AppConfig.backupPath = path + Restore.restore(path) + }.onSuccess { + fragment.toast(R.string.restore_success) + } + } + .request() + } + + private fun selectRestoreFolder(fragment: Fragment) { + fragment.alert { + titleResource = R.string.select_folder + items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> restoreUsePermission(fragment) + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + fragment.startActivityForResult(intent, restoreSelectRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + fragment.toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + FileChooserDialog.show( + fragment.childFragmentManager, + restoreSelectRequestCode, + mode = FileChooserDialog.DIRECTORY + ) + } + } + } + }.show() + } + + fun importOldData(fragment: Fragment) { + fragment.alert { + titleResource = R.string.select_folder + items(fragment.resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> importOldUsePermission(fragment) + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + fragment.startActivityForResult(intent, oldDataRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + fragment.toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + FileChooserDialog.show( + fragment.childFragmentManager, + oldDataRequestCode, + mode = FileChooserDialog.DIRECTORY + ) + } + .request() + } + } + } + }.show() + } + + private fun importOldUsePermission(fragment: Fragment) { + if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { + fragment.toast(R.string.a10_permission_toast) + } + PermissionsCompat.Builder(fragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + ImportOldData.import(fragment.requireContext()) + } + .request() + } + + fun onFilePicked(requestCode: Int, currentPath: String) { + when (requestCode) { + backupSelectRequestCode -> { + AppConfig.backupPath = currentPath + Coroutine.async { + Backup.backup(App.INSTANCE, currentPath) + }.onSuccess { + App.INSTANCE.toast(R.string.backup_success) + } + } + restoreSelectRequestCode -> { + AppConfig.backupPath = currentPath + Coroutine.async { + Restore.restore(App.INSTANCE, currentPath) + }.onSuccess { + App.INSTANCE.toast(R.string.restore_success) + } + } + } + } + + fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + when (requestCode) { + backupSelectRequestCode -> if (resultCode == RESULT_OK) { + data?.data?.let { uri -> + App.INSTANCE.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + AppConfig.backupPath = uri.toString() + Coroutine.async { + Backup.backup(App.INSTANCE, uri.toString()) + }.onSuccess { + App.INSTANCE.toast(R.string.backup_success) + } + } + } + restoreSelectRequestCode -> if (resultCode == RESULT_OK) { + data?.data?.let { uri -> + App.INSTANCE.contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + AppConfig.backupPath = uri.toString() + Coroutine.async { + Restore.restore(App.INSTANCE, uri.toString()) + }.onSuccess { + App.INSTANCE.toast(R.string.restore_success) + } + } + } + oldDataRequestCode -> + if (resultCode == RESULT_OK) data?.data?.let { uri -> + ImportOldData.importUri(uri) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt index 838b5c31d..a685ccd13 100644 --- a/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt +++ b/app/src/main/java/io/legado/app/ui/config/ConfigActivity.kt @@ -18,9 +18,9 @@ class ConfigActivity : VMBaseActivity(R.layout.activity_config) when (viewModel.configType) { ConfigViewModel.TYPE_CONFIG -> { title_bar.title = getString(R.string.other_setting) - val fTag = "configFragment" + val fTag = "otherConfigFragment" var configFragment = supportFragmentManager.findFragmentByTag(fTag) - if (configFragment == null) configFragment = ConfigFragment() + if (configFragment == null) configFragment = OtherConfigFragment() supportFragmentManager.beginTransaction() .replace(R.id.configFrameLayout, configFragment, fTag) .commit() @@ -36,9 +36,9 @@ class ConfigActivity : VMBaseActivity(R.layout.activity_config) } ConfigViewModel.TYPE_WEB_DAV_CONFIG -> { title_bar.title = getString(R.string.backup_restore) - val fTag = "webDavFragment" + val fTag = "backupConfigFragment" var configFragment = supportFragmentManager.findFragmentByTag(fTag) - if (configFragment == null) configFragment = WebDavConfigFragment() + if (configFragment == null) configFragment = BackupConfigFragment() supportFragmentManager.beginTransaction() .replace(R.id.configFrameLayout, configFragment, fTag) .commit() diff --git a/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt deleted file mode 100644 index 74d517b20..000000000 --- a/app/src/main/java/io/legado/app/ui/config/ConfigFragment.kt +++ /dev/null @@ -1,139 +0,0 @@ -package io.legado.app.ui.config - -import android.content.ComponentName -import android.content.SharedPreferences -import android.content.pm.PackageManager -import android.os.Bundle -import android.view.View -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import io.legado.app.App -import io.legado.app.R -import io.legado.app.constant.Bus -import io.legado.app.constant.PreferKey -import io.legado.app.help.BookHelp -import io.legado.app.lib.theme.ATH -import io.legado.app.receiver.SharedReceiverActivity -import io.legado.app.ui.filechooser.FileChooserDialog -import io.legado.app.utils.* - - -class ConfigFragment : PreferenceFragmentCompat(), - FileChooserDialog.CallBack, - Preference.OnPreferenceChangeListener, - SharedPreferences.OnSharedPreferenceChangeListener { - - private val downloadPath = 25324 - private val packageManager = App.INSTANCE.packageManager - private val componentName = ComponentName( - App.INSTANCE, - SharedReceiverActivity::class.java.name - ) - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - putPrefBoolean("process_text", isProcessTextEnabled()) - addPreferencesFromResource(R.xml.pref_config) - bindPreferenceSummaryToValue(findPreference(PreferKey.downloadPath)) - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - ATH.applyEdgeEffectColor(listView) - } - - override fun onResume() { - super.onResume() - preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) - } - - override fun onPause() { - preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) - super.onPause() - } - - override fun onPreferenceTreeClick(preference: Preference?): Boolean { - when (preference?.key) { - PreferKey.downloadPath -> FileChooserDialog.show( - childFragmentManager, - downloadPath, - mode = FileChooserDialog.DIRECTORY, - initPath = getPreferenceString(PreferKey.downloadPath) - ) - PreferKey.cleanCache -> { - BookHelp.clearCache() - toast(R.string.clear_cache_success) - } - } - return super.onPreferenceTreeClick(preference) - } - - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { - when (key) { - PreferKey.downloadPath -> { - BookHelp.upDownloadPath() - findPreference(key)?.summary = getPreferenceString(key) - } - PreferKey.recordLog -> LogUtils.upLevel() - PreferKey.processText -> sharedPreferences?.let { - setProcessTextEnable(it.getBoolean("process_text", true)) - } - PreferKey.showRss -> postEvent(Bus.SHOW_RSS, "unused") - } - } - - override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - val stringValue = newValue.toString() - - if (preference is ListPreference) { - val index = preference.findIndexOfValue(stringValue) - // Set the summary to reflect the new value. - preference.setSummary(if (index >= 0) preference.entries[index] else null) - } else { - // For all other preferences, set the summary to the value's - preference?.summary = stringValue - } - return true - } - - private fun bindPreferenceSummaryToValue(preference: Preference?) { - preference?.let { - preference.onPreferenceChangeListener = this - onPreferenceChange( - preference, - getPreferenceString(preference.key) - ) - } - } - - private fun getPreferenceString(key: String): String { - return when (key) { - PreferKey.downloadPath -> getPrefString(PreferKey.downloadPath) - ?: App.INSTANCE.getExternalFilesDir(null)?.absolutePath - ?: App.INSTANCE.cacheDir.absolutePath - else -> getPrefString(key) ?: "" - } - } - - private fun isProcessTextEnabled(): Boolean { - return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED - } - - private fun setProcessTextEnable(enable: Boolean) { - if (enable) { - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP - ) - } else { - packageManager.setComponentEnabledSetting( - componentName, - PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP - ) - } - } - - override fun onFilePicked(requestCode: Int, currentPath: String) { - putPrefString(PreferKey.downloadPath, currentPath) - } -} \ No newline at end of file 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 new file mode 100644 index 000000000..31c3d709f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/config/OtherConfigFragment.kt @@ -0,0 +1,181 @@ +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 +import android.view.View +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import io.legado.app.App +import io.legado.app.R +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig +import io.legado.app.help.BookHelp +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.theme.ATH +import io.legado.app.receiver.SharedReceiverActivity +import io.legado.app.ui.filechooser.FileChooserDialog +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, + SharedReceiverActivity::class.java.name + ) + + override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { + putPrefBoolean(PreferKey.processText, isProcessTextEnabled()) + addPreferencesFromResource(R.xml.pref_config_other) + upPreferenceSummary(PreferKey.downloadPath, BookHelp.downloadPath) + upPreferenceSummary(PreferKey.threadCount, AppConfig.threadCount.toString()) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + preferenceManager.sharedPreferences.registerOnSharedPreferenceChangeListener(this) + ATH.applyEdgeEffectColor(listView) + } + + override fun onDestroy() { + super.onDestroy() + preferenceManager.sharedPreferences.unregisterOnSharedPreferenceChangeListener(this) + } + + override fun onPreferenceTreeClick(preference: Preference?): Boolean { + when (preference?.key) { + PreferKey.threadCount -> NumberPickerDialog(requireContext()) + .setTitle(getString(R.string.threads_num_title)) + .setMaxValue(999) + .setMinValue(1) + .setValue(AppConfig.threadCount) + .show { + AppConfig.threadCount = it + } + PreferKey.downloadPath -> selectDownloadPath() + PreferKey.cleanCache -> { + BookHelp.clearCache() + toast(R.string.clear_cache_success) + } + } + return super.onPreferenceTreeClick(preference) + } + + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + PreferKey.downloadPath -> { + upPreferenceSummary(key, BookHelp.downloadPath) + } + PreferKey.threadCount -> upPreferenceSummary( + PreferKey.threadCount, + AppConfig.threadCount.toString() + ) + PreferKey.recordLog -> LogUtils.upLevel() + PreferKey.processText -> sharedPreferences?.let { + setProcessTextEnable(it.getBoolean(key, true)) + } + PreferKey.showRss -> postEvent(EventBus.SHOW_RSS, "unused") + } + } + + private fun upPreferenceSummary(preferenceKey: String, value: String?) { + val preference = findPreference(preferenceKey) ?: return + when (preferenceKey) { + PreferKey.threadCount -> preference.summary = getString(R.string.threads_num, value) + else -> if (preference is ListPreference) { + val index = preference.findIndexOfValue(value) + // Set the summary to reflect the new value. + preference.summary = if (index >= 0) preference.entries[index] else null + } else { + preference.summary = value + } + } + } + + private fun isProcessTextEnabled(): Boolean { + return packageManager.getComponentEnabledSetting(componentName) != PackageManager.COMPONENT_ENABLED_STATE_DISABLED + } + + private fun setProcessTextEnable(enable: Boolean) { + if (enable) { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_ENABLED, PackageManager.DONT_KILL_APP + ) + } else { + packageManager.setComponentEnabledSetting( + componentName, + PackageManager.COMPONENT_ENABLED_STATE_DISABLED, PackageManager.DONT_KILL_APP + ) + } + } + + private fun selectDownloadPath() { + alert { + titleResource = R.string.select_folder + items(resources.getStringArray(R.array.select_folder).toList()) { _, i -> + when (i) { + 0 -> { + removePref(PreferKey.downloadPath) + } + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivityForResult(intent, requestCodeDownloadPath) + } catch (e: Exception) { + e.printStackTrace() + toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> PermissionsCompat.Builder(this@OtherConfigFragment) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + FileChooserDialog.show( + childFragmentManager, + requestCodeDownloadPath, + mode = FileChooserDialog.DIRECTORY, + initPath = BookHelp.downloadPath + ) + } + .request() + } + } + }.show() + } + + override fun onFilePicked(requestCode: Int, currentPath: String) { + if (requestCode == requestCodeDownloadPath) { + putPrefString(PreferKey.downloadPath, 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 + ) + putPrefString(PreferKey.downloadPath, uri.toString()) + } + } + } + } +} \ No newline at end of file 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 6c87ff0af..1a1851ca1 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 @@ -4,28 +4,30 @@ import android.content.SharedPreferences import android.os.Bundle import android.os.Handler import android.view.View -import androidx.appcompat.app.AlertDialog import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat import io.legado.app.App import io.legado.app.R -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus +import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig +import io.legado.app.help.LauncherIconHelp import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.ColorUtils import io.legado.app.utils.* +import org.jetbrains.anko.defaultSharedPreferences class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnSharedPreferenceChangeListener { - val items = arrayOf("极简","曜夜","经典") + val items = arrayListOf("极简", "曜夜", "经典", "黑白", "A屏黑") override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.pref_config_theme) - - findPreference("defaultTheme")?.summary = "${items[getPrefInt("default_theme", 0)]}" + onSharedPreferenceChanged(requireContext().defaultSharedPreferences, "defaultTheme") } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { @@ -46,6 +48,7 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { sharedPreferences ?: return when (key) { + PreferKey.launcherIcon -> LauncherIconHelp.changeIcon(getPrefString(key)) "transparentStatusBar" -> { recreateActivities() } @@ -91,46 +94,58 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar upTheme(true) } } + "defaultTheme" -> findPreference(key)?.summary = items[getPrefInt(key)] } } override fun onPreferenceTreeClick(preference: Preference?): Boolean { when (preference?.key) { - "defaultTheme" -> { - activity?.let { - AlertDialog.Builder(it) - .setTitle("切换默认主题") - .setItems(items){ - _,which -> - preference.summary = "${items[which]}" - putPrefInt("default_theme", which) - when (which) { - 0 -> { - putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100)) - putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color)) - putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) - putPrefBoolean("isNightTheme", false) - } - 1 -> { - putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) - putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color)) - putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) - putPrefBoolean("isNightTheme", true) - } - 2 -> { - putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)) - putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)) - putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) - putPrefBoolean("isNightTheme", false) - } - } - App.INSTANCE.applyDayNight() - recreateActivities() + "defaultTheme" -> alert(title = "切换默认主题") { + items(items) { _, which -> + when (which) { + 0 -> { + putPrefInt("colorPrimary", getCompatColor(R.color.md_grey_100)) + putPrefInt("colorAccent", getCompatColor(R.color.lightBlue_color)) + putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) + AppConfig.isNightTheme = false + } + 1 -> { + putPrefInt("colorPrimaryNight", getCompatColor(R.color.shine_color)) + putPrefInt("colorAccentNight", getCompatColor(R.color.lightBlue_color)) + putPrefInt("colorBackgroundNight", getCompatColor(R.color.shine_color)) + AppConfig.isNightTheme = true + } + 2 -> { + putPrefInt("colorPrimary", getCompatColor(R.color.md_light_blue_500)) + putPrefInt("colorAccent", getCompatColor(R.color.md_pink_800)) + putPrefInt("colorBackground", getCompatColor(R.color.md_grey_100)) + AppConfig.isNightTheme = false } - .show().applyTint() + 3 -> { + putPrefInt("colorPrimary", getCompatColor(R.color.white)) + putPrefInt("colorAccent", getCompatColor(R.color.black)) + putPrefInt("colorBackground", getCompatColor(R.color.white)) + AppConfig.isNightTheme = false + } + 4 -> { + putPrefInt("colorPrimaryNight", getCompatColor(R.color.black)) + putPrefInt( + "colorAccentNight", + getCompatColor(R.color.md_grey_600) + ) + putPrefInt( + "colorBackgroundNight", + getCompatColor(R.color.black) + ) + AppConfig.isNightTheme = true + } + } + putPrefInt("defaultTheme", which) + App.INSTANCE.applyDayNight() + recreateActivities() } - } + }.show().applyTint() } return super.onPreferenceTreeClick(preference) } @@ -154,14 +169,14 @@ class ThemeConfigFragment : PreferenceFragmentCompat(), SharedPreferences.OnShar } private fun upTheme(isNightTheme: Boolean) { - if (this.isNightTheme == isNightTheme) { + if (AppConfig.isNightTheme == isNightTheme) { App.INSTANCE.applyTheme() recreateActivities() } } private fun recreateActivities() { - postEvent(Bus.RECREATE, "") + postEvent(EventBus.RECREATE, "") Handler().postDelayed({ activity?.recreate() }, 100L) } diff --git a/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt b/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt deleted file mode 100644 index 446c93f68..000000000 --- a/app/src/main/java/io/legado/app/ui/config/WebDavConfigFragment.kt +++ /dev/null @@ -1,329 +0,0 @@ -package io.legado.app.ui.config - -import android.app.Activity.RESULT_OK -import android.content.Intent -import android.net.Uri -import android.os.Build -import android.os.Bundle -import android.text.InputType -import android.view.View -import androidx.documentfile.provider.DocumentFile -import androidx.preference.EditTextPreference -import androidx.preference.ListPreference -import androidx.preference.Preference -import androidx.preference.PreferenceFragmentCompat -import io.legado.app.R -import io.legado.app.constant.PreferKey -import io.legado.app.help.IntentHelp -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.help.storage.Backup -import io.legado.app.help.storage.Restore -import io.legado.app.help.storage.WebDavHelp -import io.legado.app.lib.dialogs.alert -import io.legado.app.lib.dialogs.noButton -import io.legado.app.lib.dialogs.yesButton -import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.accentColor -import io.legado.app.utils.* -import kotlinx.coroutines.* -import kotlinx.coroutines.Dispatchers.IO -import org.jetbrains.anko.toast -import kotlin.coroutines.CoroutineContext - -class WebDavConfigFragment : PreferenceFragmentCompat(), - Preference.OnPreferenceChangeListener, - CoroutineScope { - private lateinit var job: Job - private val oldDataRequestCode = 11 - private val backupSelectRequestCode = 22 - private val restoreSelectRequestCode = 33 - - override val coroutineContext: CoroutineContext - get() = job + Dispatchers.Main - - override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { - job = Job() - fun bindPreferenceSummaryToValue(preference: Preference?) { - preference?.apply { - onPreferenceChangeListener = this@WebDavConfigFragment - onPreferenceChange( - this, - context.getPrefString(key) - ) - } - } - addPreferencesFromResource(R.xml.pref_config_web_dav) - findPreference("web_dav_url")?.let { - it.setOnBindEditTextListener { editText -> - ATH.setTint(editText, requireContext().accentColor) - } - bindPreferenceSummaryToValue(it) - } - findPreference("web_dav_account")?.let { - it.setOnBindEditTextListener { editText -> - ATH.setTint(editText, requireContext().accentColor) - } - bindPreferenceSummaryToValue(it) - } - findPreference("web_dav_password")?.let { - it.setOnBindEditTextListener { editText -> - ATH.setTint(editText, requireContext().accentColor) - editText.inputType = - InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT - } - bindPreferenceSummaryToValue(it) - } - } - - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { - super.onViewCreated(view, savedInstanceState) - ATH.applyEdgeEffectColor(listView) - } - - override fun onDestroy() { - super.onDestroy() - job.cancel() - } - - override fun onPreferenceChange(preference: Preference?, newValue: Any?): Boolean { - when { - preference?.key == "web_dav_password" -> if (newValue == null) { - preference.summary = getString(R.string.web_dav_pw_s) - } else { - preference.summary = "*".repeat(newValue.toString().length) - } - preference?.key == "web_dav_url" -> if (newValue == null) { - preference.summary = getString(R.string.web_dav_url_s) - } else { - preference.summary = newValue.toString() - } - preference?.key == "web_dav_account" -> if (newValue == null) { - preference.summary = getString(R.string.web_dav_account_s) - } else { - preference.summary = newValue.toString() - } - preference is ListPreference -> { - val index = preference.findIndexOfValue(newValue?.toString()) - // Set the summary to reflect the new value. - preference.setSummary(if (index >= 0) preference.entries[index] else null) - } - else -> preference?.summary = newValue?.toString() - } - return true - } - - override fun onPreferenceTreeClick(preference: Preference?): Boolean { - when (preference?.key) { - "web_dav_backup" -> backup() - "web_dav_restore" -> restore() - "import_old" -> importOldData() - } - return super.onPreferenceTreeClick(preference) - } - - private fun backup() { - val backupPath = getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(requireContext(), uri) - if (doc?.canWrite() == true) { - launch { - Backup.backup(requireContext(), uri) - } - } else { - selectBackupFolder() - } - } else { - selectBackupFolder() - } - } - - private fun selectBackupFolder() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, backupSelectRequestCode) - } catch (e: java.lang.Exception) { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - launch { - Backup.backup(requireContext(), null) - } - } - .request() - } - } - - fun restore() { - launch { - if (!WebDavHelp.showRestoreDialog(requireContext())) { - val backupPath = getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(requireContext(), uri) - if (doc?.canWrite() == true) { - Restore.restore(requireContext(), uri) - toast(R.string.restore_success) - } else { - selectBackupFolder() - } - } else { - selectRestoreFolder() - } - } - } - } - - private fun selectRestoreFolder() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, restoreSelectRequestCode) - } catch (e: java.lang.Exception) { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - launch { - Restore.restore(Backup.legadoPath) - toast(R.string.restore_success) - } - } - .request() - } - } - - private fun importOldData() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, oldDataRequestCode) - } catch (e: Exception) { - needInstallApps { - alert(title = "导入") { - message = "是否导入旧版本数据" - yesButton { - PermissionsCompat.Builder(this@WebDavConfigFragment) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - Restore.importYueDuData(requireContext()) - } - .request() - } - noButton { - } - }.show().applyTint() - } - } - } - - private fun importOldData(uri: Uri) { - launch(IO) { - DocumentFile.fromTreeUri(requireContext(), uri)?.listFiles()?.forEach { - when (it.name) { - "myBookShelf.json" -> - try { - DocumentUtils.readText(requireContext(), it.uri)?.let { json -> - val importCount = Restore.importOldBookshelf(json) - withContext(Dispatchers.Main) { - requireContext().toast("成功导入书籍${importCount}") - } - } - } catch (e: java.lang.Exception) { - withContext(Dispatchers.Main) { - requireContext().toast("导入书籍失败\n${e.localizedMessage}") - } - } - "myBookSource.json" -> - try { - DocumentUtils.readText(requireContext(), it.uri)?.let { json -> - val importCount = Restore.importOldSource(json) - withContext(Dispatchers.Main) { - requireContext().toast("成功导入书源${importCount}") - } - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - requireContext().toast("导入源失败\n${e.localizedMessage}") - } - } - "myBookReplaceRule.json" -> - try { - DocumentUtils.readText(requireContext(), it.uri)?.let { json -> - val importCount = Restore.importOldReplaceRule(json) - withContext(Dispatchers.Main) { - requireContext().toast("成功导入替换规则${importCount}") - } - } - } catch (e: Exception) { - withContext(Dispatchers.Main) { - requireContext().toast("导入替换规则失败\n${e.localizedMessage}") - } - } - } - } - } - } - - private fun needInstallApps(callback: () -> Unit) { - - fun canRequestPackageInstalls(): Boolean { - if (Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { - return requireContext().packageManager.canRequestPackageInstalls() - } - return true - } - if (!canRequestPackageInstalls()) { - alert(title = "开启权限提示") { - message = "需要打开「安装外部来源应用」权限才能导入旧版数据,请去设置中开启" - yesButton { - IntentHelp.toInstallUnknown(requireContext()) - } - noButton { - } - }.show().applyTint() - } else { - LogUtils.d("xxx", "import old") - callback() - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - oldDataRequestCode -> - if (resultCode == RESULT_OK) data?.data?.let { uri -> - importOldData(uri) - } - backupSelectRequestCode -> 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 - ) - putPrefString(PreferKey.backupPath, uri.toString()) - launch { - Backup.backup(requireContext(), uri) - } - } - } - restoreSelectRequestCode -> 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 - ) - putPrefString(PreferKey.backupPath, uri.toString()) - launch { - Restore.restore(requireContext(), uri) - toast(R.string.restore_success) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt b/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt index a2ff5d99e..ef1d87e68 100644 --- a/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt +++ b/app/src/main/java/io/legado/app/ui/download/DownloadActivity.kt @@ -1,29 +1,47 @@ package io.legado.app.ui.download +import android.app.Activity +import android.content.Intent import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.LinearLayoutManager +import com.google.android.material.snackbar.Snackbar import io.legado.app.App import io.legado.app.R -import io.legado.app.base.BaseActivity -import io.legado.app.constant.Bus +import io.legado.app.base.VMBaseActivity +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.help.permission.Permissions +import io.legado.app.help.permission.PermissionsCompat import io.legado.app.service.help.Download -import io.legado.app.utils.applyTint -import io.legado.app.utils.observeEvent +import io.legado.app.ui.filechooser.FileChooserDialog +import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_download.* +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.anko.alert +import org.jetbrains.anko.toast -class DownloadActivity : BaseActivity(R.layout.activity_download) { - +class DownloadActivity : VMBaseActivity(R.layout.activity_download), + FileChooserDialog.CallBack, + DownloadAdapter.CallBack { + private val exportRequestCode = 32 + private val exportBookPathKey = "exportBookPath" lateinit var adapter: DownloadAdapter private var bookshelfLiveData: LiveData>? = null private var menu: Menu? = null + private var exportPosition = -1 + + override val viewModel: DownloadViewModel + get() = getViewModel(DownloadViewModel::class.java) override fun onActivityCreated(savedInstanceState: Bundle?) { initRecyclerView() @@ -39,13 +57,17 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) { override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_download -> launch(IO) { - App.db.bookDao().webBooks.forEach { book -> - Download.start( - this@DownloadActivity, - book.bookUrl, - book.durChapterIndex, - book.totalChapterNum - ) + if (adapter.downloadMap.isNullOrEmpty()) { + App.db.bookDao().webBooks.forEach { book -> + Download.start( + this@DownloadActivity, + book.bookUrl, + book.durChapterIndex, + book.totalChapterNum + ) + } + } else { + Download.stop(this@DownloadActivity) } } } @@ -54,7 +76,7 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) { private fun initRecyclerView() { recycler_view.layoutManager = LinearLayoutManager(this) - adapter = DownloadAdapter(this) + adapter = DownloadAdapter(this, this) recycler_view.adapter = adapter } @@ -63,19 +85,120 @@ class DownloadActivity : BaseActivity(R.layout.activity_download) { bookshelfLiveData = App.db.bookDao().observeDownload() bookshelfLiveData?.observe(this, Observer { adapter.setItems(it) + initCacheSize(it) }) } + private fun initCacheSize(books: List) { + launch(IO) { + books.forEach { book -> + val chapterCaches = hashSetOf() + val cacheNames = BookHelp.getChapterFiles(book) + App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> + if (cacheNames.contains(BookHelp.formatChapterName(chapter))) { + chapterCaches.add(chapter.url) + } + } + adapter.cacheChapters[book.bookUrl] = chapterCaches + withContext(Dispatchers.Main) { + adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) + } + } + } + } + override fun observeLiveBus() { - observeEvent(Bus.UP_DOWNLOAD) { - if (it) { - menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp) + observeEvent>>(EventBus.UP_DOWNLOAD) { + if (it.isEmpty()) { + menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_play_24dp) menu?.applyTint(this) - adapter.notifyItemRangeChanged(0, adapter.itemCount, true) } else { - menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_play_24dp) + menu?.findItem(R.id.menu_download)?.setIcon(R.drawable.ic_stop_black_24dp) menu?.applyTint(this) } + adapter.downloadMap = it + adapter.notifyItemRangeChanged(0, adapter.getActualItemCount(), true) + } + observeEvent(EventBus.SAVE_CONTENT) { + adapter.cacheChapters[it.bookUrl]?.add(it.url) + } + } + + override fun export(position: Int) { + exportPosition = position + alert { + titleResource = R.string.select_folder + items(resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> { + val path = ACache.get(this@DownloadActivity).getAsString(exportBookPathKey) + if (path.isNullOrEmpty()) { + toast("没有默认路径") + } else { + startExport(path) + } + } + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivityForResult(intent, exportRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + PermissionsCompat.Builder(this@DownloadActivity) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + FileChooserDialog.show( + supportFragmentManager, + exportRequestCode, + mode = FileChooserDialog.DIRECTORY + ) + } + .request() + } + } + } + }.show() + } + + private fun startExport(path: String) { + adapter.getItem(exportPosition)?.let { book -> + Snackbar.make(title_bar, R.string.exporting, Snackbar.LENGTH_INDEFINITE) + .show() + viewModel.export(path, book) { + title_bar.snackbar(it) + } + } + } + + override fun onFilePicked(requestCode: Int, currentPath: String) { + when (requestCode) { + exportRequestCode -> { + ACache.get(this@DownloadActivity).put(exportBookPathKey, currentPath) + startExport(currentPath) + } + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + exportRequestCode -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { uri -> + contentResolver.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + ACache.get(this@DownloadActivity).put(exportBookPathKey, uri.toString()) + startExport(uri.toString()) + } + } + } } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt b/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt index 25b1b5b35..57cafc78a 100644 --- a/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/download/DownloadAdapter.kt @@ -1,36 +1,75 @@ package io.legado.app.ui.download import android.content.Context -import android.view.View +import android.widget.ImageView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.Book -import io.legado.app.help.BookHelp +import io.legado.app.data.entities.BookChapter +import io.legado.app.service.help.Download import kotlinx.android.synthetic.main.item_download.view.* +import org.jetbrains.anko.sdk27.listeners.onClick -class DownloadAdapter(context: Context) : +class DownloadAdapter(context: Context, private val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_download) { + val cacheChapters = hashMapOf>() + var downloadMap: HashMap>? = null + override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { with(holder.itemView) { if (payloads.isEmpty()) { tv_name.text = item.name - tv_author.text = item.author - upDownloadCount(this, item) + tv_author.text = context.getString(R.string.author_show, item.getRealAuthor()) + val cs = cacheChapters[item.bookUrl] + if (cs == null) { + tv_download.setText(R.string.loading) + } else { + tv_download.text = + context.getString(R.string.download_count, cs.size, item.totalChapterNum) + } + upDownloadIv(iv_download, item) } else { - upDownloadCount(this, item) + val cacheSize = cacheChapters[item.bookUrl]?.size ?: 0 + tv_download.text = + context.getString(R.string.download_count, cacheSize, item.totalChapterNum) + upDownloadIv(iv_download, item) + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + iv_download.onClick { + getItem(holder.layoutPosition)?.let { + if (downloadMap?.containsKey(it.bookUrl) == true) { + Download.remove(context, it.bookUrl) + } else { + Download.start(context, it.bookUrl, 0, it.totalChapterNum) + } + } + } + tv_export.onClick { + callBack.export(holder.layoutPosition) } } } - private fun upDownloadCount(view: View, book: Book) { - view.tv_download.text = context.getString( - R.string.download_count, - BookHelp.getChapterCount(book), - book.totalChapterNum - ) + private fun upDownloadIv(iv: ImageView, book: Book) { + downloadMap?.let { + if (it.containsKey(book.bookUrl)) { + iv.setImageResource(R.drawable.ic_stop_black_24dp) + } else { + iv.setImageResource(R.drawable.ic_play_24dp) + } + } ?: let { + iv.setImageResource(R.drawable.ic_play_24dp) + } } + interface CallBack { + fun export(position: Int) + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/download/DownloadViewModel.kt b/app/src/main/java/io/legado/app/ui/download/DownloadViewModel.kt new file mode 100644 index 000000000..da12d3d53 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/download/DownloadViewModel.kt @@ -0,0 +1,63 @@ +package io.legado.app.ui.download + +import android.app.Application +import android.net.Uri +import androidx.documentfile.provider.DocumentFile +import io.legado.app.App +import io.legado.app.R +import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.Book +import io.legado.app.help.BookHelp +import io.legado.app.utils.DocumentUtils +import io.legado.app.utils.FileUtils +import io.legado.app.utils.isContentPath +import io.legado.app.utils.writeText +import java.io.File + + +class DownloadViewModel(application: Application) : BaseViewModel(application) { + + + fun export(path: String, book: Book, finally: (msg: String) -> Unit) { + execute { + if (path.isContentPath()) { + val uri = Uri.parse(path) + DocumentFile.fromTreeUri(context, uri)?.let { + export(it, book) + } + } else { + export(FileUtils.createFolderIfNotExist(path), book) + } + }.onError { + finally(it.localizedMessage ?: "ERROR") + }.onSuccess { + finally(context.getString(R.string.success)) + } + } + + private fun export(doc: DocumentFile, book: Book) { + DocumentUtils.createFileIfNotExist(doc, "${book.name}.txt") + ?.writeText(context, getAllContents(book)) + } + + private fun export(file: File, book: Book) { + FileUtils.createFileIfNotExist(file, "${book.name}.txt") + .writeText(getAllContents(book)) + } + + private fun getAllContents(book: Book): String { + val stringBuilder = StringBuilder() + stringBuilder.append(book.name) + .append("\n") + .append(context.getString(R.string.author_show, book.author)) + App.db.bookChapterDao().getChapterList(book.bookUrl).forEach { chapter -> + BookHelp.getContent(book, chapter).let { + stringBuilder.append("\n\n") + .append(chapter.title) + .append("\n") + .append(it) + } + } + return stringBuilder.toString() + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt b/app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt index 312129fd7..8cc5d7c7e 100644 --- a/app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt +++ b/app/src/main/java/io/legado/app/ui/explore/ExploreShowActivity.kt @@ -2,7 +2,6 @@ package io.legado.app.ui.explore import android.os.Bundle import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R @@ -10,7 +9,8 @@ import io.legado.app.base.VMBaseActivity import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook import io.legado.app.ui.book.info.BookInfoActivity -import io.legado.app.ui.widget.LoadMoreView +import io.legado.app.ui.widget.recycler.LoadMoreView +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.activity_explore_show.* import org.jetbrains.anko.startActivity @@ -34,7 +34,7 @@ class ExploreShowActivity : VMBaseActivity(R.layout.activi private fun initRecyclerView() { adapter = ExploreShowAdapter(this, this) recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration(DividerItemDecoration(this, RecyclerView.VERTICAL)) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter loadMoreView = LoadMoreView(this) adapter.addFooterView(loadMoreView) diff --git a/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt b/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt index d25e7f255..2204dae03 100644 --- a/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/explore/ExploreShowAdapter.kt @@ -6,7 +6,6 @@ import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.Book import io.legado.app.data.entities.SearchBook -import io.legado.app.help.ImageLoader import io.legado.app.utils.gone import io.legado.app.utils.visible import kotlinx.android.synthetic.main.item_bookshelf_list.view.iv_cover @@ -17,8 +16,8 @@ import org.jetbrains.anko.sdk27.listeners.onClick class ExploreShowAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_search) { - override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) = - with(holder.itemView) { + override fun convert(holder: ItemViewHolder, item: SearchBook, payloads: MutableList) { + holder.itemView.apply { tv_name.text = item.name tv_author.text = context.getString(R.string.author_show, item.author) if (item.latestChapterTitle.isNullOrEmpty()) { @@ -33,42 +32,19 @@ class ExploreShowAdapter(context: Context, val callBack: CallBack) : ll_kind.gone() } else { ll_kind.visible() - for (index in 0..2) { - if (kinds.size > index) { - when (index) { - 0 -> { - tv_kind.text = kinds[index] - tv_kind.visible() - } - 1 -> { - tv_kind_1.text = kinds[index] - tv_kind_1.visible() - } - 2 -> { - tv_kind_2.text = kinds[index] - tv_kind_2.visible() - } - } - } else { - when (index) { - 0 -> tv_kind.gone() - 1 -> tv_kind_1.gone() - 2 -> tv_kind_2.gone() - } - } - } + ll_kind.setLabels(kinds) } - item.coverUrl.let { - ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.image_cover_default) - .error(R.drawable.image_cover_default) - .centerCrop() - .into(iv_cover) - } - onClick { - callBack.showBookInfo(item.toBook()) + iv_cover.load(item.coverUrl, item.name, item.author) + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.showBookInfo(it.toBook()) } } + } interface CallBack { fun showBookInfo(book: Book) diff --git a/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt b/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt index cf7ea9ebc..849b1ca87 100644 --- a/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/explore/ExploreShowViewModel.kt @@ -21,7 +21,7 @@ class ExploreShowViewModel(application: Application) : BaseViewModel(application execute { val sourceUrl = intent.getStringExtra("sourceUrl") exploreUrl = intent.getStringExtra("exploreUrl") - if (bookSource == null) { + if (bookSource == null && sourceUrl != null) { bookSource = App.db.bookSourceDao().getBookSource(sourceUrl) } explore() diff --git a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt index b2d72a021..46c1271fc 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/FileChooserDialog.kt @@ -6,22 +6,16 @@ import android.view.LayoutInflater import android.view.MenuItem import android.view.View import android.view.ViewGroup -import android.widget.LinearLayout import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.fragment.app.FragmentManager -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.constant.Theme -import io.legado.app.lib.theme.ATH import io.legado.app.ui.filechooser.adapter.FileAdapter import io.legado.app.ui.filechooser.adapter.PathAdapter -import io.legado.app.utils.FileUtils -import io.legado.app.utils.applyTint -import io.legado.app.utils.gone -import io.legado.app.utils.visible +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_file_chooser.* @@ -47,21 +41,19 @@ class FileChooserDialog : DialogFragment(), allowExtensions: Array? = null, menus: Array? = null ) { - val fragment = (manager.findFragmentByTag(tag) as? FileChooserDialog) - ?: FileChooserDialog().apply { - val bundle = Bundle() - bundle.putInt("mode", mode) - bundle.putInt("requestCode", requestCode) - bundle.putString("title", title) - bundle.putBoolean("isShowHomeDir", isShowHomeDir) - bundle.putBoolean("isShowUpDir", isShowUpDir) - bundle.putBoolean("isShowHideDir", isShowHideDir) - bundle.putString("initPath", initPath) - bundle.putStringArray("allowExtensions", allowExtensions) - bundle.putStringArray("menus", menus) - arguments = bundle - } - fragment.show(manager, tag) + FileChooserDialog().apply { + val bundle = Bundle() + bundle.putInt("mode", mode) + bundle.putInt("requestCode", requestCode) + bundle.putString("title", title) + bundle.putBoolean("isShowHomeDir", isShowHomeDir) + bundle.putBoolean("isShowUpDir", isShowUpDir) + bundle.putBoolean("isShowHideDir", isShowHideDir) + bundle.putString("initPath", initPath) + bundle.putStringArray("allowExtensions", allowExtensions) + bundle.putStringArray("menus", menus) + arguments = bundle + }.show(manager, tag) } } @@ -97,8 +89,7 @@ class FileChooserDialog : DialogFragment(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - ATH.applyBackgroundTint(view) - ATH.applyBackgroundTint(rv_path) + view.setBackgroundResource(R.color.background_card) arguments?.let { requestCode = it.getInt("requestCode") mode = it.getInt("mode", FILE) @@ -142,7 +133,7 @@ class FileChooserDialog : DialogFragment(), fileAdapter = FileAdapter(requireContext(), this) pathAdapter = PathAdapter(requireContext(), this) - rv_file.addItemDecoration(DividerItemDecoration(activity, LinearLayout.VERTICAL)) + rv_file.addItemDecoration(rv_file.getVerticalDivider()) rv_file.layoutManager = LinearLayoutManager(activity) rv_file.adapter = fileAdapter diff --git a/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt b/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt index a34c705b9..4ade6da7f 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/adapter/FileAdapter.kt @@ -64,7 +64,7 @@ class FileAdapter(context: Context, val callBack: CallBack) : fileParent.icon = upIcon fileParent.name = DIR_PARENT fileParent.size = 0 - fileParent.path = File(path).parent + fileParent.path = File(path).parent ?: "" data.add(fileParent) } currentPath?.let { currentPath -> @@ -110,9 +110,12 @@ class FileAdapter(context: Context, val callBack: CallBack) : holder.itemView.apply { image_view.setImageDrawable(item.icon) text_view.text = item.name - onClick { - callBack.onFileClick(holder.layoutPosition) - } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + callBack.onFileClick(holder.layoutPosition) } } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt b/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt index afa8ab4e6..3d2323495 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/adapter/PathAdapter.kt @@ -15,6 +15,7 @@ import java.util.* class PathAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_path_filepicker) { private val paths = LinkedList() + @Suppress("DEPRECATION") private val sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath private val arrowIcon = ConvertUtils.toDrawable(FilePickerIcon.getARROW()) @@ -35,9 +36,11 @@ class PathAdapter(context: Context, val callBack: CallBack) : path1 = path1.replace(sdCardDirectory, "") paths.clear() if (path1 != "/" && path1 != "") { - val tmps = path1.substring(path1.indexOf("/") + 1).split("/".toRegex()) - .dropLastWhile { it.isEmpty() }.toTypedArray() - Collections.addAll(paths, *tmps) + val subDirs = path1.substring(path1.indexOf("/") + 1) + .split("/".toRegex()) + .dropLastWhile { it.isEmpty() } + .toTypedArray() + Collections.addAll(paths, *subDirs) } paths.addFirst(ROOT_HINT) setItems(paths) @@ -47,9 +50,12 @@ class PathAdapter(context: Context, val callBack: CallBack) : holder.itemView.apply { text_view.text = item image_view.setImageDrawable(arrowIcon) - onClick { - callBack.onPathClick(holder.layoutPosition) - } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + callBack.onPathClick(holder.layoutPosition) } } @@ -58,6 +64,6 @@ class PathAdapter(context: Context, val callBack: CallBack) : } companion object { - private val ROOT_HINT = "SD" + private const val ROOT_HINT = "SD" } } diff --git a/app/src/main/java/io/legado/app/ui/filechooser/utils/FileUtils.kt b/app/src/main/java/io/legado/app/ui/filechooser/utils/FileUtils.kt index 71e838cae..309dee0a2 100644 --- a/app/src/main/java/io/legado/app/ui/filechooser/utils/FileUtils.kt +++ b/app/src/main/java/io/legado/app/ui/filechooser/utils/FileUtils.kt @@ -34,13 +34,13 @@ object FileUtils { * 将目录分隔符统一为平台默认的分隔符,并为目录结尾添加分隔符 */ fun separator(path: String): String { - var path = path + var path1 = path val separator = File.separator - path = path.replace("\\", separator) - if (!path.endsWith(separator)) { - path += separator + path1 = path1.replace("\\", separator) + if (!path1.endsWith(separator)) { + path1 += separator } - return path + return path1 } fun closeSilently(c: Closeable?) { @@ -62,7 +62,7 @@ object FileUtils { startDirPath: String, excludeDirs: Array? = null, @SortType sortType: Int = BY_NAME_ASC ): Array { - var excludeDirs = excludeDirs + var excludeDirs1 = excludeDirs val dirList = ArrayList() val startDir = File(startDirPath) if (!startDir.isDirectory) { @@ -75,12 +75,12 @@ object FileUtils { f.isDirectory }) ?: return arrayOfNulls(0) - if (excludeDirs == null) { - excludeDirs = arrayOfNulls(0) + if (excludeDirs1 == null) { + excludeDirs1 = arrayOfNulls(0) } for (dir in dirs) { val file = dir.absoluteFile - if (!excludeDirs.contentDeepToString().contains(file.name)) { + if (!excludeDirs1.contentDeepToString().contains(file.name)) { dirList.add(file) } } @@ -147,15 +147,15 @@ object FileUtils { if (!f.isDirectory) { return arrayOfNulls(0) } - val files = f.listFiles(FileFilter { f -> - if (f == null) { + val files = f.listFiles(FileFilter { file -> + if (file == null) { return@FileFilter false } - if (f.isDirectory) { + if (file.isDirectory) { return@FileFilter false } - filterPattern?.matcher(f.name)?.find() ?: true + filterPattern?.matcher(file.name)?.find() ?: true }) ?: return arrayOfNulls(0) for (file in files) { @@ -191,7 +191,7 @@ object FileUtils { */ fun listFiles(startDirPath: String, allowExtensions: Array): Array? { val file = File(startDirPath) - return file.listFiles { dir, name -> + return file.listFiles { _, name -> //返回当前目录所有以某些扩展名结尾的文件 val extension = getExtension(name) allowExtensions.contentDeepToString().contains(extension) @@ -294,10 +294,8 @@ object FileUtils { bis.close() bos.close() } else if (src.isDirectory) { - val files = src.listFiles() - tar.mkdirs() - for (file in files) { + src.listFiles()?.forEach { file -> copy(file.absoluteFile, File(tar.absoluteFile, file.name)) } } @@ -399,18 +397,16 @@ object FileUtils { fun writeBytes(filepath: String, data: ByteArray): Boolean { val file = File(filepath) var fos: FileOutputStream? = null - try { + return try { if (!file.exists()) { - - file.parentFile.mkdirs() - + file.parentFile?.mkdirs() file.createNewFile() } fos = FileOutputStream(filepath) fos.write(data) - return true + true } catch (e: IOException) { - return false + false } finally { closeSilently(fos) } @@ -422,16 +418,16 @@ object FileUtils { fun appendText(path: String, content: String): Boolean { val file = File(path) var writer: FileWriter? = null - try { + return try { if (!file.exists()) { file.createNewFile() } writer = FileWriter(file, true) writer.write(content) - return true + true } catch (e: IOException) { - return false + false } finally { closeSilently(writer) } diff --git a/app/src/main/java/io/legado/app/ui/importbook/ImportBookActivity.kt b/app/src/main/java/io/legado/app/ui/importbook/ImportBookActivity.kt index 8cbce52f8..8afa2793b 100644 --- a/app/src/main/java/io/legado/app/ui/importbook/ImportBookActivity.kt +++ b/app/src/main/java/io/legado/app/ui/importbook/ImportBookActivity.kt @@ -1,19 +1,210 @@ package io.legado.app.ui.importbook +import android.annotation.SuppressLint +import android.app.Activity +import android.content.Intent +import android.net.Uri import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import androidx.appcompat.widget.PopupMenu +import androidx.documentfile.provider.DocumentFile +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +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.help.AppConfig +import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.utils.DocumentUtils import io.legado.app.utils.getViewModel +import kotlinx.android.synthetic.main.activity_import_book.* +import kotlinx.coroutines.Dispatchers.IO +import kotlinx.coroutines.Dispatchers.Main +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.jetbrains.anko.sdk27.listeners.onClick +import java.io.File -class ImportBookActivity : VMBaseActivity(R.layout.activity_import_book) { +class ImportBookActivity : VMBaseActivity(R.layout.activity_import_book), + PopupMenu.OnMenuItemClickListener, + ImportBookAdapter.CallBack { + private val requestCodeSelectFolder = 342 + private var rootDoc: DocumentFile? = null + private val subDocs = arrayListOf() + private lateinit var adapter: ImportBookAdapter + private var localUriLiveData: LiveData>? = null override val viewModel: ImportBookViewModel get() = getViewModel(ImportBookViewModel::class.java) - override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initEvent() + initData() + upRootDoc() + } + + override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.import_book, menu) + return super.onCompatCreateOptionsMenu(menu) + } + + private fun initView() { + recycler_view.layoutManager = LinearLayoutManager(this) + adapter = ImportBookAdapter(this, this) + recycler_view.adapter = adapter + rotate_loading.loadingColor = accentColor + select_action_bar.setMainActionText(R.string.add_to_shelf) + select_action_bar.inflateMenu(R.menu.import_book_sel) + select_action_bar.setOnMenuItemClickListener(this) + select_action_bar.setCallBack(object : SelectActionBar.CallBack { + override fun selectAll(selectAll: Boolean) { + adapter.selectAll(selectAll) + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + viewModel.addToBookshelf(adapter.selectedUris) { + upPath() + } + } + }) + } + + private fun initEvent() { + tv_go_back.onClick { + goBackDir() + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_select_folder -> selectImportFolder() + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_del_selection -> + viewModel.deleteDoc(adapter.selectedUris) { + upPath() + } + } + return false + } + + private fun initData() { + localUriLiveData?.removeObservers(this) + localUriLiveData = App.db.bookDao().observeLocalUri() + localUriLiveData?.observe(this, Observer { + adapter.upBookHas(it) + }) + } + + private fun upRootDoc() { + AppConfig.importBookPath?.let { + val rootUri = Uri.parse(it) + rootDoc = DocumentFile.fromTreeUri(this, rootUri) + subDocs.clear() + upPath() + } + } + + @SuppressLint("SetTextI18n") + @Synchronized + private fun upPath() { + rootDoc?.let { rootDoc -> + var path = rootDoc.name.toString() + File.separator + var lastDoc = rootDoc + for (doc in subDocs) { + lastDoc = doc + path = path + doc.name + File.separator + } + tv_path.text = path + adapter.selectedUris.clear() + adapter.clearItems() + rotate_loading.show() + launch(IO) { + val docList = DocumentUtils.listFiles( + this@ImportBookActivity, + lastDoc.uri + ) + for (i in docList.lastIndex downTo 0) { + val item = docList[i] + if (item.name.startsWith(".")) { + docList.removeAt(i) + } else if (!item.isDir && !item.name.endsWith(".txt", true)) { + docList.removeAt(i) + } + } + docList.sortWith(compareBy({ !it.isDir }, { it.name })) + withContext(Main) { + rotate_loading.hide() + adapter.setData(docList) + } + } + } + } + + private fun selectImportFolder() { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivityForResult(intent, requestCodeSelectFolder) + } catch (e: Exception) { + e.printStackTrace() + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + super.onActivityResult(requestCode, resultCode, data) + when (requestCode) { + requestCodeSelectFolder -> if (resultCode == Activity.RESULT_OK) { + data?.data?.let { + contentResolver.takePersistableUriPermission( + it, + Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION + ) + AppConfig.importBookPath = it.toString() + upRootDoc() + } + } + } + } + + @Synchronized + override fun nextDoc(doc: DocumentFile) { + subDocs.add(doc) + upPath() + } + + @Synchronized + private fun goBackDir(): Boolean { + return if (subDocs.isNotEmpty()) { + subDocs.removeAt(subDocs.lastIndex) + upPath() + true + } else { + false + } + } + + override fun onBackPressed() { + if (!goBackDir()) { + super.onBackPressed() + } + } + override fun upCountView() { + select_action_bar.upCountView(adapter.selectedUris.size, adapter.checkableCount) } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/importbook/ImportBookAdapter.kt b/app/src/main/java/io/legado/app/ui/importbook/ImportBookAdapter.kt index 29ac7e579..6cb49d10f 100644 --- a/app/src/main/java/io/legado/app/ui/importbook/ImportBookAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/importbook/ImportBookAdapter.kt @@ -5,14 +5,119 @@ import androidx.documentfile.provider.DocumentFile import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.constant.AppConst +import io.legado.app.utils.* +import kotlinx.android.synthetic.main.item_import_book.view.* +import org.jetbrains.anko.sdk27.listeners.onClick -class ImportBookAdapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_import_book) { +class ImportBookAdapter(context: Context, val callBack: CallBack) : + SimpleRecyclerAdapter(context, R.layout.item_import_book) { + var selectedUris = hashSetOf() + var checkableCount = 0 + private var bookshelf = arrayListOf() + fun upBookHas(uriList: List) { + bookshelf.clear() + bookshelf.addAll(uriList) + notifyDataSetChanged() + upCheckableCount() + } + + fun setData(data: List) { + setItems(data) + upCheckableCount() + } + + private fun upCheckableCount() { + checkableCount = 0 + getItems().forEach { + if (!it.isDir && !bookshelf.contains(it.uri.toString())) { + checkableCount++ + } + } + callBack.upCountView() + } + + fun selectAll(selectAll: Boolean) { + if (selectAll) { + getItems().forEach { + if (!it.isDir && !bookshelf.contains(it.uri.toString())) { + selectedUris.add(it.uri.toString()) + } + } + } else { + selectedUris.clear() + } + notifyDataSetChanged() + callBack.upCountView() + } + + fun revertSelection() { + getItems().forEach { + if (!it.isDir) { + if (selectedUris.contains(it.uri.toString())) { + selectedUris.remove(it.uri.toString()) + } else { + selectedUris.add(it.uri.toString()) + } + } + } + callBack.upCountView() + } - override fun convert(holder: ItemViewHolder, item: DocumentFile, payloads: MutableList) { + override fun convert(holder: ItemViewHolder, item: DocItem, payloads: MutableList) { + holder.itemView.apply { + if (payloads.isEmpty()) { + if (item.isDir) { + iv_icon.setImageResource(R.drawable.ic_folder) + iv_icon.visible() + cb_select.invisible() + ll_brief.gone() + cb_select.isChecked = false + } else { + if (bookshelf.contains(item.uri.toString())) { + iv_icon.setImageResource(R.drawable.ic_book_has) + iv_icon.visible() + cb_select.invisible() + } else { + iv_icon.invisible() + cb_select.visible() + } + ll_brief.visible() + tv_tag.text = item.name.substringAfterLast(".") + tv_size.text = StringUtils.toSize(item.size) + tv_date.text = AppConst.DATE_FORMAT.format(item.date) + cb_select.isChecked = selectedUris.contains(item.uri.toString()) + } + tv_name.text = item.name + } else { + cb_select.isChecked = selectedUris.contains(item.uri.toString()) + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + if (it.isDir) { + callBack.nextDoc(DocumentFile.fromSingleUri(context, it.uri)!!) + } else if (!bookshelf.contains(it.uri.toString())) { + if (!selectedUris.contains(it.uri.toString())) { + selectedUris.add(it.uri.toString()) + } else { + selectedUris.remove(it.uri.toString()) + } + notifyItemChanged(holder.layoutPosition, true) + callBack.upCountView() + } + } + } + } + interface CallBack { + fun nextDoc(doc: DocumentFile) + fun upCountView() } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/importbook/ImportBookViewModel.kt b/app/src/main/java/io/legado/app/ui/importbook/ImportBookViewModel.kt index 1865f1be8..1250da682 100644 --- a/app/src/main/java/io/legado/app/ui/importbook/ImportBookViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/importbook/ImportBookViewModel.kt @@ -1,10 +1,34 @@ package io.legado.app.ui.importbook import android.app.Application +import android.net.Uri +import androidx.documentfile.provider.DocumentFile import io.legado.app.base.BaseViewModel +import io.legado.app.model.localBook.LocalBook class ImportBookViewModel(application: Application) : BaseViewModel(application) { + fun addToBookshelf(uriList: HashSet, finally: () -> Unit) { + execute { + uriList.forEach { uriStr -> + DocumentFile.fromSingleUri(context, Uri.parse(uriStr))?.let { doc -> + LocalBook.importFile(doc) + } + } + }.onFinally { + finally.invoke() + } + } + + fun deleteDoc(uriList: HashSet, finally: () -> Unit) { + execute { + uriList.forEach { + DocumentFile.fromSingleUri(context, Uri.parse(it))?.delete() + } + }.onFinally { + finally.invoke() + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/login/SourceLogin.kt b/app/src/main/java/io/legado/app/ui/login/SourceLogin.kt new file mode 100644 index 000000000..d4e9c2e4e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/login/SourceLogin.kt @@ -0,0 +1,14 @@ +package io.legado.app.ui.login + +import android.os.Bundle +import io.legado.app.R +import io.legado.app.base.BaseActivity + + +class SourceLogin : BaseActivity(R.layout.activity_source_login) { + + override fun onActivityCreated(savedInstanceState: Bundle?) { + + } + +} \ No newline at end of file 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 ddb8b3f10..38569115b 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 @@ -1,31 +1,31 @@ package io.legado.app.ui.main -import android.net.Uri import android.os.Bundle import android.view.KeyEvent import android.view.MenuItem -import androidx.documentfile.provider.DocumentFile import androidx.fragment.app.Fragment import androidx.fragment.app.FragmentManager import androidx.fragment.app.FragmentStatePagerAdapter import androidx.viewpager.widget.ViewPager +import com.github.houbb.opencc4j.util.ZhConverterUtil import com.google.android.material.bottomnavigation.BottomNavigationView import io.legado.app.App import io.legado.app.BuildConfig import io.legado.app.R import io.legado.app.base.VMBaseActivity -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey +import io.legado.app.help.AppConfig import io.legado.app.help.coroutine.Coroutine 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 -import io.legado.app.ui.about.UpdateLog import io.legado.app.ui.main.bookshelf.BookshelfFragment import io.legado.app.ui.main.explore.ExploreFragment import io.legado.app.ui.main.my.MyFragment import io.legado.app.ui.main.rss.RssFragment +import io.legado.app.ui.widget.dialog.TextDialog import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_main.* @@ -37,6 +37,7 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), private var pagePosition = 0 private val fragmentList = arrayListOf() + private var rssFragment: RssFragment? = null override fun onActivityCreated(savedInstanceState: Bundle?) { ATH.applyEdgeEffectColor(view_pager_main) @@ -46,8 +47,27 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), view_pager_main.adapter = TabFragmentPageAdapter(supportFragmentManager) view_pager_main.addOnPageChangeListener(this) bottom_navigation_view.setOnNavigationItemSelectedListener(this) - bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = isShowRSS + bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS + } + + override fun onPostCreate(savedInstanceState: Bundle?) { + super.onPostCreate(savedInstanceState) upVersion() + //初始化简繁转换引擎 + when (AppConfig.chineseConverterType) { + 1 -> Coroutine.async { ZhConverterUtil.toSimple("初始化") } + 2 -> Coroutine.async { ZhConverterUtil.toTraditional("初始化") } + } + //自动更新书籍 + if (AppConfig.autoRefreshBook) { + view_pager_main.postDelayed({ + viewModel.upChapterList() + }, 1000) + } + //清楚过期数据 + view_pager_main.postDelayed({ + viewModel.clearExpiredData() + }, 3000) } override fun onNavigationItemSelected(item: MenuItem): Boolean { @@ -67,28 +87,31 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), fragmentList.add(RssFragment()) fragmentList.add(MyFragment()) } - if (isShowRSS && fragmentList.size < 4) { - fragmentList.add(2, RssFragment()) + if (AppConfig.isShowRSS && fragmentList.size < 4) { + fragmentList.add(2, + rssFragment ?: RssFragment().apply { rssFragment = this }) } - if (!isShowRSS && fragmentList.size == 4) { + if (!AppConfig.isShowRSS && fragmentList.size == 4) { fragmentList.removeAt(2) } } private fun upVersion() { - if (getPrefInt("versionCode") != App.INSTANCE.versionCode) { - putPrefInt("versionCode", App.INSTANCE.versionCode) + if (getPrefInt(PreferKey.versionCode) != App.INSTANCE.versionCode) { + putPrefInt(PreferKey.versionCode, App.INSTANCE.versionCode) if (!BuildConfig.DEBUG) { - UpdateLog().show(supportFragmentManager, "updateLog") + val log = String(assets.open("updateLog.md").readBytes()) + TextDialog.show(supportFragmentManager, log, TextDialog.MD) } } } override fun onPageSelected(position: Int) { + view_pager_main.hideSoftInput() pagePosition = position when (position) { 0, 1, 3 -> bottom_navigation_view.menu.getItem(position).isChecked = true - 2 -> if (isShowRSS) { + 2 -> if (AppConfig.isShowRSS) { bottom_navigation_view.menu.getItem(position).isChecked = true } else { bottom_navigation_view.menu.getItem(3).isChecked = true @@ -116,40 +139,25 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), override fun finish() { if (!BuildConfig.DEBUG) { - backup() + Backup.autoBack(this) } super.finish() } - private fun backup() { - Coroutine.async { - val backupPath = getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(this@MainActivity, uri) - if (doc?.canWrite() == true) { - Backup.backup(this@MainActivity, uri) - } - } else { - Backup.backup(this@MainActivity, null) - } - } - } - override fun onDestroy() { super.onDestroy() ReadAloud.stop(this) } override fun observeLiveBus() { - observeEvent(Bus.RECREATE) { + observeEvent(EventBus.RECREATE) { recreate() } - observeEvent(Bus.SHOW_RSS) { - bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = isShowRSS + observeEvent(EventBus.SHOW_RSS) { + bottom_navigation_view.menu.findItem(R.id.menu_rss).isVisible = AppConfig.isShowRSS upFragmentList() view_pager_main.adapter?.notifyDataSetChanged() - if (isShowRSS) { + if (AppConfig.isShowRSS) { view_pager_main.setCurrentItem(3, false) } } @@ -167,7 +175,7 @@ class MainActivity : VMBaseActivity(R.layout.activity_main), } override fun getCount(): Int { - return if (isShowRSS) 4 else 3 + return if (AppConfig.isShowRSS) 4 else 3 } } 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 27b37fe1a..1a0652bae 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 @@ -3,21 +3,20 @@ package io.legado.app.ui.main import android.app.Application import io.legado.app.App import io.legado.app.base.BaseViewModel -import io.legado.app.constant.Bus -import io.legado.app.data.api.IHttpGetApi +import io.legado.app.constant.EventBus import io.legado.app.data.entities.RssSource import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.Restore import io.legado.app.model.WebBook import io.legado.app.utils.GSON -import io.legado.app.utils.NetworkUtils import io.legado.app.utils.fromJsonObject import io.legado.app.utils.postEvent import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.delay +import java.util.concurrent.TimeUnit class MainViewModel(application: Application) : BaseViewModel(application) { - val updateList = arrayListOf() + val updateList = hashSetOf() fun upChapterList() { execute { @@ -26,14 +25,14 @@ class MainViewModel(application: Application) : BaseViewModel(application) { App.db.bookSourceDao().getBookSource(book.origin)?.let { bookSource -> synchronized(this) { updateList.add(book.bookUrl) - postEvent(Bus.UP_BOOK, book.bookUrl) + postEvent(EventBus.UP_BOOK, book.bookUrl) } WebBook(bookSource).getChapterList(book) .timeout(300000) .onSuccess(IO) { synchronized(this) { updateList.remove(book.bookUrl) - postEvent(Bus.UP_BOOK, book.bookUrl) + postEvent(EventBus.UP_BOOK, book.bookUrl) } it?.let { App.db.bookDao().update(book) @@ -44,7 +43,7 @@ class MainViewModel(application: Application) : BaseViewModel(application) { .onError { synchronized(this) { updateList.remove(book.bookUrl) - postEvent(Bus.UP_BOOK, book.bookUrl) + postEvent(EventBus.UP_BOOK, book.bookUrl) } it.printStackTrace() } @@ -55,23 +54,26 @@ class MainViewModel(application: Application) : BaseViewModel(application) { } } + fun clearExpiredData() { + execute { + App.db.searchBookDao() + .clearExpired(System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)) + } + } + fun initRss() { execute { val url = "https://gitee.com/alanskycn/yuedu/raw/master/JS/RSS/rssSource" - NetworkUtils.getBaseUrl(url)?.let { - val response = - HttpHelper.getApiService(it).getAsync(url, mapOf()).await() - response.body()?.let { body -> - val sources = mutableListOf() - val items: List> = Restore.jsonPath.parse(body).read("$") - for (item in items) { - val jsonItem = Restore.jsonPath.parse(item) - GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> - sources.add(source) - } + HttpHelper.simpleGet(url)?.let { body -> + val sources = mutableListOf() + val items: List> = Restore.jsonPath.parse(body).read("$") + for (item in items) { + val jsonItem = Restore.jsonPath.parse(item) + GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> + sources.add(source) } - App.db.rssSourceDao().insert(*sources.toTypedArray()) } + App.db.rssSourceDao().insert(*sources.toTypedArray()) } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfAdapter.kt index d3ba217a0..55115ad94 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfAdapter.kt @@ -9,13 +9,24 @@ import io.legado.app.ui.main.bookshelf.books.BooksFragment class BookshelfAdapter(fragment: Fragment, val callBack: CallBack) : FragmentStateAdapter(fragment) { + private val ids = hashSetOf() + override fun getItemCount(): Int { return callBack.groupSize } + override fun getItemId(position: Int): Long { + return callBack.getGroup(position).groupId.toLong() + } + + override fun containsItem(itemId: Long): Boolean { + return ids.contains(itemId) + } + override fun createFragment(position: Int): Fragment { val groupId = callBack.getGroup(position).groupId - return BooksFragment.newInstance(groupId) + ids.add(groupId.toLong()) + return BooksFragment.newInstance(position, groupId) } interface CallBack { 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 401685b55..16b7b286f 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 @@ -1,5 +1,6 @@ package io.legado.app.ui.main.bookshelf +import android.annotation.SuppressLint import android.os.Bundle import android.view.Menu import android.view.MenuItem @@ -13,24 +14,28 @@ import io.legado.app.App import io.legado.app.R import io.legado.app.base.VMBaseFragment import io.legado.app.constant.AppConst -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.constant.PreferKey.saveTabPosition import io.legado.app.data.entities.BookGroup -import io.legado.app.lib.dialogs.selector +import io.legado.app.lib.dialogs.* import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.arrange.ArrangeBookActivity +import io.legado.app.ui.book.group.GroupManageDialog import io.legado.app.ui.book.search.SearchActivity import io.legado.app.ui.download.DownloadActivity +import io.legado.app.ui.importbook.ImportBookActivity +import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* +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 org.jetbrains.anko.sdk27.listeners.onLongClick import org.jetbrains.anko.startActivity class BookshelfFragment : VMBaseFragment(R.layout.fragment_bookshelf), + TabLayout.OnTabSelectedListener, SearchView.OnQueryTextListener, GroupManageDialog.CallBack, BookshelfAdapter.CallBack { @@ -38,12 +43,13 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b override val viewModel: BookshelfViewModel get() = getViewModel(BookshelfViewModel::class.java) + private lateinit var bookshelfAdapter: BookshelfAdapter private var bookGroupLiveData: LiveData>? = null private val bookGroups = mutableListOf() - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) - initRecyclerView() + initView() initBookGroupData() } @@ -58,12 +64,12 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b R.id.menu_bookshelf_layout -> selectBookshelfLayout() R.id.menu_group_manage -> GroupManageDialog() .show(childFragmentManager, "groupManageDialog") - R.id.menu_add_local -> { - } - R.id.menu_add_url -> { - } - R.id.menu_arrange_bookshelf -> { - } + R.id.menu_add_local -> startActivity() + R.id.menu_add_url -> addBookByUrl() + R.id.menu_arrange_bookshelf -> startActivity( + Pair("groupId", selectedGroup.groupId), + Pair("groupName", selectedGroup.groupName) + ) R.id.menu_download -> startActivity() } } @@ -75,22 +81,20 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b return bookGroups[position] } - private fun initRecyclerView() { + private val selectedGroup: BookGroup + get() = bookGroups[view_pager_bookshelf.currentItem] + + private fun initView() { tab_layout.isTabIndicatorFullWidth = false tab_layout.tabMode = TabLayout.MODE_SCROLLABLE tab_layout.setSelectedTabIndicatorColor(requireContext().accentColor) ATH.applyEdgeEffectColor(view_pager_bookshelf) - view_pager_bookshelf.adapter = BookshelfAdapter(this, this) + bookshelfAdapter = BookshelfAdapter(this, this) + view_pager_bookshelf.adapter = bookshelfAdapter TabLayoutMediator(tab_layout, view_pager_bookshelf) { tab, position -> tab.text = bookGroups[position].groupName - tab.view?.onLongClick { - tab.select() - putPrefInt(saveTabPosition, position) - toast("该分组<" + bookGroups[position].groupName + ">已成为默认页。") - true - } }.attach() - observeEvent(Bus.UP_TABS) { + observeEvent(EventBus.UP_TABS) { tab_layout.getTabAt(it)?.select() } } @@ -99,7 +103,9 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b bookGroupLiveData?.removeObservers(viewLifecycleOwner) bookGroupLiveData = App.db.bookGroupDao().liveDataAll() bookGroupLiveData?.observe(viewLifecycleOwner, Observer { + viewModel.checkGroup(it) synchronized(this) { + tab_layout.removeOnTabSelectedListener(this) bookGroups.clear() bookGroups.add(AppConst.bookGroupAll) if (AppConst.bookGroupLocalShow) { @@ -109,7 +115,9 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b bookGroups.add(AppConst.bookGroupAudio) } bookGroups.addAll(it) - view_pager_bookshelf.adapter?.notifyDataSetChanged() + bookshelfAdapter.notifyDataSetChanged() + tab_layout.getTabAt(getPrefInt(PreferKey.saveTabPosition, 0))?.select() + tab_layout.addOnTabSelectedListener(this) } }) } @@ -133,7 +141,7 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b if (getPrefBoolean("bookGroupLocal", true)) { bookGroups.add(1, AppConst.bookGroupLocal) } - view_pager_bookshelf.adapter?.notifyDataSetChanged() + bookshelfAdapter.notifyDataSetChanged() } } @@ -146,4 +154,37 @@ class BookshelfFragment : VMBaseFragment(R.layout.fragment_b activity?.recreate() } } + + @SuppressLint("InflateParams") + private fun addBookByUrl() { + requireContext() + .alert(titleResource = R.string.add_book_url) { + var editText: AutoCompleteTextView? = null + customView { + layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { + editText = edit_view + } + } + okButton { + editText?.text?.toString()?.let { + viewModel.addBookByUrl(it) + } + } + noButton { } + }.show().applyTint() + } + + override fun onTabReselected(tab: TabLayout.Tab?) { + + } + + override fun onTabUnselected(tab: TabLayout.Tab?) { + + } + + override fun onTabSelected(tab: TabLayout.Tab?) { + tab?.position?.let { + putPrefInt(PreferKey.saveTabPosition, it) + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt index 83d3c41a5..87d2b3e06 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt @@ -2,33 +2,81 @@ package io.legado.app.ui.main.bookshelf import android.app.Application import io.legado.app.App +import io.legado.app.R import io.legado.app.base.BaseViewModel +import io.legado.app.data.entities.Book import io.legado.app.data.entities.BookGroup +import io.legado.app.data.entities.BookSource +import io.legado.app.model.WebBook +import io.legado.app.utils.NetworkUtils +import kotlinx.coroutines.Dispatchers.IO class BookshelfViewModel(application: Application) : BaseViewModel(application) { - - fun addGroup(groupName: String) { - execute { - val maxId = App.db.bookGroupDao().maxId - val bookGroup = BookGroup( - groupId = maxId.plus(1), - groupName = groupName, - order = maxId.plus(1) - ) - App.db.bookGroupDao().insert(bookGroup) - } - } - - fun upGroup(vararg bookGroup: BookGroup) { + fun addBookByUrl(bookUrls: String) { + var successCount = 0 execute { - App.db.bookGroupDao().update(*bookGroup) + var hasBookUrlPattern: List? = null + val urls = bookUrls.split("\n") + for (url in urls) { + val bookUrl = url.trim() + if (bookUrl.isEmpty()) continue + if (App.db.bookDao().getBook(bookUrl) != null) continue + val baseUrl = NetworkUtils.getBaseUrl(bookUrl) ?: continue + var source = App.db.bookSourceDao().getBookSource(baseUrl) + if (source == null) { + if (hasBookUrlPattern == null) { + hasBookUrlPattern = App.db.bookSourceDao().hasBookUrlPattern + } + hasBookUrlPattern.forEach { bookSource -> + if (bookUrl.matches(bookSource.bookUrlPattern!!.toRegex())) { + source = bookSource + return@forEach + } + } + } + source?.let { bookSource -> + val book = Book( + bookUrl = bookUrl, + origin = bookSource.bookSourceUrl, + originName = bookSource.bookSourceName + ) + WebBook(bookSource).getBookInfo(book, this) + .onSuccess(IO) { + it?.let { book -> + App.db.bookDao().insert(book) + successCount++ + } + }.onError { + throw Exception(it.localizedMessage) + } + } + } + }.onSuccess { + if (successCount > 0) { + toast(R.string.success) + } else { + toast("ERROR") + } + }.onError { + toast(it.localizedMessage ?: "ERROR") } } - fun delGroup(vararg bookGroup: BookGroup) { + fun checkGroup(groups: List) { execute { - App.db.bookGroupDao().delete(*bookGroup) + groups.forEach { group -> + if (group.groupId and (group.groupId - 1) != 0) { + var id = 1 + val idsSum = App.db.bookGroupDao().idsSum + while (id and idsSum != 0) { + id = id.shl(1) + } + App.db.bookGroupDao().delete(group) + App.db.bookGroupDao().insert(group.copy(groupId = id)) + App.db.bookDao().upGroup(group.groupId, id) + } + } } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapter.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt similarity index 77% rename from app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapter.kt rename to app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt index c8f085b74..1e8fda88b 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BaseBooksAdapter.kt @@ -1,17 +1,18 @@ package io.legado.app.ui.main.bookshelf.books import android.content.Context +import androidx.core.os.bundleOf import io.legado.app.base.adapter.SimpleRecyclerAdapter import io.legado.app.data.entities.Book -abstract class BooksAdapter(context: Context, layoutId: Int) : +abstract class BaseBooksAdapter(context: Context, layoutId: Int) : SimpleRecyclerAdapter(context, layoutId) { fun notification(bookUrl: String) { for (i in 0 until itemCount) { getItem(i)?.let { if (it.bookUrl == bookUrl) { - notifyItemChanged(i, 5) + notifyItemChanged(i, bundleOf(Pair("refresh", null))) return } } diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt index f6761ce78..d647afd3b 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterGrid.kt @@ -1,11 +1,11 @@ package io.legado.app.ui.main.bookshelf.books import android.content.Context +import android.os.Bundle import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.constant.BookType import io.legado.app.data.entities.Book -import io.legado.app.help.ImageLoader import io.legado.app.lib.theme.ATH import io.legado.app.utils.invisible import kotlinx.android.synthetic.main.item_bookshelf_grid.view.* @@ -13,26 +13,16 @@ import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick class BooksAdapterGrid(context: Context, private val callBack: CallBack) : - BooksAdapter(context, R.layout.item_bookshelf_grid) { + BaseBooksAdapter(context, R.layout.item_bookshelf_grid) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { + val bundle = payloads.getOrNull(0) as? Bundle with(holder.itemView) { - if (payloads.isEmpty()) { + if (bundle == null) { ATH.applyBackgroundTint(this) tv_name.text = item.name bv_author.text = item.author - item.getDisplayCover()?.let { - ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.image_cover_default) - .error(R.drawable.image_cover_default) - .centerCrop() - .into(iv_cover) - } - onClick { callBack.open(item) } - onLongClick { - callBack.openBookInfo(item) - true - } + iv_cover.load(item.getDisplayCover(), item.name, item.author) if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { bv_unread.invisible() rl_loading.show() @@ -42,9 +32,12 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) : bv_unread.setHighlight(item.lastCheckCount > 0) } } else { - when (payloads[0]) { - 5 -> { - if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + bundle.keySet().map { + when (it) { + "name" -> tv_name.text = item.name + "author" -> bv_author.text = item.author + "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) + "refresh" -> if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { bv_unread.invisible() rl_loading.show() } else { @@ -58,4 +51,20 @@ class BooksAdapterGrid(context: Context, private val callBack: CallBack) : } } + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + true + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt index 133f865d9..755f55f20 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksAdapterList.kt @@ -1,11 +1,11 @@ package io.legado.app.ui.main.bookshelf.books import android.content.Context +import android.os.Bundle import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.constant.BookType import io.legado.app.data.entities.Book -import io.legado.app.help.ImageLoader import io.legado.app.lib.theme.ATH import io.legado.app.utils.invisible import kotlinx.android.synthetic.main.item_bookshelf_list.view.* @@ -13,28 +13,18 @@ import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick class BooksAdapterList(context: Context, private val callBack: CallBack) : - BooksAdapter(context, R.layout.item_bookshelf_list) { + BaseBooksAdapter(context, R.layout.item_bookshelf_list) { override fun convert(holder: ItemViewHolder, item: Book, payloads: MutableList) { + val bundle = payloads.getOrNull(0) as? Bundle with(holder.itemView) { - if (payloads.isEmpty()) { + if (bundle == null) { ATH.applyBackgroundTint(this) tv_name.text = item.name tv_author.text = item.author tv_read.text = item.durChapterTitle tv_last.text = item.latestChapterTitle - item.getDisplayCover()?.let { - ImageLoader.load(context, it)//Glide自动识别http://和file:// - .placeholder(R.drawable.image_cover_default) - .error(R.drawable.image_cover_default) - .centerCrop() - .into(iv_cover) - } - onClick { callBack.open(item) } - onLongClick { - callBack.openBookInfo(item) - true - } + iv_cover.load(item.getDisplayCover(), item.name, item.author) if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { bv_unread.invisible() rl_loading.show() @@ -44,9 +34,14 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) : bv_unread.setHighlight(item.lastCheckCount > 0) } } else { - when (payloads[0]) { - 5 -> { - if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { + bundle.keySet().map { + when (it) { + "name" -> tv_name.text = item.name + "author" -> tv_author.text = item.author + "dur" -> tv_read.text = item.durChapterTitle + "last" -> tv_last.text = item.latestChapterTitle + "cover" -> iv_cover.load(item.getDisplayCover(), item.name, item.author) + "refresh" -> if (item.origin != BookType.local && callBack.isUpdate(item.bookUrl)) { bv_unread.invisible() rl_loading.show() } else { @@ -60,4 +55,20 @@ class BooksAdapterList(context: Context, private val callBack: CallBack) : } } + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack.open(it) + } + } + + onLongClick { + getItem(holder.layoutPosition)?.let { + callBack.openBookInfo(it) + } + true + } + } + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt index 45ad277f2..27954c634 100644 --- a/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt +++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/books/BooksDiffCallBack.kt @@ -1,16 +1,12 @@ package io.legado.app.ui.main.bookshelf.books +import androidx.core.os.bundleOf import androidx.recyclerview.widget.DiffUtil import io.legado.app.data.entities.Book class BooksDiffCallBack(private val oldItems: List, private val newItems: List) : DiffUtil.Callback() { - - override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { - return oldItems[oldItemPosition].bookUrl == newItems[newItemPosition].bookUrl - } - override fun getOldListSize(): Int { return oldItems.size } @@ -19,15 +15,56 @@ class BooksDiffCallBack(private val oldItems: List, private val newItems: return newItems.size } + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return oldItems[oldItemPosition].bookUrl == newItems[newItemPosition].bookUrl + } + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return oldItem.name == newItem.name - && oldItem.durChapterTitle == newItem.durChapterTitle - && oldItem.latestChapterTitle == newItem.latestChapterTitle - && oldItem.getDisplayCover() == newItem.getDisplayCover() - && oldItem.getUnreadChapterNum() == newItem.getUnreadChapterNum() - && oldItem.lastCheckCount == newItem.lastCheckCount + if (oldItem.name != newItem.name) + return false + if (oldItem.author != newItem.author) + return false + if (oldItem.durChapterTitle != newItem.durChapterTitle) + return false + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) + return false + if (oldItem.lastCheckCount != newItem.lastCheckCount) + return false + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) + return false + if (oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum()) + return false + return true + } + + override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + val bundle = bundleOf() + if (oldItem.name != newItem.name) + bundle.putString("name", null) + if (oldItem.author != newItem.author) + bundle.putString("author", null) + if (oldItem.durChapterTitle != newItem.durChapterTitle) + bundle.putString("dur", null) + if (oldItem.latestChapterTitle != newItem.latestChapterTitle) + bundle.putString("last", null) + if (oldItem.getDisplayCover() != newItem.getDisplayCover()) + bundle.putString("cover", null) + if (oldItem.lastCheckCount != newItem.lastCheckCount) + bundle.putString("refresh", null) + if (oldItem.getUnreadChapterNum() != newItem.getUnreadChapterNum() + || oldItem.lastCheckCount != newItem.lastCheckCount + ) { + bundle.putString("refresh", null) + } + + if (bundle.isEmpty) { + return null + } + return bundle } } \ No newline at end of file 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 bff9586ff..1a462a294 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 @@ -7,11 +7,12 @@ import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil import androidx.recyclerview.widget.GridLayoutManager 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.BaseFragment import io.legado.app.constant.BookType -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey import io.legado.app.data.entities.Book import io.legado.app.help.IntentDataHelp @@ -24,40 +25,41 @@ import io.legado.app.ui.main.MainViewModel import io.legado.app.utils.getPrefInt import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.observeEvent -import io.legado.app.utils.postEvent import kotlinx.android.synthetic.main.fragment_books.* import org.jetbrains.anko.startActivity class BooksFragment : BaseFragment(R.layout.fragment_books), - BooksAdapter.CallBack { + BaseBooksAdapter.CallBack { companion object { - fun newInstance(position: Int): BooksFragment { + fun newInstance(position: Int, groupId: Int): BooksFragment { return BooksFragment().apply { val bundle = Bundle() - bundle.putInt("groupId", position) + bundle.putInt("position", position) + bundle.putInt("groupId", groupId) arguments = bundle } } } private lateinit var activityViewModel: MainViewModel - private lateinit var booksAdapter: BooksAdapter + private lateinit var booksAdapter: BaseBooksAdapter private var bookshelfLiveData: LiveData>? = null + private var position = 0 private var groupId = -1 - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { activityViewModel = getViewModelOfActivity(MainViewModel::class.java) arguments?.let { + position = it.getInt("position", 0) groupId = it.getInt("groupId", -1) } initRecyclerView() upRecyclerData() - observeEvent(Bus.UP_BOOK) { + observeEvent(EventBus.UP_BOOK) { booksAdapter.notification(it) } - postEvent(Bus.UP_TABS, getPrefInt(PreferKey.saveTabPosition, 0)) } private fun initRecyclerView() { @@ -76,6 +78,21 @@ class BooksFragment : BaseFragment(R.layout.fragment_books), booksAdapter = BooksAdapterGrid(requireContext(),this) } rv_bookshelf.adapter = booksAdapter + booksAdapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0) { + rv_bookshelf.scrollToPosition(0) + } + } + + override fun onItemRangeMoved(fromPosition: Int, toPosition: Int, itemCount: Int) { + super.onItemRangeMoved(fromPosition, toPosition, itemCount) + if (toPosition == 0) { + rv_bookshelf.scrollToPosition(0) + } + } + }) } private fun upRecyclerData() { @@ -87,15 +104,9 @@ class BooksFragment : BaseFragment(R.layout.fragment_books), else -> App.db.bookDao().observeByGroup(groupId) } bookshelfLiveData?.observe(this, Observer { - val diffResult = - DiffUtil.calculateDiff( - BooksDiffCallBack( - booksAdapter.getItems(), - it - ) - ) - booksAdapter.setItems(it, false) - diffResult.dispatchUpdatesTo(booksAdapter) + val diffResult = DiffUtil + .calculateDiff(BooksDiffCallBack(ArrayList(booksAdapter.getItems()), it)) + booksAdapter.setItems(it, diffResult) }) } diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt index 144fa4e19..3ceeca8b6 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreAdapter.kt @@ -2,8 +2,9 @@ package io.legado.app.ui.main.explore import android.content.Context import android.view.LayoutInflater -import android.view.Menu +import android.view.View import android.widget.PopupMenu +import io.legado.app.App import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -13,8 +14,8 @@ import io.legado.app.lib.theme.accentColor import io.legado.app.utils.ACache import io.legado.app.utils.gone import io.legado.app.utils.visible +import kotlinx.android.synthetic.main.item_fillet_text.view.* import kotlinx.android.synthetic.main.item_find_book.view.* -import kotlinx.android.synthetic.main.item_text.view.* import kotlinx.coroutines.CoroutineScope import org.jetbrains.anko.sdk27.listeners.onClick import org.jetbrains.anko.sdk27.listeners.onLongClick @@ -22,46 +23,21 @@ import org.jetbrains.anko.sdk27.listeners.onLongClick class ExploreAdapter(context: Context, private val scope: CoroutineScope, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_find_book) { - private var exIndex = 0 + private var scrollTo = -1 override fun convert(holder: ItemViewHolder, item: BookSource, payloads: MutableList) { with(holder.itemView) { if (payloads.isEmpty()) { tv_name.text = item.bookSourceName - ll_title.onClick { - val oldEx = exIndex - exIndex = if (exIndex == holder.layoutPosition) -1 else holder.layoutPosition - notifyItemChanged(oldEx, false) - if (exIndex != -1) { - notifyItemChanged(holder.layoutPosition, false) - } - callBack.scrollTo(holder.layoutPosition) - } - ll_title.onLongClick { - val popupMenu = PopupMenu(context, ll_title) - popupMenu.menu.add(Menu.NONE, R.id.menu_edit, Menu.NONE, R.string.edit) - popupMenu.menu.add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top) - popupMenu.menu.add(Menu.NONE, R.id.menu_refresh, Menu.NONE, R.string.refresh) - popupMenu.setOnMenuItemClickListener { - when (it.itemId) { - R.id.menu_edit -> callBack.editSource(item.bookSourceUrl) - R.id.menu_top -> callBack.toTop(item) - R.id.menu_refresh -> { - ACache.get(context, "explore").remove(item.bookSourceUrl) - notifyItemChanged(holder.layoutPosition) - } - } - true - } - popupMenu.show() - true - } } if (exIndex == holder.layoutPosition) { iv_status.setImageResource(R.drawable.ic_remove) rotate_loading.loadingColor = context.accentColor rotate_loading.show() + if (scrollTo >= 0) { + callBack.scrollTo(scrollTo) + } Coroutine.async(scope) { item.getExploreKinds() }.onSuccess { kindList -> @@ -70,7 +46,7 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca gl_child.removeAllViews() kindList.map { kind -> val tv = LayoutInflater.from(context) - .inflate(R.layout.item_text, gl_child, false) + .inflate(R.layout.item_fillet_text, gl_child, false) gl_child.addView(tv) tv.text_view.text = kind.title tv.text_view.onClick { @@ -86,6 +62,10 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca } }.onFinally { rotate_loading.hide() + if (scrollTo >= 0) { + callBack.scrollTo(scrollTo) + scrollTo = -1 + } } } else { iv_status.setImageResource(R.drawable.ic_add) @@ -95,6 +75,47 @@ class ExploreAdapter(context: Context, private val scope: CoroutineScope, val ca } } + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + ll_title.onClick { + val position = holder.layoutPosition + val oldEx = exIndex + exIndex = if (exIndex == position) -1 else position + notifyItemChanged(oldEx, false) + if (exIndex != -1) { + scrollTo = position + callBack.scrollTo(position) + notifyItemChanged(position, false) + } + } + ll_title.onLongClick { + showMenu(ll_title, holder.layoutPosition) + } + } + } + + private fun showMenu(view: View, position: Int): Boolean { + val source = getItem(position) ?: return true + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.explore_item) + popupMenu.setOnMenuItemClickListener { + when (it.itemId) { + R.id.menu_edit -> callBack.editSource(source.bookSourceUrl) + R.id.menu_top -> callBack.toTop(source) + R.id.menu_refresh -> { + ACache.get(context, "explore").remove(source.bookSourceUrl) + notifyItemChanged(position) + } + R.id.menu_del -> Coroutine.async(scope) { + App.db.bookSourceDao().delete(source) + } + } + true + } + popupMenu.show() + return true + } + interface CallBack { fun scrollTo(pos: Int) fun openExplore(sourceUrl: String, title: String, exploreUrl: String) diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffCallBack.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffCallBack.kt new file mode 100644 index 000000000..7f3c7d020 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreDiffCallBack.kt @@ -0,0 +1,34 @@ +package io.legado.app.ui.main.explore + +import androidx.recyclerview.widget.DiffUtil +import io.legado.app.data.entities.BookSource + + +class ExploreDiffCallBack( + private val oldItems: List, + private val newItems: List +) : + DiffUtil.Callback() { + + override fun getOldListSize(): Int { + return oldItems.size + } + + override fun getNewListSize(): Int { + return newItems.size + } + + override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + return true + } + + override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { + val oldItem = oldItems[oldItemPosition] + val newItem = newItems[newItemPosition] + if (oldItem.bookSourceName != newItem.bookSourceName) { + return false + } + return true + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt index ea23c0909..78cdd141a 100644 --- a/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt +++ b/app/src/main/java/io/legado/app/ui/main/explore/ExploreFragment.kt @@ -1,11 +1,16 @@ package io.legado.app.ui.main.explore import android.os.Bundle +import android.view.Menu +import android.view.MenuItem +import android.view.SubMenu import android.view.View import androidx.appcompat.widget.SearchView import androidx.lifecycle.LiveData import androidx.lifecycle.Observer +import androidx.recyclerview.widget.DiffUtil 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.VMBaseFragment @@ -15,6 +20,7 @@ import io.legado.app.lib.theme.primaryTextColor import io.legado.app.ui.book.source.edit.BookSourceEditActivity import io.legado.app.ui.explore.ExploreShowActivity import io.legado.app.utils.getViewModel +import io.legado.app.utils.splitNotBlank import io.legado.app.utils.startActivity import kotlinx.android.synthetic.main.fragment_find_book.* import kotlinx.android.synthetic.main.view_search.* @@ -28,13 +34,24 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ private lateinit var adapter: ExploreAdapter private lateinit var linearLayoutManager: LinearLayoutManager + private val groups = linkedSetOf() + private var liveGroup: LiveData>? = null private var liveExplore: LiveData>? = null + private var groupsMenu: SubMenu? = null - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) initSearchView() initRecyclerView() - initData() + initGroupData() + initExploreData() + } + + override fun onCompatCreateOptionsMenu(menu: Menu) { + super.onCompatCreateOptionsMenu(menu) + menuInflater.inflate(R.menu.main_explore, menu) + groupsMenu = menu.findItem(R.id.menu_group)?.subMenu + upGroupsMenu() } private fun initSearchView() { @@ -49,7 +66,7 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ } override fun onQueryTextChange(newText: String?): Boolean { - initData(newText) + initExploreData(newText) return false } }) @@ -61,9 +78,30 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ rv_find.layoutManager = linearLayoutManager adapter = ExploreAdapter(requireContext(), this, this) rv_find.adapter = adapter + adapter.registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + + override fun onItemRangeInserted(positionStart: Int, itemCount: Int) { + super.onItemRangeInserted(positionStart, itemCount) + if (positionStart == 0) { + rv_find.scrollToPosition(0) + } + } + }) + } + + private fun initGroupData() { + liveGroup?.removeObservers(viewLifecycleOwner) + liveGroup = App.db.bookSourceDao().liveGroupExplore() + liveGroup?.observe(viewLifecycleOwner, Observer { + groups.clear() + it.map { group -> + groups.addAll(group.splitNotBlank(",", ";")) + } + upGroupsMenu() + }) } - private fun initData(key: String? = null) { + private fun initExploreData(key: String? = null) { liveExplore?.removeObservers(viewLifecycleOwner) liveExplore = if (key.isNullOrBlank()) { App.db.bookSourceDao().liveExplore() @@ -71,12 +109,31 @@ class ExploreFragment : VMBaseFragment(R.layout.fragment_find_ App.db.bookSourceDao().liveExplore("%$key%") } liveExplore?.observe(viewLifecycleOwner, Observer { + val diffResult = DiffUtil + .calculateDiff(ExploreDiffCallBack(ArrayList(adapter.getItems()), it)) adapter.setItems(it) + diffResult.dispatchUpdatesTo(adapter) }) } + private fun upGroupsMenu() { + groupsMenu?.let { subMenu -> + subMenu.removeGroup(R.id.menu_group_text) + groups.forEach { + subMenu.add(R.id.menu_group_text, Menu.NONE, Menu.NONE, it) + } + } + } + + override fun onCompatOptionsItemSelected(item: MenuItem) { + super.onCompatOptionsItemSelected(item) + if (item.groupId == R.id.menu_group_text) { + search_view.setQuery(item.title, true) + } + } + override fun scrollTo(pos: Int) { - rv_find.smoothScrollToPosition(pos) + (rv_find.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(pos, 0) } override fun openExplore(sourceUrl: String, title: String, exploreUrl: String) { 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 6d79f55d4..de1f63ae1 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 @@ -1,51 +1,42 @@ package io.legado.app.ui.main.my -import android.app.Activity import android.content.Intent import android.content.SharedPreferences -import android.net.Uri import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.View -import androidx.documentfile.provider.DocumentFile import androidx.preference.Preference import androidx.preference.PreferenceFragmentCompat -import androidx.preference.SwitchPreference import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseFragment -import io.legado.app.constant.Bus +import io.legado.app.constant.EventBus import io.legado.app.constant.PreferKey -import io.legado.app.help.BookHelp -import io.legado.app.help.permission.Permissions -import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.help.storage.Backup -import io.legado.app.help.storage.Restore -import io.legado.app.help.storage.WebDavHelp import io.legado.app.lib.theme.ATH import io.legado.app.service.WebService import io.legado.app.ui.about.AboutActivity import io.legado.app.ui.about.DonateActivity import io.legado.app.ui.book.source.manage.BookSourceActivity +import io.legado.app.ui.config.BackupRestoreUi import io.legado.app.ui.config.ConfigActivity import io.legado.app.ui.config.ConfigViewModel +import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.replacerule.ReplaceRuleActivity +import io.legado.app.ui.widget.prefs.SwitchPreference import io.legado.app.utils.* import kotlinx.android.synthetic.main.view_title_bar.* -import kotlinx.coroutines.launch import org.jetbrains.anko.startActivity -class MyFragment : BaseFragment(R.layout.fragment_my_config) { - private val backupSelectRequestCode = 22 - private val restoreSelectRequestCode = 33 +class MyFragment : BaseFragment(R.layout.fragment_my_config), FileChooserDialog.CallBack { - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) val fragmentTag = "prefFragment" var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag) if (preferenceFragment == null) preferenceFragment = PreferenceFragment() - childFragmentManager.beginTransaction().replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit() + childFragmentManager.beginTransaction() + .replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit() } override fun onCompatCreateOptionsMenu(menu: Menu) { @@ -55,115 +46,18 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { override fun onCompatOptionsItemSelected(item: MenuItem) { when (item.itemId) { R.id.menu_help -> startActivity() - R.id.menu_backup -> backup() - R.id.menu_restore -> restore() + R.id.menu_backup -> BackupRestoreUi.backup(this) + R.id.menu_restore -> BackupRestoreUi.restore(this) } } - - private fun backup() { - val backupPath = getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(requireContext(), uri) - if (doc?.canWrite() == true) { - launch { - Backup.backup(requireContext(), uri) - } - } else { - selectBackupFolder() - } - } else { - selectBackupFolder() - } - } - - private fun selectBackupFolder() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, backupSelectRequestCode) - } catch (e: java.lang.Exception) { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - launch { - Backup.backup(requireContext(), null) - } - } - .request() - } - } - - fun restore() { - launch { - if (!WebDavHelp.showRestoreDialog(requireContext())) { - val backupPath = getPrefString(PreferKey.backupPath) - if (backupPath?.isNotEmpty() == true) { - val uri = Uri.parse(backupPath) - val doc = DocumentFile.fromTreeUri(requireContext(), uri) - if (doc?.canWrite() == true) { - Restore.restore(requireContext(), uri) - toast(R.string.restore_success) - } else { - selectBackupFolder() - } - } else { - selectRestoreFolder() - } - } - } - } - - private fun selectRestoreFolder() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, restoreSelectRequestCode) - } catch (e: java.lang.Exception) { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { - launch { - Restore.restore(Backup.legadoPath) - toast(R.string.restore_success) - } - } - .request() - } + override fun onFilePicked(requestCode: Int, currentPath: String) { + BackupRestoreUi.onFilePicked(requestCode, currentPath) } override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - backupSelectRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - requireContext().contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - putPrefString(PreferKey.backupPath, uri.toString()) - launch { - Backup.backup(requireContext(), uri) - } - } - } - restoreSelectRequestCode -> if (resultCode == Activity.RESULT_OK) { - data?.data?.let { uri -> - requireContext().contentResolver.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION - ) - putPrefString(PreferKey.backupPath, uri.toString()) - launch { - Restore.restore(requireContext(), uri) - toast(R.string.restore_success) - } - } - } - } + BackupRestoreUi.onActivityResult(requestCode, resultCode, data) } class PreferenceFragment : PreferenceFragmentCompat(), @@ -171,15 +65,14 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { if (WebService.isRun) { - putPrefBoolean("webService", true) + putPrefBoolean(PreferKey.webService, true) } else { - putPrefBoolean("webService", false) + putPrefBoolean(PreferKey.webService, false) } addPreferencesFromResource(R.xml.pref_main) - observeEvent(Bus.WEB_SERVICE_STOP) { - findPreference("webService")?.let { - it.isChecked = false - } + val webServicePre = findPreference(PreferKey.webService) + observeEvent(EventBus.WEB_SERVICE_STOP) { + webServicePre?.isChecked = false } } @@ -198,20 +91,22 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) { super.onPause() } - override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + override fun onSharedPreferenceChanged( + sharedPreferences: SharedPreferences?, + key: String? + ) { when (key) { - "isNightTheme" -> App.INSTANCE.applyDayNight() - "webService" -> { + PreferKey.themeMode -> App.INSTANCE.applyDayNight() + PreferKey.webService -> { if (requireContext().getPrefBoolean("webService")) { WebService.start(requireContext()) - toast("正在启动服务\n具体信息查看通知栏") - }else{ + toast(R.string.service_start) + } else { WebService.stop(requireContext()) - toast("服务已停止") + toast(R.string.service_stop) } } "recordLog" -> LogUtils.upLevel() - "downloadPath" -> BookHelp.upDownloadPath() } } 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 63bec82e2..d58f7587e 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 @@ -20,7 +20,14 @@ class RssAdapter(context: Context, val callBack: CallBack) : .placeholder(R.drawable.image_rss) .error(R.drawable.image_rss) .into(iv_icon) - onClick { callBack.openRss(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.openRss(it) + } } } 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 0ec49f315..b960041d8 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 @@ -13,6 +13,7 @@ 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.favorites.RssFavoritesActivity import io.legado.app.ui.rss.source.manage.RssSourceActivity import io.legado.app.utils.getViewModelOfActivity import io.legado.app.utils.startActivity @@ -24,7 +25,7 @@ class RssFragment : BaseFragment(R.layout.fragment_rss), private lateinit var adapter: RssAdapter - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) { setSupportToolbar(toolbar) initRecyclerView() initData() @@ -38,8 +39,7 @@ class RssFragment : BaseFragment(R.layout.fragment_rss), super.onCompatOptionsItemSelected(item) when (item.itemId) { R.id.menu_rss_config -> startActivity() - R.id.menu_rss_star -> { - } + R.id.menu_rss_star -> startActivity() } } diff --git a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt index b84cc3f6f..bfd4d23a1 100644 --- a/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt +++ b/app/src/main/java/io/legado/app/ui/qrcode/QrCodeActivity.kt @@ -11,7 +11,7 @@ import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.utils.FileUtils +import io.legado.app.utils.RealPathUtil import kotlinx.android.synthetic.main.activity_qrcode_capture.* import kotlinx.android.synthetic.main.view_title_bar.* import org.jetbrains.anko.toast @@ -98,7 +98,7 @@ class QrCodeActivity : BaseActivity(R.layout.activity_qrcode_capture), QRCodeVie zxingview.startSpotAndShowRect() // 显示扫描框,并开始识别 if (resultCode == Activity.RESULT_OK && requestCode == requestQrImage) { - val picturePath = FileUtils.getPath(this, it) + val picturePath = RealPathUtil.getPath(this, it) // 本来就用到 QRCodeView 时可直接调 QRCodeView 的方法,走通用的回调 zxingview.decodeQRCode(picturePath) } diff --git a/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt b/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt index 1824313b3..62fe0cec3 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/DiffCallBack.kt @@ -1,5 +1,6 @@ package io.legado.app.ui.replacerule +import android.os.Bundle import androidx.recyclerview.widget.DiffUtil import io.legado.app.data.entities.ReplaceRule @@ -24,19 +25,34 @@ class DiffCallBack( override fun areContentsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return oldItem.name == newItem.name - && oldItem.group == newItem.group - && oldItem.isEnabled == newItem.isEnabled + if (oldItem.name != newItem.name) { + return false + } + if (oldItem.group != newItem.group) { + return false + } + if (oldItem.isEnabled != newItem.isEnabled) { + return false + } + return true } override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? { val oldItem = oldItems[oldItemPosition] val newItem = newItems[newItemPosition] - return when { - oldItem.name == newItem.name - && oldItem.group == newItem.group - && oldItem.isEnabled != newItem.isEnabled -> 2 - else -> null + val payload = Bundle() + if (oldItem.name != newItem.name) { + payload.putString("name", newItem.name) + } + if (oldItem.group != newItem.group) { + payload.putString("group", newItem.group) + } + if (oldItem.isEnabled != newItem.isEnabled) { + payload.putBoolean("enabled", newItem.isEnabled) + } + if (payload.isEmpty) { + return null } + return payload } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt index 2dffe8b8d..6f9a61f4c 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/GroupManageDialog.kt @@ -12,9 +12,7 @@ import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration 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.adapter.ItemViewHolder @@ -24,10 +22,7 @@ import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.requestInputMethod -import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.item_group_manage.view.* @@ -65,9 +60,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter App.db.replaceRuleDao().liveGroup().observe(viewLifecycleOwner, Observer { val groups = linkedSetOf() @@ -132,8 +125,20 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { with(holder.itemView) { tv_group.text = item - tv_edit.onClick { editGroup(item) } - tv_del.onClick { viewModel.delGroup(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + tv_edit.onClick { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + + tv_del.onClick { + getItem(holder.layoutPosition)?.let { viewModel.delGroup(it) } + } } } } diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt index 5c53e91e1..9474ad4d1 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleActivity.kt @@ -7,12 +7,11 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.SubMenu +import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView -import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -23,15 +22,13 @@ import io.legado.app.data.entities.ReplaceRule import io.legado.app.help.ItemTouchCallback 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.cancelButton -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.dialogs.* import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.lib.theme.view.ATEAutoCompleteTextView import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.replacerule.edit.ReplaceEditDialog +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_replace_rule.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* @@ -43,10 +40,12 @@ import java.io.FileNotFoundException class ReplaceRuleActivity : VMBaseActivity(R.layout.activity_replace_rule), SearchView.OnQueryTextListener, + PopupMenu.OnMenuItemClickListener, FileChooserDialog.CallBack, ReplaceRuleAdapter.CallBack { override val viewModel: ReplaceRuleViewModel get() = getViewModel(ReplaceRuleViewModel::class.java) + private val importRecordKey = "replaceRuleRecordKey" private val importSource = 132 private lateinit var adapter: ReplaceRuleAdapter private var groups = hashSetOf() @@ -57,6 +56,7 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi override fun onActivityCreated(savedInstanceState: Bundle?) { initRecyclerView() initSearchView() + initSelectActionView() observeReplaceRuleData() observeGroupData() } @@ -72,35 +72,12 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi return super.onPrepareOptionsMenu(menu) } - override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { - R.id.menu_add_replace_rule -> - ReplaceEditDialog().show(supportFragmentManager, "replaceNew") - R.id.menu_group_manage -> - GroupManageDialog().show(supportFragmentManager, "groupManage") - R.id.menu_select_all -> adapter.selectAll() - R.id.menu_revert_selection -> adapter.revertSelection() - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds()) - R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds()) - R.id.menu_import_source_onLine -> showImportDialog() - R.id.menu_import_source_local -> selectFileSys() - R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelectionIds()) - } - return super.onCompatOptionsItemSelected(item) - } - private fun initRecyclerView() { ATH.applyEdgeEffectColor(recycler_view) recycler_view.layoutManager = LinearLayoutManager(this) adapter = ReplaceRuleAdapter(this, this) recycler_view.adapter = adapter - recycler_view.addItemDecoration( - DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { - ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let { - this.setDrawable(it) - } - }) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) val itemTouchCallback = ItemTouchCallback() itemTouchCallback.onItemTouchCallbackListener = adapter itemTouchCallback.isCanDrag = true @@ -115,6 +92,34 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi search_view.setOnQueryTextListener(this) } + private fun initSelectActionView() { + select_action_bar.setMainActionText(R.string.delete) + select_action_bar.inflateMenu(R.menu.replace_rule_sel) + select_action_bar.setOnMenuItemClickListener(this) + select_action_bar.setCallBack(object : SelectActionBar.CallBack { + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + this@ReplaceRuleActivity + .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.getSelection()) } + noButton { } + } + .show().applyTint() + } + }) + } + private fun observeReplaceRuleData(key: String? = null) { replaceRuleLiveData?.removeObservers(this) dataInit = false @@ -127,10 +132,11 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi if (dataInit) { setResult(Activity.RESULT_OK) } - val diffResult = DiffUtil.calculateDiff(DiffCallBack(adapter.getItems(), it)) - adapter.setItems(it, false) - diffResult.dispatchUpdatesTo(adapter) + val diffResult = + DiffUtil.calculateDiff(DiffCallBack(ArrayList(adapter.getItems()), it)) + adapter.setItems(it, diffResult) dataInit = true + upCountView() }) } @@ -144,6 +150,29 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi }) } + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.menu_add_replace_rule -> + ReplaceEditDialog().show(supportFragmentManager, "replaceNew") + R.id.menu_group_manage -> + GroupManageDialog().show(supportFragmentManager, "groupManage") + + R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) + R.id.menu_import_source_onLine -> showImportDialog() + R.id.menu_import_source_local -> selectFileSys() + } + return super.onCompatOptionsItemSelected(item) + } + + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) + R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelection()) + } + return false + } + private fun upGroupMenu() { groupMenu?.removeGroup(R.id.source_group) groups.map { @@ -155,17 +184,17 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi private fun showImportDialog() { val aCache = ACache.get(this, cacheDir = false) val cacheUrls: MutableList = aCache - .getAsString("replaceRuleUrl") + .getAsString(importRecordKey) ?.splitNotBlank(",") ?.toMutableList() ?: mutableListOf() alert(titleResource = R.string.import_replace_rule_on_line) { - var editText: ATEAutoCompleteTextView? = null + var editText: AutoCompleteTextView? = null customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view edit_view.setFilterValues(cacheUrls) { cacheUrls.remove(it) - aCache.put("replaceRuleUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } } } @@ -174,7 +203,7 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) - aCache.put("replaceRuleUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() viewModel.importSource(it) { msg -> @@ -264,6 +293,10 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi } } + override fun upCountView() { + select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) + } + override fun update(vararg rule: ReplaceRule) { viewModel.update(*rule) } @@ -273,9 +306,7 @@ class ReplaceRuleActivity : VMBaseActivity(R.layout.activi } override fun edit(rule: ReplaceRule) { - ReplaceEditDialog - .newInstance(rule.id) - .show(supportFragmentManager, "editReplace") + ReplaceEditDialog.show(supportFragmentManager, rule.id) } override fun toTop(rule: ReplaceRule) { diff --git a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt index 49f01a828..dd51d31d0 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/ReplaceRuleAdapter.kt @@ -1,8 +1,11 @@ package io.legado.app.ui.replacerule import android.content.Context -import android.view.Menu +import android.os.Bundle +import android.view.View import android.widget.PopupMenu +import androidx.core.os.bundleOf +import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -11,37 +14,40 @@ import io.legado.app.help.ItemTouchCallback import io.legado.app.lib.theme.backgroundColor import kotlinx.android.synthetic.main.item_replace_rule.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_replace_rule), ItemTouchCallback.OnItemTouchCallbackListener { - private val selectedIds = linkedSetOf() + private val selected = linkedSetOf() fun selectAll() { getItems().forEach { - selectedIds.add(it.id) + selected.add(it) } - notifyItemRangeChanged(0, itemCount, 1) + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() } fun revertSelection() { getItems().forEach { - if (selectedIds.contains(it.id)) { - selectedIds.remove(it.id) + if (selected.contains(it)) { + selected.remove(it) } else { - selectedIds.add(it.id) + selected.add(it) } } - notifyItemRangeChanged(0, itemCount, 1) + notifyItemRangeChanged(0, itemCount, bundleOf(Pair("selected", null))) + callBack.upCountView() } - fun getSelectionIds(): LinkedHashSet { - val selection = linkedSetOf() + fun getSelection(): LinkedHashSet { + val selection = linkedSetOf() getItems().map { - if (selectedIds.contains(it.id)) { - selection.add(it.id) + if (selected.contains(it)) { + selection.add(it) } } return selection @@ -49,7 +55,8 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : override fun convert(holder: ItemViewHolder, item: ReplaceRule, payloads: MutableList) { with(holder.itemView) { - if (payloads.isEmpty()) { + val bundle = payloads.getOrNull(0) as? Bundle + if (bundle == null) { this.setBackgroundColor(context.backgroundColor) if (item.group.isNullOrEmpty()) { cb_name.text = item.name @@ -58,43 +65,68 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : String.format("%s (%s)", item.name, item.group) } swt_enabled.isChecked = item.isEnabled - swt_enabled.onClick { - item.isEnabled = swt_enabled.isChecked - callBack.update(item) + cb_name.isChecked = selected.contains(item) + } else { + bundle.keySet().map { + when (it) { + "selected" -> cb_name.isChecked = selected.contains(item) + "name", "group" -> + if (item.group.isNullOrEmpty()) { + cb_name.text = item.name + } else { + cb_name.text = + String.format("%s (%s)", item.name, item.group) + } + "enabled" -> swt_enabled.isChecked = item.isEnabled + } + } + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + swt_enabled.setOnCheckedChangeListener { _, isChecked -> + getItem(holder.layoutPosition)?.let { + it.isEnabled = isChecked + callBack.update(it) } - iv_edit.onClick { - callBack.edit(item) + } + iv_edit.onClick { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) } - cb_name.isChecked = selectedIds.contains(item.id) - cb_name.onClick { + } + cb_name.onClick { + getItem(holder.layoutPosition)?.let { if (cb_name.isChecked) { - selectedIds.add(item.id) + selected.add(it) } else { - selectedIds.remove(item.id) + selected.remove(it) } } - iv_menu_more.onClick { - val popupMenu = PopupMenu(context, it) - popupMenu.menu.add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top) - popupMenu.menu.add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete) - popupMenu.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.menu_top -> callBack.toTop(item) - R.id.menu_del -> callBack.delete(item) - } - true - } - popupMenu.show() - } - } else { - when (payloads[0]) { - 1 -> cb_name.isChecked = selectedIds.contains(item.id) - 2 -> swt_enabled.isChecked = item.isEnabled - } + callBack.upCountView() + } + iv_menu_more.onClick { + showMenu(iv_menu_more, holder.layoutPosition) } } } + private fun showMenu(view: View, position: Int) { + val item = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.replace_rule_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(item) + R.id.menu_del -> callBack.delete(item) + } + true + } + popupMenu.show() + } + override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { val srcItem = getItem(srcPosition) val targetItem = getItem(targetPosition) @@ -105,14 +137,22 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : val srcOrder = srcItem.order srcItem.order = targetItem.order targetItem.order = srcOrder - callBack.update(srcItem, targetItem) + movedItems.add(srcItem) + movedItems.add(targetItem) } } + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) return true } - override fun onSwiped(adapterPosition: Int) { + private val movedItems = linkedSetOf() + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } } interface CallBack { @@ -121,5 +161,6 @@ class ReplaceRuleAdapter(context: Context, var callBack: CallBack) : fun edit(rule: ReplaceRule) fun toTop(rule: ReplaceRule) fun upOrder() + fun upCountView() } } 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 ba6bc2a61..42217ec18 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 @@ -6,10 +6,12 @@ import io.legado.app.App import io.legado.app.R import io.legado.app.base.BaseViewModel import io.legado.app.data.entities.ReplaceRule -import io.legado.app.help.FileHelp +import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.Backup -import io.legado.app.help.storage.Restore +import io.legado.app.help.storage.ImportOldData +import io.legado.app.utils.FileUtils import io.legado.app.utils.GSON +import io.legado.app.utils.isAbsUrl import io.legado.app.utils.splitNotBlank import org.jetbrains.anko.toast import java.io.File @@ -18,7 +20,13 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application fun importSource(text: String, showMsg: (msg: String) -> Unit) { execute { - Restore.importOldReplaceRule(text) + if (text.isAbsUrl()) { + HttpHelper.simpleGet(text)?.let { + ImportOldData.importOldReplaceRule(it) + } + } else { + ImportOldData.importOldReplaceRule(text) + } }.onError { showMsg(it.localizedMessage ?: "ERROR") }.onSuccess { @@ -48,41 +56,45 @@ class ReplaceRuleViewModel(application: Application) : BaseViewModel(application fun upOrder() { execute { val rules = App.db.replaceRuleDao().all - for ((index: Int, rule: ReplaceRule) in rules.withIndex()) { + for ((index, rule) in rules.withIndex()) { rule.order = index + 1 } App.db.replaceRuleDao().update(*rules.toTypedArray()) } } - fun enableSelection(ids: LinkedHashSet) { + fun enableSelection(rules: LinkedHashSet) { execute { - App.db.replaceRuleDao().enableSection(*ids.toLongArray()) + val list = arrayListOf() + rules.forEach { + list.add(it.copy(isEnabled = true)) + } + App.db.replaceRuleDao().update(*list.toTypedArray()) } } - fun disableSelection(ids: LinkedHashSet) { + fun disableSelection(rules: LinkedHashSet) { execute { - App.db.replaceRuleDao().disableSection(*ids.toLongArray()) + val list = arrayListOf() + rules.forEach { + list.add(it.copy(isEnabled = false)) + } + App.db.replaceRuleDao().update(*list.toTypedArray()) } } - fun delSelection(ids: LinkedHashSet) { + fun delSelection(rules: LinkedHashSet) { execute { - App.db.replaceRuleDao().delSection(*ids.toLongArray()) + App.db.replaceRuleDao().delete(*rules.toTypedArray()) } } - fun exportSelection(ids: LinkedHashSet) { + fun exportSelection(rules: LinkedHashSet) { execute { - ids.map { - App.db.replaceRuleDao().findById(it) - }.let { - val json = GSON.toJson(it) - val file = - FileHelp.getFile(Backup.exportPath + File.separator + "exportReplaceRule.json") - file.writeText(json) - } + val json = GSON.toJson(rules) + val file = + FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportReplaceRule.json") + file.writeText(json) }.onSuccess { context.toast("成功导出至\n${Backup.exportPath}") }.onError { diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt index 5470b4c5f..de5002931 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditDialog.kt @@ -9,6 +9,7 @@ import android.view.ViewGroup import android.view.ViewGroup.LayoutParams.WRAP_CONTENT import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager import androidx.lifecycle.Observer import io.legado.app.R import io.legado.app.constant.Theme @@ -22,14 +23,13 @@ class ReplaceEditDialog : DialogFragment(), companion object { - fun newInstance(id: Long? = null): ReplaceEditDialog { + fun show(fragmentManager: FragmentManager, id: Long = -1, pattern: String? = null) { val dialog = ReplaceEditDialog() - id?.let { - val bundle = Bundle() - bundle.putLong("id", id) - dialog.arguments = bundle - } - return dialog + val bundle = Bundle() + bundle.putLong("id", id) + bundle.putString("pattern", pattern) + dialog.arguments = bundle + dialog.show(fragmentManager, "editReplace") } } @@ -56,7 +56,7 @@ class ReplaceEditDialog : DialogFragment(), tool_bar.inflateMenu(R.menu.replace_edit) tool_bar.menu.applyTint(requireContext(), Theme.getTheme()) tool_bar.setOnMenuItemClickListener(this) - viewModel.replaceRuleData.observe(this, Observer { + viewModel.replaceRuleData.observe(viewLifecycleOwner, Observer { upReplaceView(it) }) arguments?.let { diff --git a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt index 281a0ac2d..f326c88c3 100644 --- a/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt +++ b/app/src/main/java/io/legado/app/ui/replacerule/edit/ReplaceEditViewModel.kt @@ -19,6 +19,12 @@ class ReplaceEditViewModel(application: Application) : BaseViewModel(application App.db.replaceRuleDao().findById(id)?.let { replaceRuleData.postValue(it) } + } else { + bundle.getString("pattern")?.let { pattern -> + replaceRuleData.postValue( + ReplaceRule(pattern = pattern) + ) + } } } } 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/RssArticlesActivity.kt index 4c042c579..5476657a4 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/RssArticlesActivity.kt @@ -3,10 +3,8 @@ package io.legado.app.ui.rss.article import android.os.Bundle import android.view.Menu import android.view.MenuItem -import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import io.legado.app.App @@ -16,7 +14,8 @@ 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.LoadMoreView +import io.legado.app.ui.widget.recycler.LoadMoreView +import io.legado.app.utils.getVerticalDivider import io.legado.app.utils.getViewModel import kotlinx.android.synthetic.main.activity_rss_artivles.* import kotlinx.android.synthetic.main.view_load_more.view.* @@ -71,12 +70,7 @@ class RssArticlesActivity : VMBaseActivity(R.layout.activi private fun initView() { ATH.applyEdgeEffectColor(recycler_view) recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration( - DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { - ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let { - this.setDrawable(it) - } - }) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) adapter = RssArticlesAdapter(this, this) recycler_view.adapter = adapter loadMoreView = LoadMoreView(this) diff --git a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt index 1c6e144fd..00b296359 100644 --- a/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/article/RssArticlesAdapter.kt @@ -25,9 +25,6 @@ class RssArticlesAdapter(context: Context, val callBack: CallBack) : with(holder.itemView) { tv_title.text = item.title tv_pub_date.text = item.pubDate - onClick { - callBack.readRss(item) - } if (item.image.isNullOrBlank()) { image_view.gone() } else { @@ -65,6 +62,14 @@ class RssArticlesAdapter(context: Context, val callBack: CallBack) : } } + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + interface CallBack { fun readRss(rssArticle: RssArticle) } 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 8a93301b9..42bc5c2d4 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 @@ -6,6 +6,7 @@ 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 @@ -101,8 +102,7 @@ class RssArticlesViewModel(application: Application) : BaseViewModel(application fun read(rssArticle: RssArticle) { execute { - rssArticle.read = true - App.db.rssArticleDao().update(rssArticle) + App.db.rssArticleDao().insertRecord(RssReadRecord(rssArticle.link)) } } diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt new file mode 100644 index 000000000..a8eb39cea --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesActivity.kt @@ -0,0 +1,52 @@ +package io.legado.app.ui.rss.favorites + +import android.os.Bundle +import androidx.lifecycle.LiveData +import androidx.lifecycle.Observer +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.App +import io.legado.app.R +import io.legado.app.base.BaseActivity +import io.legado.app.data.entities.RssStar +import io.legado.app.lib.theme.ATH +import io.legado.app.ui.rss.read.ReadRssActivity +import io.legado.app.utils.getVerticalDivider +import kotlinx.android.synthetic.main.view_refresh_recycler.* +import org.jetbrains.anko.startActivity + + +class RssFavoritesActivity : BaseActivity(R.layout.activity_rss_favorites), + RssFavoritesAdapter.CallBack { + + private var liveData: LiveData>? = null + private lateinit var adapter: RssFavoritesAdapter + + override fun onActivityCreated(savedInstanceState: Bundle?) { + initView() + initData() + } + + private fun initView() { + ATH.applyEdgeEffectColor(recycler_view) + recycler_view.layoutManager = LinearLayoutManager(this) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) + adapter = RssFavoritesAdapter(this, this) + recycler_view.adapter = adapter + } + + private fun initData() { + liveData?.removeObservers(this) + liveData = App.db.rssStarDao().liveAll() + liveData?.observe(this, Observer { + adapter.setItems(it) + }) + } + + override fun readRss(rssStar: RssStar) { + startActivity( + Pair("title", rssStar.title), + Pair("origin", rssStar.origin), + Pair("link", rssStar.link) + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt new file mode 100644 index 000000000..6077d5db9 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/rss/favorites/RssFavoritesAdapter.kt @@ -0,0 +1,69 @@ +package io.legado.app.ui.rss.favorites + +import android.content.Context +import android.graphics.drawable.Drawable +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.data.entities.RssStar +import io.legado.app.help.ImageLoader +import io.legado.app.utils.gone +import io.legado.app.utils.visible +import kotlinx.android.synthetic.main.item_rss_article.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class RssFavoritesAdapter(context: Context, val callBack: CallBack) : + SimpleRecyclerAdapter(context, R.layout.item_rss_article) { + + override fun convert(holder: ItemViewHolder, item: RssStar, payloads: MutableList) { + with(holder.itemView) { + tv_title.text = item.title + tv_pub_date.text = item.pubDate + if (item.image.isNullOrBlank()) { + image_view.gone() + } else { + ImageLoader.load(context, item.image) + .addListener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + image_view.gone() + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + image_view.visible() + return false + } + + }) + .into(image_view) + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.readRss(it) + } + } + } + + interface CallBack { + fun readRss(rssStar: RssStar) + } +} \ 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 a8ca32fdd..c68db66a1 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 @@ -40,15 +40,19 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { menuInflater.inflate(R.menu.rss_read, menu) - starMenuItem = menu.findItem(R.id.menu_rss_star) - ttsMenuItem = menu.findItem(R.id.menu_aloud) - upStarMenu() return super.onCompatCreateOptionsMenu(menu) } + override fun onPrepareOptionsMenu(menu: Menu?): Boolean { + starMenuItem = menu?.findItem(R.id.menu_rss_star) + ttsMenuItem = menu?.findItem(R.id.menu_aloud) + upStarMenu() + return super.onPrepareOptionsMenu(menu) + } + override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { - R.id.menu_rss_star -> viewModel.star() + R.id.menu_rss_star -> viewModel.favorite() R.id.menu_share_it -> viewModel.rssArticle?.let { shareText("链接分享", it.link) } @@ -73,9 +77,10 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r val url = NetworkUtils.getAbsoluteURL(it.origin, it.link) val html = viewModel.clHtml(content) if (viewModel.rssSource?.loadWithBaseUrl == true) { - webView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url) + webView.loadDataWithBaseURL(url, html, "text/html", "utf-8", url)//不想用baseUrl进else } else { - webView.loadData(html, "text/html", "utf-8") + //webView.loadData(html, "text/html;charset=utf-8", "utf-8")//经测试可以解决中文乱码 + webView.loadDataWithBaseURL(null, html, "text/html;charset=utf-8", "utf-8", url) } } }) @@ -95,10 +100,10 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r override fun upStarMenu() { if (viewModel.star) { starMenuItem?.setIcon(R.drawable.ic_star) - starMenuItem?.setTitle(R.string.y_store_up) + starMenuItem?.setTitle(R.string.in_favorites) } else { starMenuItem?.setIcon(R.drawable.ic_star_border) - starMenuItem?.setTitle(R.string.w_store_up) + starMenuItem?.setTitle(R.string.out_favorites) } DrawableUtils.setTint(starMenuItem?.icon, primaryTextColor) } @@ -149,9 +154,13 @@ class ReadRssActivity : VMBaseActivity(R.layout.activity_rss_r webView.settings.javaScriptEnabled = true webView.evaluateJavascript("document.documentElement.outerHTML") { val html = StringEscapeUtils.unescapeJson(it) - viewModel.readAloud(Jsoup.clean(html, Whitelist())) + val text = Jsoup.clean(html, Whitelist.none()) + .replace(Regex("""&\w+;"""), "") + .trim()//朗读过程中总是听到一些杂音,清理一下 + //longToast(需读内容)调试一下 + viewModel.readAloud(text) } } } -} \ No newline at end of file +} 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 5af6841e9..17b66f41c 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 @@ -70,7 +70,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } } - fun star() { + fun favorite() { execute { rssArticle?.let { if (star) { @@ -78,6 +78,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } else { App.db.rssStarDao().insert(it.toStar()) } + star = !star } }.onSuccess { callBack?.upStarMenu() @@ -85,7 +86,7 @@ class ReadRssViewModel(application: Application) : BaseViewModel(application), } fun clHtml(content: String): String { - return if (content.contains("$content - """ + + $content + """.trimIndent() } } diff --git a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt index a5e4eecbf..3d5262db4 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/debug/RssSourceDebugAdapter.kt @@ -26,4 +26,8 @@ class RssSourceDebugAdapter(context: Context) : text_view.text = item } } + + override fun registerListener(holder: ItemViewHolder) { + //nothing + } } \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/data/entities/EditEntity.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/EditEntity.kt similarity index 56% rename from app/src/main/java/io/legado/app/data/entities/EditEntity.kt rename to app/src/main/java/io/legado/app/ui/rss/source/edit/EditEntity.kt index d4c1ecc1a..f0f23b32b 100644 --- a/app/src/main/java/io/legado/app/data/entities/EditEntity.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/EditEntity.kt @@ -1,3 +1,3 @@ -package io.legado.app.data.entities +package io.legado.app.ui.rss.source.edit data class EditEntity(var key: String, var value: String?, var hint: Int) \ No newline at end of file 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 1ae70df5d..09839e458 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 @@ -16,7 +16,6 @@ import androidx.recyclerview.widget.LinearLayoutManager import io.legado.app.R import io.legado.app.base.VMBaseActivity import io.legado.app.constant.AppConst -import io.legado.app.data.entities.EditEntity import io.legado.app.data.entities.RssSource import io.legado.app.lib.dialogs.alert import io.legado.app.lib.theme.ATH @@ -193,18 +192,16 @@ class RssSourceEditActivity : } private fun showKeyboardTopPopupWindow() { - mSoftKeyboardTool?.isShowing?.let { if (it) return } - if (!isFinishing) { - mSoftKeyboardTool?.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + mSoftKeyboardTool?.let { + if (it.isShowing) return + if (!isFinishing) { + it.showAtLocation(ll_content, Gravity.BOTTOM, 0, 0) + } } } private fun closePopupWindow() { - mSoftKeyboardTool?.let { - if (it.isShowing) { - it.dismiss() - } - } + mSoftKeyboardTool?.dismiss() } private inner class KeyboardOnGlobalChangeListener : ViewTreeObserver.OnGlobalLayoutListener { diff --git a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt index 78e0740ef..b467c9b48 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/edit/RssSourceEditAdapter.kt @@ -7,7 +7,6 @@ import android.view.View import android.view.ViewGroup import androidx.recyclerview.widget.RecyclerView import io.legado.app.R -import io.legado.app.data.entities.EditEntity import kotlinx.android.synthetic.main.item_source_edit.view.* class RssSourceEditAdapter : RecyclerView.Adapter() { diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt index 32ce74791..1ba2a567b 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/GroupManageDialog.kt @@ -12,9 +12,7 @@ import android.widget.EditText import androidx.appcompat.widget.Toolbar import androidx.fragment.app.DialogFragment import androidx.lifecycle.Observer -import androidx.recyclerview.widget.DividerItemDecoration 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.adapter.ItemViewHolder @@ -24,10 +22,7 @@ import io.legado.app.lib.dialogs.alert import io.legado.app.lib.dialogs.customView import io.legado.app.lib.dialogs.noButton import io.legado.app.lib.dialogs.yesButton -import io.legado.app.utils.applyTint -import io.legado.app.utils.getViewModelOfActivity -import io.legado.app.utils.requestInputMethod -import io.legado.app.utils.splitNotBlank +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* import kotlinx.android.synthetic.main.dialog_recycler_view.* import kotlinx.android.synthetic.main.item_group_manage.view.* @@ -65,9 +60,7 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { tool_bar.setOnMenuItemClickListener(this) adapter = GroupAdapter(requireContext()) recycler_view.layoutManager = LinearLayoutManager(requireContext()) - recycler_view.addItemDecoration( - DividerItemDecoration(requireContext(), RecyclerView.VERTICAL) - ) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) recycler_view.adapter = adapter App.db.rssSourceDao().liveGroup().observe(viewLifecycleOwner, Observer { val groups = linkedSetOf() @@ -132,8 +125,22 @@ class GroupManageDialog : DialogFragment(), Toolbar.OnMenuItemClickListener { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { with(holder.itemView) { tv_group.text = item - tv_edit.onClick { editGroup(item) } - tv_del.onClick { viewModel.delGroup(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + tv_edit.onClick { + getItem(holder.layoutPosition)?.let { + editGroup(it) + } + } + + tv_del.onClick { + getItem(holder.layoutPosition)?.let { + viewModel.delGroup(it) + } + } } } } 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 b536a0b84..e83a15c19 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 @@ -7,12 +7,11 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import android.view.SubMenu +import androidx.appcompat.widget.PopupMenu import androidx.appcompat.widget.SearchView -import androidx.core.content.ContextCompat import androidx.lifecycle.LiveData import androidx.lifecycle.Observer import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.DividerItemDecoration import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.snackbar.Snackbar @@ -23,16 +22,14 @@ import io.legado.app.data.entities.RssSource import io.legado.app.help.ItemTouchCallback 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.cancelButton -import io.legado.app.lib.dialogs.customView -import io.legado.app.lib.dialogs.okButton +import io.legado.app.lib.dialogs.* import io.legado.app.lib.theme.ATH import io.legado.app.lib.theme.primaryTextColor -import io.legado.app.lib.theme.view.ATEAutoCompleteTextView import io.legado.app.ui.filechooser.FileChooserDialog import io.legado.app.ui.qrcode.QrCodeActivity import io.legado.app.ui.rss.source.edit.RssSourceEditActivity +import io.legado.app.ui.widget.SelectActionBar +import io.legado.app.ui.widget.text.AutoCompleteTextView import io.legado.app.utils.* import kotlinx.android.synthetic.main.activity_rss_source.* import kotlinx.android.synthetic.main.dialog_edit_text.view.* @@ -44,12 +41,13 @@ import java.io.FileNotFoundException class RssSourceActivity : VMBaseActivity(R.layout.activity_rss_source), + PopupMenu.OnMenuItemClickListener, FileChooserDialog.CallBack, RssSourceAdapter.CallBack { override val viewModel: RssSourceViewModel get() = getViewModel(RssSourceViewModel::class.java) - + private val importRecordKey = "rssSourceRecordKey" private val qrRequestCode = 101 private val importSource = 13141 private lateinit var adapter: RssSourceAdapter @@ -62,6 +60,7 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r initSearchView() initLiveDataGroup() initLiveDataSource() + initViewEvent() } override fun onCompatCreateOptionsMenu(menu: Menu): Boolean { @@ -78,12 +77,6 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r override fun onCompatOptionsItemSelected(item: MenuItem): Boolean { when (item.itemId) { R.id.menu_add -> startActivity() - R.id.menu_select_all -> adapter.selectAll() - R.id.menu_revert_selection -> adapter.revertSelection() - R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelectionIds()) - R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelectionIds()) - R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelectionIds()) - R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelectionIds()) R.id.menu_import_source_local -> selectFileSys() R.id.menu_import_source_onLine -> showImportDialog() R.id.menu_import_source_qr -> startActivityForResult(qrRequestCode) @@ -96,15 +89,22 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r return super.onCompatOptionsItemSelected(item) } + override fun onMenuItemClick(item: MenuItem?): Boolean { + when (item?.itemId) { + R.id.menu_enable_selection -> viewModel.enableSelection(adapter.getSelection()) + R.id.menu_disable_selection -> viewModel.disableSelection(adapter.getSelection()) + R.id.menu_del_selection -> viewModel.delSelection(adapter.getSelection()) + R.id.menu_export_selection -> viewModel.exportSelection(adapter.getSelection()) + R.id.menu_check_source -> { + } + } + return true + } + private fun initRecyclerView() { ATH.applyEdgeEffectColor(recycler_view) recycler_view.layoutManager = LinearLayoutManager(this) - recycler_view.addItemDecoration( - DividerItemDecoration(this, DividerItemDecoration.VERTICAL).apply { - ContextCompat.getDrawable(baseContext, R.drawable.ic_divider)?.let { - this.setDrawable(it) - } - }) + recycler_view.addItemDecoration(recycler_view.getVerticalDivider()) adapter = RssSourceAdapter(this, this) recycler_view.adapter = adapter val itemTouchCallback = ItemTouchCallback() @@ -140,6 +140,34 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r }) } + private fun initViewEvent() { + select_action_bar.setMainActionText(R.string.delete) + select_action_bar.inflateMenu(R.menu.rss_source_sel) + select_action_bar.setOnMenuItemClickListener(this) + select_action_bar.setCallBack(object : SelectActionBar.CallBack { + override fun selectAll(selectAll: Boolean) { + if (selectAll) { + adapter.selectAll() + } else { + adapter.revertSelection() + } + } + + override fun revertSelection() { + adapter.revertSelection() + } + + override fun onClickMainAction() { + this@RssSourceActivity + .alert(titleResource = R.string.draw, messageResource = R.string.sure_del) { + okButton { viewModel.delSelection(adapter.getSelection()) } + noButton { } + } + .show().applyTint() + } + }) + } + private fun upGroupMenu() { groupMenu?.removeGroup(R.id.source_group) groups.map { @@ -158,26 +186,30 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r sourceLiveData?.observe(this, Observer { val diffResult = DiffUtil .calculateDiff(DiffCallBack(adapter.getItems(), it)) - adapter.setItems(it, false) - diffResult.dispatchUpdatesTo(adapter) + adapter.setItems(it, diffResult) + upCountView() }) } + override fun upCountView() { + select_action_bar.upCountView(adapter.getSelection().size, adapter.getActualItemCount()) + } + @SuppressLint("InflateParams") private fun showImportDialog() { val aCache = ACache.get(this, cacheDir = false) val cacheUrls: MutableList = aCache - .getAsString("sourceUrl") + .getAsString(importRecordKey) ?.splitNotBlank(",") ?.toMutableList() ?: mutableListOf() alert(titleResource = R.string.import_book_source_on_line) { - var editText: ATEAutoCompleteTextView? = null + var editText: AutoCompleteTextView? = null customView { layoutInflater.inflate(R.layout.dialog_edit_text, null).apply { editText = edit_view edit_view.setFilterValues(cacheUrls) { cacheUrls.remove(it) - aCache.put("sourceUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } } } @@ -186,7 +218,7 @@ class RssSourceActivity : VMBaseActivity(R.layout.activity_r text?.let { if (!cacheUrls.contains(it)) { cacheUrls.add(0, it) - aCache.put("sourceUrl", cacheUrls.joinToString(",")) + aCache.put(importRecordKey, cacheUrls.joinToString(",")) } Snackbar.make(title_bar, R.string.importing, Snackbar.LENGTH_INDEFINITE).show() viewModel.importSource(it) { msg -> diff --git a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt index 708abb904..165d9bd0c 100644 --- a/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/rss/source/manage/RssSourceAdapter.kt @@ -1,8 +1,9 @@ package io.legado.app.ui.rss.source.manage import android.content.Context -import android.view.Menu +import android.view.View import android.widget.PopupMenu +import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter @@ -11,36 +12,39 @@ import io.legado.app.help.ItemTouchCallback import io.legado.app.lib.theme.backgroundColor import kotlinx.android.synthetic.main.item_rss_source.view.* import org.jetbrains.anko.sdk27.listeners.onClick +import java.util.* class RssSourceAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_rss_source), ItemTouchCallback.OnItemTouchCallbackListener { - private val selectedIds = linkedSetOf() + private val selected = linkedSetOf() fun selectAll() { getItems().forEach { - selectedIds.add(it.sourceUrl) + selected.add(it) } notifyItemRangeChanged(0, itemCount, 1) + callBack.upCountView() } fun revertSelection() { getItems().forEach { - if (selectedIds.contains(it.sourceUrl)) { - selectedIds.remove(it.sourceUrl) + if (selected.contains(it)) { + selected.remove(it) } else { - selectedIds.add(it.sourceUrl) + selected.add(it) } } notifyItemRangeChanged(0, itemCount, 1) + callBack.upCountView() } - fun getSelectionIds(): LinkedHashSet { - val selection = linkedSetOf() + fun getSelection(): LinkedHashSet { + val selection = linkedSetOf() getItems().forEach { - if (selectedIds.contains(it.sourceUrl)) { - selection.add(it.sourceUrl) + if (selected.contains(it)) { + selection.add(it) } } return selection @@ -57,44 +61,61 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : String.format("%s (%s)", item.sourceName, item.sourceGroup) } swt_enabled.isChecked = item.enabled - swt_enabled.onClick { - item.enabled = swt_enabled.isChecked - callBack.update(item) + cb_source.isChecked = selected.contains(item) + } else { + when (payloads[0]) { + 1 -> cb_source.isChecked = selected.contains(item) + 2 -> swt_enabled.isChecked = item.enabled } - cb_source.isChecked = selectedIds.contains(item.sourceUrl) - cb_source.setOnClickListener { - if (cb_source.isChecked) { - selectedIds.add(item.sourceUrl) - } else { - selectedIds.remove(item.sourceUrl) + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + swt_enabled.setOnCheckedChangeListener { view, checked -> + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + it.enabled = checked + callBack.update(it) } } - iv_edit.onClick { callBack.edit(item) } - iv_menu_more.onClick { - val popupMenu = PopupMenu(context, it) - popupMenu.menu.add(Menu.NONE, R.id.menu_top, Menu.NONE, R.string.to_top) - popupMenu.menu.add(Menu.NONE, R.id.menu_del, Menu.NONE, R.string.delete) - popupMenu.setOnMenuItemClickListener { menuItem -> - when (menuItem.itemId) { - R.id.menu_top -> callBack.toTop(item) - R.id.menu_del -> callBack.del(item) + } + cb_source.setOnCheckedChangeListener { view, checked -> + getItem(holder.layoutPosition)?.let { + if (view.isPressed) { + if (checked) { + selected.add(it) + } else { + selected.remove(it) } - true + callBack.upCountView() } - popupMenu.show() } - } else { - when (payloads[0]) { - 1 -> cb_source.isChecked = selectedIds.contains(item.sourceUrl) - 2 -> swt_enabled.isChecked = item.enabled + } + iv_edit.onClick { + getItem(holder.layoutPosition)?.let { + callBack.edit(it) } } + iv_menu_more.onClick { + showMenu(iv_menu_more, holder.layoutPosition) + } } } - - override fun onSwiped(adapterPosition: Int) { - + private fun showMenu(view: View, position: Int) { + val source = getItem(position) ?: return + val popupMenu = PopupMenu(context, view) + popupMenu.inflate(R.menu.rss_source_item) + popupMenu.setOnMenuItemClickListener { menuItem -> + when (menuItem.itemId) { + R.id.menu_top -> callBack.toTop(source) + R.id.menu_del -> callBack.del(source) + } + true + } + popupMenu.show() } override fun onMove(srcPosition: Int, targetPosition: Int): Boolean { @@ -107,17 +128,30 @@ class RssSourceAdapter(context: Context, val callBack: CallBack) : val srcOrder = srcItem.customOrder srcItem.customOrder = targetItem.customOrder targetItem.customOrder = srcOrder - callBack.update(srcItem, targetItem) + movedItems.add(srcItem) + movedItems.add(targetItem) } } + Collections.swap(getItems(), srcPosition, targetPosition) + notifyItemMoved(srcPosition, targetPosition) return true } + private val movedItems = hashSetOf() + + override fun clearView(recyclerView: RecyclerView, viewHolder: RecyclerView.ViewHolder) { + if (movedItems.isNotEmpty()) { + callBack.update(*movedItems.toTypedArray()) + movedItems.clear() + } + } + interface CallBack { fun del(source: RssSource) fun edit(source: RssSource) fun update(vararg source: RssSource) fun toTop(source: RssSource) fun upOrder() + fun upCountView() } } 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 c47bb8c9d..bb9e5f219 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 @@ -4,10 +4,9 @@ import android.app.Application import android.text.TextUtils import com.jayway.jsonpath.JsonPath import io.legado.app.App +import io.legado.app.R import io.legado.app.base.BaseViewModel -import io.legado.app.data.api.IHttpGetApi import io.legado.app.data.entities.RssSource -import io.legado.app.help.FileHelp import io.legado.app.help.http.HttpHelper import io.legado.app.help.storage.Backup import io.legado.app.help.storage.Restore.jsonPath @@ -42,32 +41,38 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } } - fun enableSelection(ids: LinkedHashSet) { + fun enableSelection(sources: LinkedHashSet) { execute { - App.db.rssSourceDao().enableSection(*ids.toTypedArray()) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabled = true)) + } + App.db.rssSourceDao().update(*list.toTypedArray()) } } - fun disableSelection(ids: LinkedHashSet) { + fun disableSelection(sources: LinkedHashSet) { execute { - App.db.rssSourceDao().disableSection(*ids.toTypedArray()) + val list = arrayListOf() + sources.forEach { + list.add(it.copy(enabled = false)) + } + App.db.rssSourceDao().update(*list.toTypedArray()) } } - fun delSelection(ids: LinkedHashSet) { + fun delSelection(sources: LinkedHashSet) { execute { - App.db.rssSourceDao().delSection(*ids.toTypedArray()) + App.db.rssSourceDao().delete(*sources.toTypedArray()) } } - fun exportSelection(ids: LinkedHashSet) { + fun exportSelection(sources: LinkedHashSet) { execute { - App.db.rssSourceDao().getRssSources(*ids.toTypedArray()).let { - val json = GSON.toJson(it) - val file = - FileHelp.getFile(Backup.exportPath + File.separator + "exportRssSource.json") - file.writeText(json) - } + val json = GSON.toJson(sources) + val file = + FileUtils.createFileIfNotExist(Backup.exportPath + File.separator + "exportRssSource.json") + file.writeText(json) }.onSuccess { context.toast("成功导出至\n${Backup.exportPath}") }.onError { @@ -123,6 +128,8 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) App.db.rssSourceDao().insert(*it.toTypedArray()) } } + }.onSuccess { + finally.invoke(context.getString(R.string.success)) } } @@ -172,20 +179,18 @@ class RssSourceViewModel(application: Application) : BaseViewModel(application) } private fun importSourceUrl(url: String): Int { - NetworkUtils.getBaseUrl(url)?.let { - val response = HttpHelper.getApiService(it).get(url, mapOf()).execute() - response.body()?.let { body -> - val sources = mutableListOf() - val items: List> = jsonPath.parse(body).read("$") - for (item in items) { - val jsonItem = jsonPath.parse(item) - GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> - sources.add(source) - } + HttpHelper.simpleGet(url)?.let { body -> + val sources = mutableListOf() + val items: List> = jsonPath.parse(body).read("$") + for (item in items) { + val jsonItem = jsonPath.parse(item) + GSON.fromJsonObject(jsonItem.jsonString())?.let { source -> + sources.add(source) } - App.db.rssSourceDao().insert(*sources.toTypedArray()) - return sources.size } + App.db.rssSourceDao().insert(*sources.toTypedArray()) + return sources.size + } return 0 } 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 77ef26284..86b326ee1 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 @@ -7,11 +7,13 @@ import android.os.Bundle import io.legado.app.R import io.legado.app.base.BaseActivity import io.legado.app.lib.theme.accentColor +import io.legado.app.ui.book.read.ReadBookActivity import io.legado.app.ui.main.MainActivity +import io.legado.app.utils.getPrefBoolean import kotlinx.android.synthetic.main.activity_welcome.* import org.jetbrains.anko.startActivity -class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { +open class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { override fun onActivityCreated(savedInstanceState: Bundle?) { iv_bg.setColorFilter(accentColor) @@ -29,22 +31,23 @@ class WelcomeActivity : BaseActivity(R.layout.activity_welcome) { welAnimator.addListener(object : Animator.AnimatorListener { override fun onAnimationStart(animation: Animator) { startActivity() + if (getPrefBoolean(getString(R.string.pk_default_read))) { + startActivity() + } finish() } - override fun onAnimationEnd(animation: Animator) { + override fun onAnimationEnd(animation: Animator) = Unit - } - - override fun onAnimationCancel(animation: Animator) { - - } + override fun onAnimationCancel(animation: Animator) = Unit - override fun onAnimationRepeat(animation: Animator) { - - } + override fun onAnimationRepeat(animation: Animator) = Unit }) welAnimator.start() } -} \ No newline at end of file +} + +class Launcher1 : WelcomeActivity() +class Launcher2 : WelcomeActivity() +class Launcher3 : WelcomeActivity() \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt new file mode 100644 index 000000000..504837afd --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/DetailSeekBar.kt @@ -0,0 +1,68 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.View +import android.widget.FrameLayout +import android.widget.SeekBar +import io.legado.app.R +import io.legado.app.utils.progressAdd +import kotlinx.android.synthetic.main.view_detail_seek_bar.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class DetailSeekBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs), + SeekBar.OnSeekBarChangeListener { + + var valueFormat: ((progress: Int) -> String)? = null + var onChanged: ((progress: Int) -> Unit)? = null + var progress: Int + get() = seek_bar.progress + set(value) { + seek_bar.progress = value + } + var max: Int + get() = seek_bar.max + set(value) { + seek_bar.max = value + } + + init { + View.inflate(context, R.layout.view_detail_seek_bar, this) + + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.DetailSeekBar) + tv_seek_title.text = typedArray.getText(R.styleable.DetailSeekBar_title) + seek_bar.max = typedArray.getInteger(R.styleable.DetailSeekBar_max, 0) + typedArray.recycle() + + iv_seek_plus.onClick { + seek_bar.progressAdd(1) + onChanged?.invoke(seek_bar.progress) + } + iv_seek_reduce.onClick { + seek_bar.progressAdd(-1) + onChanged?.invoke(seek_bar.progress) + } + seek_bar.setOnSeekBarChangeListener(this) + } + + private fun upValue(progress: Int = seek_bar.progress) { + valueFormat?.let { + tv_seek_value.text = it.invoke(progress) + } ?: let { + tv_seek_value.text = progress.toString() + } + } + + override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { + upValue(progress) + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) { + + } + + override fun onStopTrackingTouch(seekBar: SeekBar?) { + onChanged?.invoke(seek_bar.progress) + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt index 40e321b87..56dff107c 100644 --- a/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt +++ b/app/src/main/java/io/legado/app/ui/widget/KeyboardToolPop.kt @@ -10,7 +10,7 @@ import androidx.recyclerview.widget.RecyclerView import io.legado.app.R import io.legado.app.base.adapter.ItemViewHolder import io.legado.app.base.adapter.SimpleRecyclerAdapter -import kotlinx.android.synthetic.main.item_text.view.* +import kotlinx.android.synthetic.main.item_fillet_text.view.* import kotlinx.android.synthetic.main.popup_keyboard_tool.view.* import org.jetbrains.anko.sdk27.listeners.onClick @@ -23,7 +23,7 @@ class KeyboardToolPop( init { @SuppressLint("InflateParams") - this.contentView = LayoutInflater.from(context).inflate(R.layout.popup_keyboard_tool, null) + contentView = LayoutInflater.from(context).inflate(R.layout.popup_keyboard_tool, null) isTouchable = true isOutsideTouchable = false @@ -40,12 +40,21 @@ class KeyboardToolPop( } inner class Adapter(context: Context) : - SimpleRecyclerAdapter(context, R.layout.item_text) { + SimpleRecyclerAdapter(context, R.layout.item_fillet_text) { override fun convert(holder: ItemViewHolder, item: String, payloads: MutableList) { with(holder.itemView) { text_view.text = item - onClick { callBack?.sendText(item) } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.apply { + onClick { + getItem(holder.layoutPosition)?.let { + callBack?.sendText(it) + } + } } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt new file mode 100644 index 000000000..ab933edbb --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/LabelsBar.kt @@ -0,0 +1,57 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.widget.LinearLayout +import android.widget.TextView +import io.legado.app.ui.widget.text.AccentBgTextView +import io.legado.app.utils.dp + +@Suppress("unused") +class LabelsBar(context: Context, attrs: AttributeSet?) : LinearLayout(context, attrs) { + + private val unUsedViews = arrayListOf() + private val usedViews = arrayListOf() + + fun setLabels(labels: Array) { + clear() + labels.forEach { + addLabel(it) + } + } + + fun setLabels(labels: List) { + clear() + labels.forEach { + addLabel(it) + } + } + + fun clear() { + unUsedViews.addAll(usedViews) + usedViews.clear() + removeAllViews() + } + + fun addLabel(label: String) { + val tv = if (unUsedViews.isEmpty()) { + AccentBgTextView(context, null).apply { + setPadding(3.dp, 0, 3.dp, 0) + setRadios(2) + val lp = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) + lp.setMargins(0, 0, 2.dp, 0) + layoutParams = lp + text = label + maxLines = 1 + usedViews.add(this) + } + } else { + unUsedViews.last().apply { + usedViews.add(this) + unUsedViews.removeAt(unUsedViews.lastIndex) + } + } + tv.text = label + addView(tv) + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt new file mode 100644 index 000000000..3aab86a50 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/SelectActionBar.kt @@ -0,0 +1,97 @@ +package io.legado.app.ui.widget + +import android.content.Context +import android.util.AttributeSet +import android.view.Menu +import android.view.View +import android.widget.FrameLayout +import androidx.annotation.MenuRes +import androidx.annotation.StringRes +import androidx.appcompat.widget.PopupMenu +import io.legado.app.R +import io.legado.app.utils.visible +import kotlinx.android.synthetic.main.view_select_action_bar.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + +class SelectActionBar(context: Context, attrs: AttributeSet?) : FrameLayout(context, attrs) { + private var callBack: CallBack? = null + private var selMenu: PopupMenu? = null + + init { + setBackgroundResource(R.color.background_menu) + View.inflate(context, R.layout.view_select_action_bar, this) + cb_selected_all.setOnCheckedChangeListener { buttonView, isChecked -> + if (buttonView.isPressed) { + callBack?.selectAll(isChecked) + } + } + btn_revert_selection.onClick { callBack?.revertSelection() } + btn_select_action_main.onClick { callBack?.onClickMainAction() } + iv_menu_more.onClick { selMenu?.show() } + } + + fun setMainActionText(text: String) { + btn_select_action_main.text = text + btn_select_action_main.visible() + } + + fun setMainActionText(@StringRes id: Int) { + btn_select_action_main.setText(id) + btn_select_action_main.visible() + } + + fun inflateMenu(@MenuRes resId: Int): Menu? { + selMenu = PopupMenu(context, iv_menu_more) + selMenu?.inflate(resId) + iv_menu_more.visible() + return selMenu?.menu + } + + fun setCallBack(callBack: CallBack) { + this.callBack = callBack + } + + fun setOnMenuItemClickListener(listener: PopupMenu.OnMenuItemClickListener) { + selMenu?.setOnMenuItemClickListener(listener) + } + + fun upCountView(selectCount: Int, allCount: Int) { + if (selectCount == 0) { + cb_selected_all.isChecked = false + } else { + cb_selected_all.isChecked = selectCount >= allCount + } + + //重置全选的文字 + if (cb_selected_all.isChecked) { + cb_selected_all.text = context.getString( + R.string.select_cancel_count, + selectCount, + allCount + ) + } else { + cb_selected_all.text = context.getString( + R.string.select_all_count, + selectCount, + allCount + ) + } + setMenuClickable(selectCount > 0) + } + + private fun setMenuClickable(isClickable: Boolean) { + btn_revert_selection.isEnabled = isClickable + btn_revert_selection.isClickable = isClickable + btn_select_action_main.isEnabled = isClickable + btn_select_action_main.isClickable = isClickable + } + + interface CallBack { + + fun selectAll(selectAll: Boolean) + + fun revertSelection() + + fun onClickMainAction() {} + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt index 28bb1602a..9b80b1825 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionAnimator.kt @@ -23,13 +23,11 @@ import java.util.* class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rect) : ValueAnimator() { - private val mPaint: Paint + private val mPaint: Paint = Paint() private val mParticles: Array - private val mBound: Rect + private val mBound: Rect = Rect(bound) init { - mPaint = Paint() - mBound = Rect(bound) val partLen = 15 mParticles = arrayOfNulls(partLen * partLen) val random = Random(System.currentTimeMillis()) @@ -97,7 +95,7 @@ class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rec override fun start() { super.start() - mContainer.invalidate(mBound) + mContainer.invalidate() } private inner class Particle { @@ -141,7 +139,7 @@ class ExplosionAnimator(private val mContainer: View, bitmap: Bitmap, bound: Rec internal var DEFAULT_DURATION: Long = 0x400 private val DEFAULT_INTERPOLATOR = AccelerateInterpolator(0.6f) - private val END_VALUE = 1.4f + private const val END_VALUE = 1.4f private val X = Utils.dp2Px(5).toFloat() private val Y = Utils.dp2Px(20).toFloat() private val V = Utils.dp2Px(2).toFloat() diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt index 1332a3969..9b014b7d2 100644 --- a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionField.kt @@ -1,191 +1,21 @@ -/* - * Copyright (C) 2015 tyrantgit - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ package io.legado.app.ui.widget.anima.explosion_field -import android.animation.Animator -import android.animation.AnimatorListenerAdapter -import android.animation.ValueAnimator import android.app.Activity -import android.content.Context -import android.graphics.Bitmap -import android.graphics.Canvas -import android.graphics.Rect -import android.media.MediaPlayer -import android.util.AttributeSet -import android.util.Log import android.view.View import android.view.ViewGroup import android.view.Window -import java.util.* +object ExplosionField { -class ExplosionField : View { - - private var customDuration = ExplosionAnimator.DEFAULT_DURATION - private var idPlayAnimationEffect = 0 - private var mZAnimatorListener: OnAnimatorListener? = null - private var mOnClickListener: View.OnClickListener? = null - - private val mExplosions = ArrayList() - private val mExpandInset = IntArray(2) - - constructor(context: Context) : super(context) { - init() - } - - constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { - init() - } - - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( - context, - attrs, - defStyleAttr - ) { - init() - } - - private fun init() { - - Arrays.fill(mExpandInset, Utils.dp2Px(32)) - } - - override fun onDraw(canvas: Canvas) { - super.onDraw(canvas) - for (explosion in mExplosions) { - explosion.draw(canvas) - } - } - - fun playSoundAnimationEffect(id: Int) { - this.idPlayAnimationEffect = id - } - - fun setCustomDuration(customDuration: Long) { - this.customDuration = customDuration - } - - fun addActionEvent(ievents: OnAnimatorListener) { - this.mZAnimatorListener = ievents - } - - - fun expandExplosionBound(dx: Int, dy: Int) { - mExpandInset[0] = dx - mExpandInset[1] = dy - } - - @JvmOverloads - fun explode(bitmap: Bitmap?, bound: Rect, startDelay: Long, view: View? = null) { - val currentDuration = customDuration - val explosion = ExplosionAnimator(this, bitmap!!, bound) - explosion.addListener(object : AnimatorListenerAdapter() { - override fun onAnimationEnd(animation: Animator) { - mExplosions.remove(animation) - if (view != null) { - view.scaleX = 1f - view.scaleY = 1f - view.alpha = 1f - view.setOnClickListener(mOnClickListener)//set event - - } - } - }) - explosion.startDelay = startDelay - explosion.duration = currentDuration - mExplosions.add(explosion) - explosion.start() - } - - @JvmOverloads - fun explode(view: View, restartState: Boolean? = false) { - - val r = Rect() - view.getGlobalVisibleRect(r) - val location = IntArray(2) - getLocationOnScreen(location) - // getLocationInWindow(location); - // view.getLocationInWindow(location); - r.offset(-location[0], -location[1]) - r.inset(-mExpandInset[0], -mExpandInset[1]) - val startDelay = 100 - val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150) - animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { - - internal var random = Random() - - override fun onAnimationUpdate(animation: ValueAnimator) { - view.translationX = (random.nextFloat() - 0.5f) * view.width.toFloat() * 0.05f - view.translationY = (random.nextFloat() - 0.5f) * view.height.toFloat() * 0.05f - } - }) - - animator.addListener(object : Animator.AnimatorListener { - override fun onAnimationStart(animator: Animator) { - if (idPlayAnimationEffect != 0) - MediaPlayer.create(context, idPlayAnimationEffect).start() - } - - override fun onAnimationEnd(animator: Animator) { - if (mZAnimatorListener != null) { - mZAnimatorListener!!.onAnimationEnd(animator, this@ExplosionField) - } - } - - override fun onAnimationCancel(animator: Animator) { - Log.i("PRUEBA", "CANCEL") - } - - override fun onAnimationRepeat(animator: Animator) { - Log.i("PRUEBA", "REPEAT") - } - }) - - animator.start() - view.animate().setDuration(150).setStartDelay(startDelay.toLong()).scaleX(0f).scaleY(0f) - .alpha(0f).start() - if (restartState!!) - explode(Utils.createBitmapFromView(view), r, startDelay.toLong(), view) - else - explode(Utils.createBitmapFromView(view), r, startDelay.toLong()) - - } - - fun clear() { - mExplosions.clear() - invalidate() - } - - override fun setOnClickListener(mOnClickListener: View.OnClickListener?) { - this.mOnClickListener = mOnClickListener - } - - companion object { - - fun attach2Window(activity: Activity): ExplosionField { - val rootView = activity.findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup - val explosionField = ExplosionField(activity) - rootView.addView( - explosionField, ViewGroup.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT - ) + fun attach2Window(activity: Activity): ExplosionView { + val rootView = activity.findViewById(Window.ID_ANDROID_CONTENT) as ViewGroup + val explosionField = ExplosionView(activity) + rootView.addView( + explosionField, ViewGroup.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT ) - return explosionField - } + ) + return explosionField } - -} +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt new file mode 100644 index 000000000..c79695a51 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/anima/explosion_field/ExplosionView.kt @@ -0,0 +1,173 @@ +/* + * Copyright (C) 2015 tyrantgit + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.legado.app.ui.widget.anima.explosion_field + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Rect +import android.media.MediaPlayer +import android.util.AttributeSet +import android.util.Log +import android.view.View +import java.util.* + + +class ExplosionView : View { + + private var customDuration = ExplosionAnimator.DEFAULT_DURATION + private var idPlayAnimationEffect = 0 + private var mZAnimatorListener: OnAnimatorListener? = null + private var mOnClickListener: OnClickListener? = null + + private val mExplosions = ArrayList() + private val mExpandInset = IntArray(2) + + constructor(context: Context) : super(context) { + init() + } + + constructor(context: Context, attrs: AttributeSet) : super(context, attrs) { + init() + } + + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) { + init() + } + + private fun init() { + + Arrays.fill(mExpandInset, Utils.dp2Px(32)) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + for (explosion in mExplosions) { + explosion.draw(canvas) + } + } + + fun playSoundAnimationEffect(id: Int) { + this.idPlayAnimationEffect = id + } + + fun setCustomDuration(customDuration: Long) { + this.customDuration = customDuration + } + + fun addActionEvent(ievents: OnAnimatorListener) { + this.mZAnimatorListener = ievents + } + + + fun expandExplosionBound(dx: Int, dy: Int) { + mExpandInset[0] = dx + mExpandInset[1] = dy + } + + @JvmOverloads + fun explode(bitmap: Bitmap?, bound: Rect, startDelay: Long, view: View? = null) { + val currentDuration = customDuration + val explosion = ExplosionAnimator(this, bitmap!!, bound) + explosion.addListener(object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + mExplosions.remove(animation) + if (view != null) { + view.scaleX = 1f + view.scaleY = 1f + view.alpha = 1f + view.setOnClickListener(mOnClickListener)//set event + + } + } + }) + explosion.startDelay = startDelay + explosion.duration = currentDuration + mExplosions.add(explosion) + explosion.start() + } + + @JvmOverloads + fun explode(view: View, restartState: Boolean? = false) { + + val r = Rect() + view.getGlobalVisibleRect(r) + val location = IntArray(2) + getLocationOnScreen(location) + // getLocationInWindow(location); + // view.getLocationInWindow(location); + r.offset(-location[0], -location[1]) + r.inset(-mExpandInset[0], -mExpandInset[1]) + val startDelay = 100 + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(150) + animator.addUpdateListener(object : ValueAnimator.AnimatorUpdateListener { + + var random = Random() + + override fun onAnimationUpdate(animation: ValueAnimator) { + view.translationX = (random.nextFloat() - 0.5f) * view.width.toFloat() * 0.05f + view.translationY = (random.nextFloat() - 0.5f) * view.height.toFloat() * 0.05f + } + }) + + animator.addListener(object : Animator.AnimatorListener { + override fun onAnimationStart(animator: Animator) { + if (idPlayAnimationEffect != 0) + MediaPlayer.create(context, idPlayAnimationEffect).start() + } + + override fun onAnimationEnd(animator: Animator) { + if (mZAnimatorListener != null) { + mZAnimatorListener!!.onAnimationEnd(animator, this@ExplosionView) + } + } + + override fun onAnimationCancel(animator: Animator) { + Log.i("PRUEBA", "CANCEL") + } + + override fun onAnimationRepeat(animator: Animator) { + Log.i("PRUEBA", "REPEAT") + } + }) + + animator.start() + view.animate().setDuration(150).setStartDelay(startDelay.toLong()).scaleX(0f).scaleY(0f) + .alpha(0f).start() + if (restartState!!) + explode(Utils.createBitmapFromView(view), r, startDelay.toLong(), view) + else + explode(Utils.createBitmapFromView(view), r, startDelay.toLong()) + + } + + fun clear() { + mExplosions.clear() + invalidate() + } + + override fun setOnClickListener(mOnClickListener: OnClickListener?) { + this.mOnClickListener = mOnClickListener + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt b/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt new file mode 100644 index 000000000..b18e59332 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/checkbox/SmoothCheckBox.kt @@ -0,0 +1,326 @@ +package io.legado.app.ui.widget.checkbox + +import android.animation.ValueAnimator +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.view.View +import android.view.animation.LinearInterpolator +import android.widget.Checkable +import io.legado.app.R +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.dp +import io.legado.app.utils.getCompatColor +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.roundToInt +import kotlin.math.sqrt + +class SmoothCheckBox @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null +) : View(context, attrs), Checkable { + private var mPaint: Paint + private var mTickPaint: Paint + private var mFloorPaint: Paint + private var mTickPoints: Array + private var mCenterPoint: Point + private var mTickPath: Path + private var mLeftLineDistance = 0f + private var mRightLineDistance = 0f + private var mDrewDistance = 0f + private var mScaleVal = 1.0f + private var mFloorScale = 1.0f + private var mWidth = 0 + private var mAnimDuration = 0 + private var mStrokeWidth = 0 + private var mCheckedColor = 0 + private var mUnCheckedColor = 0 + private var mFloorColor = 0 + private var mFloorUnCheckedColor = 0 + private var mChecked = false + private var mTickDrawing = false + var onCheckedChangeListener: ((checkBox: SmoothCheckBox, isChecked: Boolean) -> Unit)? = null + + init { + val ta = context.obtainStyledAttributes(attrs, R.styleable.SmoothCheckBox) + var tickColor = ThemeStore.accentColor(context) + mCheckedColor = context.getCompatColor(R.color.background_menu) + mUnCheckedColor = context.getCompatColor(R.color.background_menu) + mFloorColor = context.getCompatColor(R.color.transparent30) + tickColor = ta.getColor(R.styleable.SmoothCheckBox_color_tick, tickColor) + mAnimDuration = ta.getInt(R.styleable.SmoothCheckBox_duration, DEF_ANIM_DURATION) + mFloorColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked_stroke, mFloorColor) + mCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_checked, mCheckedColor) + mUnCheckedColor = ta.getColor(R.styleable.SmoothCheckBox_color_unchecked, mUnCheckedColor) + mStrokeWidth = ta.getDimensionPixelSize(R.styleable.SmoothCheckBox_stroke_width, 0) + ta.recycle() + mFloorUnCheckedColor = mFloorColor + mTickPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mTickPaint.style = Paint.Style.STROKE + mTickPaint.strokeCap = Paint.Cap.ROUND + mTickPaint.color = tickColor + mFloorPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mFloorPaint.style = Paint.Style.FILL + mFloorPaint.color = mFloorColor + mPaint = Paint(Paint.ANTI_ALIAS_FLAG) + mPaint.style = Paint.Style.FILL + mPaint.color = mCheckedColor + mTickPath = Path() + mCenterPoint = Point() + mTickPoints = arrayOf(Point(), Point(), Point()) + setOnClickListener { + toggle() + mTickDrawing = false + mDrewDistance = 0f + if (isChecked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + } + } + + override fun isChecked(): Boolean { + return mChecked + } + + override fun setChecked(checked: Boolean) { + mChecked = checked + reset() + invalidate() + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } + + override fun toggle() { + this.isChecked = !isChecked + } + + /** + * checked with animation + * + * @param checked checked + * @param animate change with animation + */ + fun setChecked(checked: Boolean, animate: Boolean) { + if (animate) { + mTickDrawing = false + mChecked = checked + mDrewDistance = 0f + if (checked) { + startCheckedAnimation() + } else { + startUnCheckedAnimation() + } + onCheckedChangeListener?.invoke(this@SmoothCheckBox, mChecked) + } else { + this.isChecked = checked + } + } + + private fun reset() { + mTickDrawing = true + mFloorScale = 1.0f + mScaleVal = if (isChecked) 0f else 1.0f + mFloorColor = if (isChecked) mCheckedColor else mFloorUnCheckedColor + mDrewDistance = if (isChecked) mLeftLineDistance + mRightLineDistance else 0f + } + + private fun measureSize(measureSpec: Int): Int { + val defSize: Int = DEF_DRAW_SIZE.dp + val specSize = MeasureSpec.getSize(measureSpec) + val specMode = MeasureSpec.getMode(measureSpec) + var result = 0 + when (specMode) { + MeasureSpec.UNSPECIFIED, MeasureSpec.AT_MOST -> result = min(defSize, specSize) + MeasureSpec.EXACTLY -> result = specSize + } + return result + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + setMeasuredDimension(measureSize(widthMeasureSpec), measureSize(heightMeasureSpec)) + } + + override fun onLayout( + changed: Boolean, + left: Int, + top: Int, + right: Int, + bottom: Int + ) { + mWidth = measuredWidth + mStrokeWidth = if (mStrokeWidth == 0) measuredWidth / 10 else mStrokeWidth + mStrokeWidth = + if (mStrokeWidth > measuredWidth / 5) measuredWidth / 5 else mStrokeWidth + mStrokeWidth = if (mStrokeWidth < 3) 3 else mStrokeWidth + mCenterPoint.x = mWidth / 2 + mCenterPoint.y = measuredHeight / 2 + mTickPoints[0].x = (measuredWidth.toFloat() / 30 * 7).roundToInt() + mTickPoints[0].y = (measuredHeight.toFloat() / 30 * 14).roundToInt() + mTickPoints[1].x = (measuredWidth.toFloat() / 30 * 13).roundToInt() + mTickPoints[1].y = (measuredHeight.toFloat() / 30 * 20).roundToInt() + mTickPoints[2].x = (measuredWidth.toFloat() / 30 * 22).roundToInt() + mTickPoints[2].y = (measuredHeight.toFloat() / 30 * 10).roundToInt() + mLeftLineDistance = sqrt( + (mTickPoints[1].x - mTickPoints[0].x.toDouble()).pow(2.0) + + (mTickPoints[1].y - mTickPoints[0].y.toDouble()).pow(2.0) + ).toFloat() + mRightLineDistance = sqrt( + (mTickPoints[2].x - mTickPoints[1].x.toDouble()).pow(2.0) + + (mTickPoints[2].y - mTickPoints[1].y.toDouble()).pow(2.0) + ).toFloat() + mTickPaint.strokeWidth = mStrokeWidth.toFloat() + } + + override fun onDraw(canvas: Canvas) { + drawBorder(canvas) + drawCenter(canvas) + drawTick(canvas) + } + + private fun drawCenter(canvas: Canvas) { + mPaint.color = mUnCheckedColor + val radius = (mCenterPoint.x - mStrokeWidth) * mScaleVal + canvas.drawCircle(mCenterPoint.x.toFloat(), mCenterPoint.y.toFloat(), radius, mPaint) + } + + private fun drawBorder(canvas: Canvas) { + mFloorPaint.color = mFloorColor + val radius = mCenterPoint.x + canvas.drawCircle( + mCenterPoint.x.toFloat(), + mCenterPoint.y.toFloat(), + radius * mFloorScale, + mFloorPaint + ) + } + + private fun drawTick(canvas: Canvas) { + if (mTickDrawing && isChecked) { + drawTickPath(canvas) + } + } + + private fun drawTickPath(canvas: Canvas) { + mTickPath.reset() + // draw left of the tick + if (mDrewDistance < mLeftLineDistance) { + val step: Float = if (mWidth / 20.0f < 3) 3f else mWidth / 20.0f + mDrewDistance += step + val stopX = + mTickPoints[0].x + (mTickPoints[1].x - mTickPoints[0].x) * mDrewDistance / mLeftLineDistance + val stopY = + mTickPoints[0].y + (mTickPoints[1].y - mTickPoints[0].y) * mDrewDistance / mLeftLineDistance + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + if (mDrewDistance > mLeftLineDistance) { + mDrewDistance = mLeftLineDistance + } + } else { + mTickPath.moveTo(mTickPoints[0].x.toFloat(), mTickPoints[0].y.toFloat()) + mTickPath.lineTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + // draw right of the tick + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + val stopX = + mTickPoints[1].x + (mTickPoints[2].x - mTickPoints[1].x) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + val stopY = + mTickPoints[1].y - (mTickPoints[1].y - mTickPoints[2].y) * (mDrewDistance - mLeftLineDistance) / mRightLineDistance + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(stopX, stopY) + canvas.drawPath(mTickPath, mTickPaint) + val step: Float = if (mWidth / 20f < 3) 3f else mWidth / 20f + mDrewDistance += step + } else { + mTickPath.reset() + mTickPath.moveTo(mTickPoints[1].x.toFloat(), mTickPoints[1].y.toFloat()) + mTickPath.lineTo(mTickPoints[2].x.toFloat(), mTickPoints[2].y.toFloat()) + canvas.drawPath(mTickPath, mTickPaint) + } + } + // invalidate + if (mDrewDistance < mLeftLineDistance + mRightLineDistance) { + postDelayed({ this.postInvalidate() }, 10) + } + } + + private fun startCheckedAnimation() { + val animator = ValueAnimator.ofFloat(1.0f, 0f) + animator.duration = mAnimDuration / 3 * 2.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mUnCheckedColor, + mCheckedColor, + 1 - mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + drawTickDelayed() + } + + private fun startUnCheckedAnimation() { + val animator = ValueAnimator.ofFloat(0f, 1.0f) + animator.duration = mAnimDuration.toLong() + animator.interpolator = LinearInterpolator() + animator.addUpdateListener { animation: ValueAnimator -> + mScaleVal = animation.animatedValue as Float + mFloorColor = getGradientColor( + mCheckedColor, + mFloorUnCheckedColor, + mScaleVal + ) + postInvalidate() + } + animator.start() + val floorAnimator = ValueAnimator.ofFloat(1.0f, 0.8f, 1.0f) + floorAnimator.duration = mAnimDuration.toLong() + floorAnimator.interpolator = LinearInterpolator() + floorAnimator.addUpdateListener { animation: ValueAnimator -> + mFloorScale = animation.animatedValue as Float + postInvalidate() + } + floorAnimator.start() + } + + private fun drawTickDelayed() { + postDelayed({ + mTickDrawing = true + postInvalidate() + }, mAnimDuration.toLong()) + } + + companion object { + private const val DEF_DRAW_SIZE = 25 + private const val DEF_ANIM_DURATION = 300 + private fun getGradientColor(startColor: Int, endColor: Int, percent: Float): Int { + val startA = Color.alpha(startColor) + val startR = Color.red(startColor) + val startG = Color.green(startColor) + val startB = Color.blue(startColor) + val endA = Color.alpha(endColor) + val endR = Color.red(endColor) + val endG = Color.green(endColor) + val endB = Color.blue(endColor) + val currentA = (startA * (1 - percent) + endA * percent).toInt() + val currentR = (startR * (1 - percent) + endR * percent).toInt() + val currentG = (startG * (1 - percent) + endG * percent).toInt() + val currentB = (startB * (1 - percent) + endB * percent).toInt() + return Color.argb(currentA, currentR, currentG, currentB) + } + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..01a55f2da --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/dialog/TextDialog.kt @@ -0,0 +1,64 @@ +package io.legado.app.ui.widget.dialog + +import android.os.Bundle +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentManager +import io.legado.app.R +import kotlinx.android.synthetic.main.dialog_text_view.* +import ru.noties.markwon.Markwon + + +class TextDialog : DialogFragment() { + + companion object { + const val MD = 1 + + fun show(fragmentManager: FragmentManager, content: String?, mode: Int = 0) { + TextDialog().apply { + val bundle = Bundle() + bundle.putString("content", content) + bundle.putInt("mode", mode) + arguments = bundle + }.show(fragmentManager, "textDialog") + } + + } + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.windowManager?.defaultDisplay?.getMetrics(dm) + dialog?.window?.setLayout((dm.widthPixels * 0.9).toInt(), (dm.heightPixels * 0.9).toInt()) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_text_view, container) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + arguments?.let { + val content = it.getString("content") ?: "" + when (it.getInt("mode")) { + MD -> text_view.post { + Markwon.create(requireContext()) + .setMarkdown( + text_view, + content + ) + } + else -> text_view.text = content + } + } + + } + +} diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt index 902a5bfae..f7c73030a 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontAdapter.kt @@ -14,7 +14,7 @@ import java.io.File class FontAdapter(context: Context, val callBack: CallBack) : SimpleRecyclerAdapter(context, R.layout.item_font) { - override fun convert(holder: ItemViewHolder, item: File, payloads: MutableList) = + override fun convert(holder: ItemViewHolder, item: File, payloads: MutableList) { with(holder.itemView) { val typeface = Typeface.createFromFile(item) tv_font.typeface = typeface @@ -26,6 +26,15 @@ class FontAdapter(context: Context, val callBack: CallBack) : iv_checked.invisible() } } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + callBack.onClick(it) + } + } + } interface CallBack { fun onClick(file: File) diff --git a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt index 420bc9233..be5523b58 100644 --- a/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt +++ b/app/src/main/java/io/legado/app/ui/widget/font/FontSelectDialog.kt @@ -5,7 +5,6 @@ import android.app.Activity.RESULT_OK import android.content.Intent import android.net.Uri import android.os.Bundle -import android.os.Environment import android.util.DisplayMetrics import android.view.LayoutInflater import android.view.MenuItem @@ -13,40 +12,35 @@ import android.view.View import android.view.ViewGroup import androidx.appcompat.widget.Toolbar import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.DialogFragment 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.constant.PreferKey -import io.legado.app.help.FileHelp import io.legado.app.help.permission.Permissions import io.legado.app.help.permission.PermissionsCompat -import io.legado.app.utils.DocumentUtils -import io.legado.app.utils.getPrefString -import io.legado.app.utils.putPrefString -import io.legado.app.utils.toast +import io.legado.app.lib.dialogs.alert +import io.legado.app.ui.filechooser.FileChooserDialog +import io.legado.app.utils.* import kotlinx.android.synthetic.main.dialog_font_select.* -import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.Dispatchers.Main -import kotlinx.coroutines.Job import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import org.jetbrains.anko.toast import java.io.File -import kotlin.coroutines.CoroutineContext -class FontSelectDialog : DialogFragment(), +class FontSelectDialog : BaseDialogFragment(), + FileChooserDialog.CallBack, Toolbar.OnMenuItemClickListener, - CoroutineScope, FontAdapter.CallBack { - lateinit var job: Job private val fontFolderRequestCode = 35485 - private val fontFolder = - App.INSTANCE.filesDir.absolutePath + File.separator + "Fonts" + File.separator - private val fontCacheFolder = - App.INSTANCE.cacheDir.absolutePath + File.separator + "Fonts" + File.separator - override val coroutineContext: CoroutineContext - get() = job + Main + private val fontFolder by lazy { + FileUtils.createFolderIfNotExist(App.INSTANCE.filesDir, "Fonts") + } + private val fontCacheFolder by lazy { + FileUtils.createFolderIfNotExist(App.INSTANCE.cacheDir, "Fonts") + } private var adapter: FontAdapter? = null override fun onStart() { @@ -61,7 +55,6 @@ class FontSelectDialog : DialogFragment(), container: ViewGroup?, savedInstanceState: Bundle? ): View? { - job = Job() return inflater.inflate(R.layout.dialog_font_select, container) } @@ -78,11 +71,15 @@ class FontSelectDialog : DialogFragment(), if (fontPath.isNullOrEmpty()) { openFolder() } else { - val uri = Uri.parse(fontPath) - if (DocumentFile.fromTreeUri(requireContext(), uri)?.canRead() == true) { - getFontFiles(uri) + if (fontPath.isContentPath()) { + val doc = DocumentFile.fromTreeUri(requireContext(), Uri.parse(fontPath)) + if (doc?.canRead() == true) { + getFontFiles(doc) + } else { + openFolder() + } } else { - openFolder() + getFontFilesByPermission(fontPath) } } } @@ -105,39 +102,72 @@ class FontSelectDialog : DialogFragment(), return true } - override fun onDestroy() { - super.onDestroy() - job.cancel() - } - private fun openFolder() { - try { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - startActivityForResult(intent, fontFolderRequestCode) - } catch (e: java.lang.Exception) { - PermissionsCompat.Builder(this) - .addPermissions(*Permissions.Group.STORAGE) - .rationale(R.string.tip_perm_request_storage) - .onGranted { getFontFilesOld() } - .request() - } + alert { + titleResource = R.string.select_folder + items(resources.getStringArray(R.array.select_folder).toList()) { _, index -> + when (index) { + 0 -> { + val path = "${FileUtils.getSdCardPath()}${File.separator}Fonts" + putPrefString(PreferKey.fontFolder, path) + getFontFilesByPermission(path) + } + 1 -> { + try { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + startActivityForResult(intent, fontFolderRequestCode) + } catch (e: java.lang.Exception) { + e.printStackTrace() + requireContext().toast(e.localizedMessage ?: "ERROR") + } + } + 2 -> { + PermissionsCompat.Builder(this@FontSelectDialog) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + FileChooserDialog.show( + childFragmentManager, + fontFolderRequestCode, + mode = FileChooserDialog.DIRECTORY + ) + } + .request() + } + } + } + }.show() } @SuppressLint("DefaultLocale") - private fun getFontFiles(uri: Uri) { + private fun getFontFiles(doc: DocumentFile) { launch(IO) { - FileHelp.deleteFile(fontCacheFolder) - DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { file -> - if (file.name?.toLowerCase()?.matches(".*\\.[ot]tf".toRegex()) == true) { - DocumentUtils.readBytes(App.INSTANCE, file.uri)?.let { - FileHelp.getFile(fontCacheFolder + file.name).writeBytes(it) + val docItems = DocumentUtils.listFiles(App.INSTANCE, doc.uri) + fontCacheFolder.listFiles()?.forEach { fontFile -> + var contain = false + for (item in docItems) { + if (fontFile.name == item.name) { + contain = true + break + } + } + if (!contain) { + fontFile.delete() + } + } + docItems.forEach { item -> + if (item.name.toLowerCase().matches(".*\\.[ot]tf".toRegex())) { + val fontFile = FileUtils.getFile(fontCacheFolder, item.name) + if (!fontFile.exists()) { + DocumentUtils.readBytes(App.INSTANCE, item.uri)?.let { byteArray -> + fontFile.writeBytes(byteArray) + } } } } try { - val file = File(fontCacheFolder) - file.listFiles { pathName -> + fontCacheFolder.listFiles { pathName -> pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) }?.let { withContext(Main) { @@ -151,23 +181,29 @@ class FontSelectDialog : DialogFragment(), } @SuppressLint("DefaultLocale") - private fun getFontFilesOld() { - try { - val file = - File(Environment.getExternalStorageDirectory().absolutePath + File.separator + "Fonts") - file.listFiles { pathName -> - pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) - }?.let { - adapter?.setItems(it.toList()) + private fun getFontFilesByPermission(path: String) { + PermissionsCompat.Builder(this@FontSelectDialog) + .addPermissions(*Permissions.Group.STORAGE) + .rationale(R.string.tip_perm_request_storage) + .onGranted { + try { + val file = File(path) + file.listFiles { pathName -> + pathName.name.toLowerCase().matches(".*\\.[ot]tf".toRegex()) + }?.let { + adapter?.setItems(it.toList()) + } + } catch (e: Exception) { + toast(e.localizedMessage ?: "") + } } - } catch (e: Exception) { - toast(e.localizedMessage ?: "") - } + .request() } override fun onClick(file: File) { launch(IO) { - file.copyTo(FileHelp.getFile(fontFolder + file.name), true).absolutePath.let { path -> + file.copyTo(FileUtils.createFileIfNotExist(fontFolder, file.name), true) + .absolutePath.let { path -> val cb = (parentFragment as? CallBack) ?: (activity as? CallBack) cb?.let { if (it.curFontPath != path) { @@ -187,17 +223,33 @@ class FontSelectDialog : DialogFragment(), ?: "" } + override fun onFilePicked(requestCode: Int, currentPath: String) { + when (requestCode) { + fontFolderRequestCode -> { + putPrefString(PreferKey.fontFolder, currentPath) + getFontFilesByPermission(currentPath) + } + } + } + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { super.onActivityResult(requestCode, resultCode, data) when (requestCode) { fontFolderRequestCode -> if (resultCode == RESULT_OK) { data?.data?.let { uri -> putPrefString(PreferKey.fontFolder, uri.toString()) - context?.contentResolver?.takePersistableUriPermission( - uri, - Intent.FLAG_GRANT_READ_URI_PERMISSION - ) - getFontFiles(uri) + val doc = DocumentFile.fromTreeUri(requireContext(), uri) + if (doc != null) { + context?.contentResolver?.takePersistableUriPermission( + uri, + Intent.FLAG_GRANT_READ_URI_PERMISSION + ) + getFontFiles(doc) + } else { + RealPathUtil.getPath(requireContext(), uri)?.let { + getFontFilesByPermission(it) + } + } } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt index 31a23f623..253c1bef3 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/CircleImageView.kt @@ -8,6 +8,7 @@ import android.graphics.drawable.ColorDrawable import android.graphics.drawable.Drawable import android.net.Uri import android.os.Build +import android.text.TextPaint import android.util.AttributeSet import android.view.MotionEvent import android.view.View @@ -19,6 +20,7 @@ import androidx.annotation.RequiresApi import androidx.appcompat.widget.AppCompatImageView import io.legado.app.R import io.legado.app.utils.getCompatColor +import io.legado.app.utils.sp import kotlin.math.min import kotlin.math.pow @@ -31,6 +33,12 @@ class CircleImageView : AppCompatImageView { private val mBitmapPaint = Paint() private val mBorderPaint = Paint() private val mCircleBackgroundPaint = Paint() + private val textPaint by lazy { + val textPaint = TextPaint() + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint + } private var mBorderColor = DEFAULT_BORDER_COLOR private var mBorderWidth = DEFAULT_BORDER_WIDTH @@ -105,22 +113,43 @@ class CircleImageView : AppCompatImageView { setup() } - constructor(context: Context) : super(context) { + private var text: String? = null + + private var textColor = context.getCompatColor(R.color.tv_text_default) + constructor(context: Context) : super(context) { init() } @JvmOverloads - constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super(context, attrs, defStyle) { + constructor(context: Context, attrs: AttributeSet, defStyle: Int = 0) : super( + context, + attrs, + defStyle + ) { val a = context.obtainStyledAttributes(attrs, R.styleable.CircleImageView, defStyle, 0) - mBorderWidth = a.getDimensionPixelSize(R.styleable.CircleImageView_civ_border_width, DEFAULT_BORDER_WIDTH) - mBorderColor = a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) - mBorderOverlay = a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) + mBorderWidth = a.getDimensionPixelSize( + R.styleable.CircleImageView_civ_border_width, + DEFAULT_BORDER_WIDTH + ) + mBorderColor = + a.getColor(R.styleable.CircleImageView_civ_border_color, DEFAULT_BORDER_COLOR) + mBorderOverlay = + a.getBoolean(R.styleable.CircleImageView_civ_border_overlay, DEFAULT_BORDER_OVERLAY) mCircleBackgroundColor = - a.getColor(R.styleable.CircleImageView_civ_circle_background_color, DEFAULT_CIRCLE_BACKGROUND_COLOR) - + a.getColor( + R.styleable.CircleImageView_civ_circle_background_color, + DEFAULT_CIRCLE_BACKGROUND_COLOR + ) + text = a.getString(R.styleable.CircleImageView_text) + if (a.hasValue(R.styleable.CircleImageView_textColor)) { + textColor = a.getColor( + R.styleable.CircleImageView_textColor, + context.getCompatColor(R.color.tv_text_default) + ) + } a.recycle() init() @@ -161,20 +190,54 @@ class CircleImageView : AppCompatImageView { super.onDraw(canvas) return } - if (mBitmap == null) { return } if (mCircleBackgroundColor != Color.TRANSPARENT) { - canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mCircleBackgroundPaint) + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mCircleBackgroundPaint + ) } - canvas.drawCircle(mDrawableRect.centerX(), mDrawableRect.centerY(), mDrawableRadius, mBitmapPaint) + canvas.drawCircle( + mDrawableRect.centerX(), + mDrawableRect.centerY(), + mDrawableRadius, + mBitmapPaint + ) if (mBorderWidth > 0) { - canvas.drawCircle(mBorderRect.centerX(), mBorderRect.centerY(), mBorderRadius, mBorderPaint) + canvas.drawCircle( + mBorderRect.centerX(), + mBorderRect.centerY(), + mBorderRadius, + mBorderPaint + ) + } + drawText(canvas) + } + + private fun drawText(canvas: Canvas) { + text?.let { + textPaint.color = textColor + textPaint.textSize = 15.sp.toFloat() + val fm = textPaint.fontMetrics + canvas.drawText( + it, + width * 0.5f, + (height * 0.5f + (fm.bottom - fm.top) * 0.5f - fm.bottom), + textPaint + ) } } + fun setTextColor(@ColorInt textColor: Int) { + this.textColor = textColor + invalidate() + } + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { super.onSizeChanged(w, h, oldw, oldh) setup() @@ -243,9 +306,17 @@ class CircleImageView : AppCompatImageView { return try { val bitmap: Bitmap = if (drawable is ColorDrawable) { - Bitmap.createBitmap(COLORDRAWABLE_DIMENSION, COLORDRAWABLE_DIMENSION, BITMAP_CONFIG) + Bitmap.createBitmap( + COLOR_DRAWABLE_DIMENSION, + COLOR_DRAWABLE_DIMENSION, + BITMAP_CONFIG + ) } else { - Bitmap.createBitmap(drawable.intrinsicWidth, drawable.intrinsicHeight, BITMAP_CONFIG) + Bitmap.createBitmap( + drawable.intrinsicWidth, + drawable.intrinsicHeight, + BITMAP_CONFIG + ) } val canvas = Canvas(bitmap) @@ -260,10 +331,10 @@ class CircleImageView : AppCompatImageView { } private fun initializeBitmap() { - if (isDisableCircularTransformation) { - mBitmap = null + mBitmap = if (isDisableCircularTransformation) { + null } else { - mBitmap = getBitmapFromDrawable(drawable) + getBitmapFromDrawable(drawable) } setup() } @@ -302,7 +373,10 @@ class CircleImageView : AppCompatImageView { mBorderRect.set(calculateBounds()) mBorderRadius = - min((mBorderRect.height() - mBorderWidth) / 2.0f, (mBorderRect.width() - mBorderWidth) / 2.0f) + min( + (mBorderRect.height() - mBorderWidth) / 2.0f, + (mBorderRect.width() - mBorderWidth) / 2.0f + ) mDrawableRect.set(mBorderRect) if (!mBorderOverlay && mBorderWidth > 0) { @@ -343,7 +417,10 @@ class CircleImageView : AppCompatImageView { } mShaderMatrix.setScale(scale, scale) - mShaderMatrix.postTranslate((dx + 0.5f).toInt() + mDrawableRect.left, (dy + 0.5f).toInt() + mDrawableRect.top) + mShaderMatrix.postTranslate( + (dx + 0.5f).toInt() + mDrawableRect.left, + (dy + 0.5f).toInt() + mDrawableRect.top + ) mBitmapShader!!.setLocalMatrix(mShaderMatrix) } @@ -371,12 +448,9 @@ class CircleImageView : AppCompatImageView { } companion object { - private val SCALE_TYPE = ScaleType.CENTER_CROP - private val BITMAP_CONFIG = Bitmap.Config.ARGB_8888 - private const val COLORDRAWABLE_DIMENSION = 2 - + private const val COLOR_DRAWABLE_DIMENSION = 2 private const val DEFAULT_BORDER_WIDTH = 0 private const val DEFAULT_BORDER_COLOR = Color.BLACK private const val DEFAULT_CIRCLE_BACKGROUND_COLOR = Color.TRANSPARENT diff --git a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt index 289790a1a..426391be4 100644 --- a/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/image/CoverImageView.kt @@ -2,31 +2,73 @@ package io.legado.app.ui.widget.image import android.annotation.SuppressLint import android.content.Context -import android.graphics.Canvas -import android.graphics.Path +import android.graphics.* +import android.graphics.drawable.Drawable +import android.text.TextPaint import android.util.AttributeSet +import com.bumptech.glide.load.DataSource +import com.bumptech.glide.load.engine.GlideException +import com.bumptech.glide.request.RequestListener +import com.bumptech.glide.request.target.Target +import io.legado.app.R +import io.legado.app.help.ImageLoader class CoverImageView : androidx.appcompat.widget.AppCompatImageView { internal var width: Float = 0.toFloat() internal var height: Float = 0.toFloat() + private var nameHeight = 0f + private var authorHeight = 0f + private val namePaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT_BOLD + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSkewX = -0.2f + textPaint + } + private val authorPaint by lazy { + val textPaint = TextPaint() + textPaint.typeface = Typeface.DEFAULT + textPaint.isAntiAlias = true + textPaint.textAlign = Paint.Align.CENTER + textPaint.textSkewX = -0.1f + textPaint + } + private var name: String? = null + private var author: String? = null + private var loadFailed = false constructor(context: Context) : super(context) constructor(context: Context, attrs: AttributeSet) : super(context, attrs) - constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super(context, attrs, defStyleAttr) + constructor(context: Context, attrs: AttributeSet, defStyleAttr: Int) : super( + context, + attrs, + defStyleAttr + ) + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) + val measuredHeight = measuredWidth * 7 / 5 + super.onMeasure( + widthMeasureSpec, + MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY) + ) + } override fun onLayout(changed: Boolean, left: Int, top: Int, right: Int, bottom: Int) { super.onLayout(changed, left, top, right, bottom) width = getWidth().toFloat() height = getHeight().toFloat() - } - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - val measuredWidth = MeasureSpec.getSize(widthMeasureSpec) - val measuredHeight = measuredWidth * 7 / 5 - super.onMeasure(widthMeasureSpec, MeasureSpec.makeMeasureSpec(measuredHeight, MeasureSpec.EXACTLY)) + namePaint.textSize = width / 6 + namePaint.strokeWidth = namePaint.textSize / 10 + authorPaint.textSize = width / 9 + authorPaint.strokeWidth = authorPaint.textSize / 10 + val fm = namePaint.fontMetrics + nameHeight = height * 0.5f + (fm.bottom - fm.top) * 0.5f + authorHeight = nameHeight + (fm.bottom - fm.top) * 0.6f } override fun onDraw(canvas: Canvas) { @@ -47,10 +89,74 @@ class CoverImageView : androidx.appcompat.widget.AppCompatImageView { canvas.clipPath(path) } super.onDraw(canvas) + if (!loadFailed) return + name?.let { + namePaint.color = Color.WHITE + namePaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, nameHeight, namePaint) + namePaint.color = Color.RED + namePaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, nameHeight, namePaint) + } + author?.let { + authorPaint.color = Color.WHITE + authorPaint.style = Paint.Style.STROKE + canvas.drawText(it, width / 2, authorHeight, authorPaint) + authorPaint.color = Color.RED + authorPaint.style = Paint.Style.FILL + canvas.drawText(it, width / 2, authorHeight, authorPaint) + } } fun setHeight(height: Int) { val width = height * 5 / 7 minimumWidth = width } + + private fun setText(name: String?, author: String?) { + this.name = + when { + name == null -> null + name.length > 5 -> name.substring(0, 4) + "…" + else -> name + } + this.author = + when { + author == null -> null + author.length > 8 -> author.substring(0, 7) + "…" + else -> author + } + } + + fun load(path: String?, name: String?, author: String?) { + setText(name, author) + ImageLoader.load(context, path)//Glide自动识别http://和file:// + .placeholder(R.drawable.image_cover_default) + .error(R.drawable.image_cover_default) + .listener(object : RequestListener { + override fun onLoadFailed( + e: GlideException?, + model: Any?, + target: Target?, + isFirstResource: Boolean + ): Boolean { + loadFailed = true + return false + } + + override fun onResourceReady( + resource: Drawable?, + model: Any?, + target: Target?, + dataSource: DataSource?, + isFirstResource: Boolean + ): Boolean { + loadFailed = false + return false + } + + }) + .centerCrop() + .into(this) + } } diff --git a/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt new file mode 100644 index 000000000..17fe9ab53 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/number/NumberPickerDialog.kt @@ -0,0 +1,66 @@ +package io.legado.app.ui.widget.number + +import android.content.Context +import android.widget.NumberPicker +import androidx.appcompat.app.AlertDialog +import io.legado.app.R +import io.legado.app.utils.applyTint +import io.legado.app.utils.hideSoftInput +import kotlinx.android.synthetic.main.dialog_number_picker.* + + +class NumberPickerDialog(context: Context) { + private val builder = AlertDialog.Builder(context) + private var numberPicker: NumberPicker? = null + private var maxValue: Int? = null + private var minValue: Int? = null + private var value: Int? = null + + init { + builder.setView(R.layout.dialog_number_picker) + } + + fun setTitle(title: String): NumberPickerDialog { + builder.setTitle(title) + return this + } + + fun setMaxValue(value: Int): NumberPickerDialog { + maxValue = value + return this + } + + fun setMinValue(value: Int): NumberPickerDialog { + minValue = value + return this + } + + fun setValue(value: Int): NumberPickerDialog { + this.value = value + return this + } + + fun show(callBack: ((value: Int) -> Unit)?) { + builder.setPositiveButton(R.string.ok) { _, _ -> + numberPicker?.let { + it.clearFocus() + it.hideSoftInput() + callBack?.invoke(it.value) + } + } + builder.setNegativeButton(R.string.cancel, null) + val dialog = builder.show().applyTint() + numberPicker = dialog.number_picker + numberPicker?.let { np -> + minValue?.let { + np.minValue = it + } + maxValue?.let { + np.maxValue = it + } + value?.let { + np.value = it + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/prefs/ATEColorPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt similarity index 92% rename from app/src/main/java/io/legado/app/lib/theme/prefs/ATEColorPreference.kt rename to app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt index 517ba8d68..6bea75a51 100644 --- a/app/src/main/java/io/legado/app/lib/theme/prefs/ATEColorPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/ColorPreference.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.prefs +package io.legado.app.ui.widget.prefs import android.content.Context import android.content.ContextWrapper @@ -15,11 +15,11 @@ import androidx.preference.PreferenceViewHolder import com.jaredrummler.android.colorpicker.* import io.legado.app.lib.theme.ATH -class ATEColorPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), +class ColorPreference(context: Context, attrs: AttributeSet) : Preference(context, attrs), ColorPickerDialogListener { - private val SIZE_NORMAL = 0 - private val SIZE_LARGE = 1 + private val sizeNormal = 0 + private val sizeLarge = 1 private var onShowDialogListener: OnShowDialogListener? = null private var color = Color.BLACK @@ -46,20 +46,18 @@ class ATEColorPreference(context: Context, attrs: AttributeSet) : Preference(con allowCustom = a.getBoolean(R.styleable.ColorPreference_cpv_allowCustom, true) showAlphaSlider = a.getBoolean(R.styleable.ColorPreference_cpv_showAlphaSlider, false) showColorShades = a.getBoolean(R.styleable.ColorPreference_cpv_showColorShades, true) - previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, SIZE_NORMAL) + previewSize = a.getInt(R.styleable.ColorPreference_cpv_previewSize, sizeNormal) val presetsResId = a.getResourceId(R.styleable.ColorPreference_cpv_colorPresets, 0) dialogTitle = a.getResourceId(R.styleable.ColorPreference_cpv_dialogTitle, R.string.cpv_default_title) - if (presetsResId != 0) { - presets = context.resources.getIntArray(presetsResId) + presets = if (presetsResId != 0) { + context.resources.getIntArray(presetsResId) } else { - presets = ColorPickerDialog.MATERIAL_COLORS + ColorPickerDialog.MATERIAL_COLORS } - if (colorShape == ColorShape.CIRCLE) { - widgetLayoutResource = - if (previewSize == SIZE_LARGE) R.layout.cpv_preference_circle_large else R.layout.cpv_preference_circle + widgetLayoutResource = if (colorShape == ColorShape.CIRCLE) { + if (previewSize == sizeLarge) R.layout.cpv_preference_circle_large else R.layout.cpv_preference_circle } else { - widgetLayoutResource = - if (previewSize == SIZE_LARGE) R.layout.cpv_preference_square_large else R.layout.cpv_preference_square + if (previewSize == sizeLarge) R.layout.cpv_preference_square_large else R.layout.cpv_preference_square } a.recycle() } @@ -88,7 +86,7 @@ class ATEColorPreference(context: Context, attrs: AttributeSet) : Preference(con } } - fun getActivity(): FragmentActivity { + private fun getActivity(): FragmentActivity { val context = context if (context is FragmentActivity) { return context @@ -399,7 +397,8 @@ class ATEColorPreference(context: Context, attrs: AttributeSet) : Preference(con * @see .show */ fun create(): ColorPickerDialog { - val dialog = ColorPickerDialogCompat() + val dialog = + ColorPickerDialogCompat() val args = Bundle() args.putInt(ARG_ID, dialogId) args.putInt(ARG_TYPE, dialogType) @@ -418,14 +417,6 @@ class ATEColorPreference(context: Context, attrs: AttributeSet) : Preference(con return dialog } - /** - * Create and show the [ColorPickerDialog] created with this builder. - * - * @param activity The current activity. - */ - fun show(activity: FragmentActivity) { - create().show(activity.supportFragmentManager, "color-picker-dialog") - } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt new file mode 100644 index 000000000..ff3fa801c --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/IconListPreference.kt @@ -0,0 +1,201 @@ +package io.legado.app.ui.widget.prefs + +import android.content.Context +import android.content.ContextWrapper +import android.graphics.drawable.Drawable +import android.os.Bundle +import android.util.AttributeSet +import android.util.DisplayMetrics +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.ImageView +import androidx.fragment.app.DialogFragment +import androidx.fragment.app.FragmentActivity +import androidx.preference.ListPreference +import androidx.preference.PreferenceViewHolder +import androidx.recyclerview.widget.LinearLayoutManager +import io.legado.app.R +import io.legado.app.base.adapter.ItemViewHolder +import io.legado.app.base.adapter.SimpleRecyclerAdapter +import io.legado.app.utils.getCompatDrawable +import kotlinx.android.synthetic.main.dialog_recycler_view.* +import kotlinx.android.synthetic.main.item_icon_preference.view.* +import org.jetbrains.anko.sdk27.listeners.onClick + + +class IconListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { + private var iconNames: Array + private val mEntryDrawables = arrayListOf() + + init { + widgetLayoutResource = R.layout.view_icon + + val a = context.theme.obtainStyledAttributes(attrs, R.styleable.IconListPreference, 0, 0) + + iconNames = try { + a.getTextArray(R.styleable.IconListPreference_icons) + } finally { + a.recycle() + } + + for (iconName in iconNames) { + val resId = context.resources + .getIdentifier(iconName.toString(), "mipmap", context.packageName) + var d: Drawable? = null + kotlin.runCatching { + d = context.getCompatDrawable(resId) + } + mEntryDrawables.add(d) + } + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + super.onBindViewHolder(holder) + holder?.itemView?.findViewById(R.id.preview)?.let { + val selectedIndex = findIndexOfValue(value) + if (selectedIndex >= 0) { + val drawable = mEntryDrawables[selectedIndex] + it.setImageDrawable(drawable) + } + } + } + + override fun onClick() { + getActivity()?.let { + val dialog = IconDialog().apply { + val args = Bundle() + args.putString("value", value) + args.putCharSequenceArray("entries", entries) + args.putCharSequenceArray("entryValues", entryValues) + args.putCharSequenceArray("iconNames", iconNames) + arguments = args + onChanged = { value -> + this@IconListPreference.value = value + } + } + it.supportFragmentManager + .beginTransaction() + .add(dialog, getFragmentTag()) + .commitAllowingStateLoss() + } + } + + override fun onAttached() { + super.onAttached() + val fragment = + getActivity()?.supportFragmentManager?.findFragmentByTag(getFragmentTag()) as IconDialog? + fragment?.onChanged = { value -> + this@IconListPreference.value = value + } + } + + private fun getActivity(): FragmentActivity? { + val context = context + if (context is FragmentActivity) { + return context + } else if (context is ContextWrapper) { + val baseContext = context.baseContext + if (baseContext is FragmentActivity) { + return baseContext + } + } + return null + } + + private fun getFragmentTag(): String { + return "icon_$key" + } + + class IconDialog : DialogFragment() { + + var onChanged: ((value: String) -> Unit)? = null + var dialogValue: String? = null + var dialogEntries: Array? = null + var dialogEntryValues: Array? = null + var dialogIconNames: Array? = null + + override fun onStart() { + super.onStart() + val dm = DisplayMetrics() + activity?.windowManager?.defaultDisplay?.getMetrics(dm) + dialog?.window?.setLayout( + (dm.widthPixels * 0.8).toInt(), + ViewGroup.LayoutParams.WRAP_CONTENT + ) + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.dialog_recycler_view, container) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + tool_bar.setTitle(R.string.change_icon) + recycler_view.layoutManager = LinearLayoutManager(requireContext()) + val adapter = Adapter(requireContext()) + recycler_view.adapter = adapter + arguments?.let { + dialogValue = it.getString("value") + dialogEntries = it.getCharSequenceArray("entries") + dialogEntryValues = it.getCharSequenceArray("entryValues") + dialogIconNames = it.getCharSequenceArray("iconNames") + dialogEntryValues?.let { values -> + adapter.setItems(values.toList()) + } + } + } + + + inner class Adapter(context: Context) : + SimpleRecyclerAdapter(context, R.layout.item_icon_preference) { + + override fun convert( + holder: ItemViewHolder, + item: CharSequence, + payloads: MutableList + ) { + with(holder.itemView) { + val index = findIndexOfValue(item.toString()) + dialogEntries?.let { + label.text = it[index] + } + dialogIconNames?.let { + val resId = context.resources + .getIdentifier(it[index].toString(), "mipmap", context.packageName) + val d = context.getCompatDrawable(resId) + icon.setImageDrawable(d) + } + label.isChecked = item.toString() == dialogValue + onClick { + onChanged?.invoke(item.toString()) + this@IconDialog.dismiss() + } + } + } + + override fun registerListener(holder: ItemViewHolder) { + holder.itemView.onClick { + getItem(holder.layoutPosition)?.let { + onChanged?.invoke(it.toString()) + } + } + } + + private fun findIndexOfValue(value: String?): Int { + dialogEntryValues?.let { values -> + for (i in values.indices.reversed()) { + if (values[i] == value) { + return i + } + } + } + return -1 + } + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt new file mode 100644 index 000000000..d8b51e90f --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/NameListPreference.kt @@ -0,0 +1,22 @@ +package io.legado.app.ui.widget.prefs + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView +import androidx.preference.ListPreference +import androidx.preference.PreferenceViewHolder +import io.legado.app.R + + +class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference(context, attrs) { + + init { + widgetLayoutResource = R.layout.item_fillet_text + } + + override fun onBindViewHolder(holder: PreferenceViewHolder?) { + super.onBindViewHolder(holder) + val textView = holder?.itemView?.findViewById(R.id.text_view) + textView?.text = entry + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/prefs/ATEPreferenceCategory.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt similarity index 61% rename from app/src/main/java/io/legado/app/lib/theme/prefs/ATEPreferenceCategory.kt rename to app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt index 16f9e168f..e1458b76e 100644 --- a/app/src/main/java/io/legado/app/lib/theme/prefs/ATEPreferenceCategory.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/PreferenceCategory.kt @@ -1,22 +1,22 @@ -package io.legado.app.lib.theme.prefs +package io.legado.app.ui.widget.prefs import android.content.Context import android.util.AttributeSet import android.widget.TextView import androidx.preference.PreferenceCategory import androidx.preference.PreferenceViewHolder -import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.accentColor -class ATEPreferenceCategory(context: Context, attrs: AttributeSet) : +class PreferenceCategory(context: Context, attrs: AttributeSet) : PreferenceCategory(context, attrs) { override fun onBindViewHolder(holder: PreferenceViewHolder?) { super.onBindViewHolder(holder) holder?.let { val view = it.findViewById(android.R.id.title) - if (view is TextView) { - view.setTextColor(ThemeStore.accentColor(view.getContext()))//设置title文本的颜色 + if (view is TextView && !view.isInEditMode) { + view.setTextColor(context.accentColor)//设置title文本的颜色 } } } diff --git a/app/src/main/java/io/legado/app/lib/theme/prefs/ATESwitchPreference.kt b/app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt similarity index 71% rename from app/src/main/java/io/legado/app/lib/theme/prefs/ATESwitchPreference.kt rename to app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt index 998865a7f..4fe66af7b 100644 --- a/app/src/main/java/io/legado/app/lib/theme/prefs/ATESwitchPreference.kt +++ b/app/src/main/java/io/legado/app/ui/widget/prefs/SwitchPreference.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.prefs +package io.legado.app.ui.widget.prefs import android.content.Context import android.util.AttributeSet @@ -7,9 +7,9 @@ import androidx.preference.PreferenceViewHolder import androidx.preference.SwitchPreferenceCompat import io.legado.app.R import io.legado.app.lib.theme.ATH -import io.legado.app.lib.theme.ThemeStore +import io.legado.app.lib.theme.accentColor -class ATESwitchPreference(context: Context, attrs: AttributeSet) : +class SwitchPreference(context: Context, attrs: AttributeSet) : SwitchPreferenceCompat(context, attrs) { override fun onBindViewHolder(holder: PreferenceViewHolder?) { @@ -17,7 +17,7 @@ class ATESwitchPreference(context: Context, attrs: AttributeSet) : holder?.let { val view = it.findViewById(R.id.switchWidget) if (view is SwitchCompat) { - ATH.setTint(view, ThemeStore.accentColor(view.getContext())) + ATH.setTint(view, context.accentColor) } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/LoadMoreView.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt similarity index 86% rename from app/src/main/java/io/legado/app/ui/widget/LoadMoreView.kt rename to app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt index ee17ee735..a2437c0b4 100644 --- a/app/src/main/java/io/legado/app/ui/widget/LoadMoreView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/LoadMoreView.kt @@ -1,7 +1,7 @@ -package io.legado.app.ui.widget +package io.legado.app.ui.widget.recycler import android.content.Context -import android.view.LayoutInflater +import android.view.View import android.view.ViewGroup import android.widget.FrameLayout import io.legado.app.R @@ -15,7 +15,7 @@ class LoadMoreView(context: Context) : FrameLayout(context) { private set init { - LayoutInflater.from(context).inflate(R.layout.view_load_more, this, true) + View.inflate(context, R.layout.view_load_more, this) } override fun onAttachedToWindow() { diff --git a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt index 25af39c6a..20f638d61 100644 --- a/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt +++ b/app/src/main/java/io/legado/app/ui/widget/recycler/scroller/FastScroller.kt @@ -482,18 +482,17 @@ class FastScroller : LinearLayout { var showTrack = true if (attrs != null) { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.FastScroller, 0, 0) - if (typedArray != null) { - try { - bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor) - handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor) - trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor) - textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor) - fadeScrollbar = typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar) - showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble) - showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack) - } finally { - typedArray.recycle() - } + try { + bubbleColor = typedArray.getColor(R.styleable.FastScroller_bubbleColor, bubbleColor) + handleColor = typedArray.getColor(R.styleable.FastScroller_handleColor, handleColor) + trackColor = typedArray.getColor(R.styleable.FastScroller_trackColor, trackColor) + textColor = typedArray.getColor(R.styleable.FastScroller_bubbleTextColor, textColor) + fadeScrollbar = + typedArray.getBoolean(R.styleable.FastScroller_fadeScrollbar, fadeScrollbar) + showBubble = typedArray.getBoolean(R.styleable.FastScroller_showBubble, showBubble) + showTrack = typedArray.getBoolean(R.styleable.FastScroller_showTrack, showTrack) + } finally { + typedArray.recycle() } } setTrackColor(trackColor) diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt new file mode 100644 index 000000000..6f48e81af --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentBgTextView.kt @@ -0,0 +1,47 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.graphics.Color +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.ColorUtils +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.dp +import io.legado.app.utils.getCompatColor + +class AccentBgTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + private var radios = 0 + + init { + val typedArray = context.obtainStyledAttributes(attrs, R.styleable.AccentBgTextView) + radios = typedArray.getDimensionPixelOffset(R.styleable.AccentBgTextView_radius, radios) + typedArray.recycle() + upBackground() + setTextColor(Color.WHITE) + } + + fun setRadios(radio: Int) { + this.radios = radio.dp + upBackground() + } + + private fun upBackground() { + background = if (isInEditMode) { + Selector.shapeBuild() + .setCornerRadius(radios) + .setDefaultBgColor(context.getCompatColor(R.color.colorAccent)) + .setPressedBgColor(ColorUtils.darkenColor(context.getCompatColor(R.color.colorAccent))) + .create() + } else { + Selector.shapeBuild() + .setCornerRadius(radios) + .setDefaultBgColor(ThemeStore.accentColor(context)) + .setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) + .create() + } + } +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEAccentStrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt similarity index 89% rename from app/src/main/java/io/legado/app/lib/theme/view/ATEAccentStrokeTextView.kt rename to app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt index 355ed65a6..37b18051a 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEAccentStrokeTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentStrokeTextView.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.view +package io.legado.app.ui.widget.text import android.content.Context import android.util.AttributeSet @@ -9,7 +9,7 @@ import io.legado.app.lib.theme.ThemeStore import io.legado.app.utils.dp import io.legado.app.utils.getCompatColor -class ATEAccentStrokeTextView(context: Context, attrs: AttributeSet) : +class AccentStrokeTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { init { diff --git a/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt new file mode 100644 index 000000000..f797a9f57 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/AccentTextView.kt @@ -0,0 +1,22 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.accentColor +import org.jetbrains.anko.textColor +import org.jetbrains.anko.textColorResource + +class AccentTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + init { + if (!isInEditMode) { + textColor = context.accentColor + } else { + textColorResource = R.color.colorAccent + } + } + +} diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEAutoCompleteTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt similarity index 95% rename from app/src/main/java/io/legado/app/lib/theme/view/ATEAutoCompleteTextView.kt rename to app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt index ae7dec6e0..ac0a5a8ac 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEAutoCompleteTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/AutoCompleteTextView.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.view +package io.legado.app.ui.widget.text import android.annotation.SuppressLint import android.content.Context @@ -17,7 +17,7 @@ import kotlinx.android.synthetic.main.item_1line_text_and_del.view.* import org.jetbrains.anko.sdk27.listeners.onClick -class ATEAutoCompleteTextView : AppCompatAutoCompleteTextView { +class AutoCompleteTextView : AppCompatAutoCompleteTextView { constructor(context: Context) : super(context) diff --git a/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt similarity index 99% rename from app/src/main/java/io/legado/app/ui/widget/BadgeView.kt rename to app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt index 3f1572164..33fc8b3a2 100644 --- a/app/src/main/java/io/legado/app/ui/widget/BadgeView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/BadgeView.kt @@ -1,4 +1,4 @@ -package io.legado.app.ui.widget +package io.legado.app.ui.widget.text import android.content.Context import android.graphics.Color @@ -77,7 +77,7 @@ class BadgeView @JvmOverloads constructor( init { val typedArray = context.obtainStyledAttributes(attrs, R.styleable.BadgeView) val radios = - typedArray.getDimensionPixelOffset(R.styleable.BadgeView_bv_radius, 8) + typedArray.getDimensionPixelOffset(R.styleable.BadgeView_radius, 8) flatangle = typedArray.getBoolean(R.styleable.BadgeView_up_flat_angle, false) typedArray.recycle() diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEEditText.kt b/app/src/main/java/io/legado/app/ui/widget/text/EditText.kt similarity index 53% rename from app/src/main/java/io/legado/app/lib/theme/view/ATEEditText.kt rename to app/src/main/java/io/legado/app/ui/widget/text/EditText.kt index c6c692634..ad1b979ff 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEEditText.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/EditText.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.view +package io.legado.app.ui.widget.text import android.content.Context import android.util.AttributeSet @@ -9,9 +9,11 @@ import io.legado.app.lib.theme.ThemeStore /** * @author Aidan Follestad (afollestad) */ -class ATEEditText(context: Context, attrs: AttributeSet) : AppCompatEditText(context, attrs) { +class EditText(context: Context, attrs: AttributeSet) : AppCompatEditText(context, attrs) { init { - ATH.setTint(this, ThemeStore.accentColor(context)) + if (!isInEditMode) { + ATH.setTint(this, ThemeStore.accentColor(context)) + } } } diff --git a/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt new file mode 100644 index 000000000..051d5af4e --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/InertiaScrollTextView.kt @@ -0,0 +1,231 @@ +package io.legado.app.ui.widget.text + +import android.annotation.SuppressLint +import android.content.Context +import android.util.AttributeSet +import android.view.MotionEvent +import android.view.VelocityTracker +import android.view.ViewConfiguration +import android.view.animation.Interpolator +import android.widget.OverScroller +import androidx.appcompat.widget.AppCompatTextView +import androidx.core.view.ViewCompat +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.min + + +open class InertiaScrollTextView : AppCompatTextView { + constructor(context: Context) : super(context) + + constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) + + constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) + : super(context, attrs, defStyleAttr) + + + private val scrollStateIdle = 0 + private val scrollStateDragging = 1 + val scrollStateSettling = 2 + + private val mViewFling: ViewFling by lazy { ViewFling() } + private var velocityTracker: VelocityTracker? = null + private var mScrollState = scrollStateIdle + private var mLastTouchY: Int = 0 + private var mTouchSlop: Int = 0 + private var mMinFlingVelocity: Int = 0 + private var mMaxFlingVelocity: Int = 0 + + //滑动距离的最大边界 + private var mOffsetHeight: Int = 0 + + //f(x) = (x-1)^5 + 1 + private val sQuinticInterpolator = Interpolator { + var t = it + t -= 1.0f + t * t * t * t * t + 1.0f + } + + init { + val vc = ViewConfiguration.get(context) + mTouchSlop = vc.scaledTouchSlop + mMinFlingVelocity = vc.scaledMinimumFlingVelocity + mMaxFlingVelocity = vc.scaledMaximumFlingVelocity + } + + fun atTop(): Boolean { + return scrollY <= 0 + } + + fun atBottom(): Boolean { + return scrollY >= mOffsetHeight + } + + override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { + super.onMeasure(widthMeasureSpec, heightMeasureSpec) + initOffsetHeight() + } + + override fun onTextChanged( + text: CharSequence?, + start: Int, + lengthBefore: Int, + lengthAfter: Int + ) { + super.onTextChanged(text, start, lengthBefore, lengthAfter) + initOffsetHeight() + } + + private fun initOffsetHeight() { + val mLayoutHeight: Int + + //获得内容面板 + val mLayout = layout ?: return + //获得内容面板的高度 + mLayoutHeight = mLayout.height + + //计算滑动距离的边界 + mOffsetHeight = mLayoutHeight + totalPaddingTop + totalPaddingBottom - measuredHeight + } + + override fun scrollTo(x: Int, y: Int) { + super.scrollTo(x, min(y, mOffsetHeight)) + } + + @SuppressLint("ClickableViewAccessibility") + override fun onTouchEvent(event: MotionEvent?): Boolean { + event?.let { + if (velocityTracker == null) { + velocityTracker = VelocityTracker.obtain() + } + velocityTracker?.addMovement(it) + when (event.action) { + MotionEvent.ACTION_DOWN -> { + setScrollState(scrollStateIdle) + mLastTouchY = (event.y + 0.5f).toInt() + } + MotionEvent.ACTION_MOVE -> { + val y = (event.y + 0.5f).toInt() + var dy = mLastTouchY - y + if (mScrollState != scrollStateDragging) { + var startScroll = false + + if (abs(dy) > mTouchSlop) { + if (dy > 0) { + dy -= mTouchSlop + } else { + dy += mTouchSlop + } + startScroll = true + } + if (startScroll) { + setScrollState(scrollStateDragging) + } + } + if (mScrollState == scrollStateDragging) { + mLastTouchY = y + } + } + MotionEvent.ACTION_UP -> { + velocityTracker?.computeCurrentVelocity(1000, mMaxFlingVelocity.toFloat()) + val yVelocity = velocityTracker?.yVelocity ?: 0f + if (abs(yVelocity) > mMinFlingVelocity) { + mViewFling.fling(-yVelocity.toInt()) + } else { + setScrollState(scrollStateIdle) + } + resetTouch() + } + MotionEvent.ACTION_CANCEL -> { + resetTouch() + } + } + } + return super.onTouchEvent(event) + } + + private fun resetTouch() { + velocityTracker?.clear() + } + + private fun setScrollState(state: Int) { + if (state == mScrollState) { + return + } + mScrollState = state + if (state != scrollStateSettling) { + mViewFling.stop() + } + } + + /** + * 惯性滚动 + */ + private inner class ViewFling : Runnable { + + private var mLastFlingY = 0 + private val mScroller: OverScroller = OverScroller(context, sQuinticInterpolator) + private var mEatRunOnAnimationRequest = false + private var mReSchedulePostAnimationCallback = false + + override fun run() { + disableRunOnAnimationRequests() + val scroller = mScroller + if (scroller.computeScrollOffset()) { + val y = scroller.currY + val dy = y - mLastFlingY + mLastFlingY = y + if (dy < 0 && scrollY > 0) { + scrollBy(0, max(dy, -scrollY)) + } else if (dy > 0 && scrollY < mOffsetHeight) { + scrollBy(0, min(dy, mOffsetHeight - scrollY)) + } + postOnAnimation() + } + enableRunOnAnimationRequests() + } + + fun fling(velocityY: Int) { + mLastFlingY = 0 + setScrollState(scrollStateSettling) + mScroller.fling( + 0, + 0, + 0, + velocityY, + Integer.MIN_VALUE, + Integer.MAX_VALUE, + Integer.MIN_VALUE, + Integer.MAX_VALUE + ) + postOnAnimation() + } + + fun stop() { + removeCallbacks(this) + mScroller.abortAnimation() + } + + private fun disableRunOnAnimationRequests() { + mReSchedulePostAnimationCallback = false + mEatRunOnAnimationRequest = true + } + + private fun enableRunOnAnimationRequests() { + mEatRunOnAnimationRequest = false + if (mReSchedulePostAnimationCallback) { + postOnAnimation() + } + } + + internal fun postOnAnimation() { + if (mEatRunOnAnimationRequest) { + mReSchedulePostAnimationCallback = true + } else { + removeCallbacks(this) + ViewCompat.postOnAnimation(this@InertiaScrollTextView, this) + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATEPrimaryTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt similarity index 75% rename from app/src/main/java/io/legado/app/lib/theme/view/ATEPrimaryTextView.kt rename to app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt index b28a97305..2c2666e07 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATEPrimaryTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/PrimaryTextView.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.view +package io.legado.app.ui.widget.text import android.content.Context import android.util.AttributeSet @@ -8,7 +8,7 @@ import io.legado.app.lib.theme.ThemeStore /** * @author Aidan Follestad (afollestad) */ -class ATEPrimaryTextView(context: Context, attrs: AttributeSet) : +class PrimaryTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { init { diff --git a/app/src/main/java/io/legado/app/lib/theme/view/ATESecondaryTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt similarity index 75% rename from app/src/main/java/io/legado/app/lib/theme/view/ATESecondaryTextView.kt rename to app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt index 8031961d3..6d59af0e6 100644 --- a/app/src/main/java/io/legado/app/lib/theme/view/ATESecondaryTextView.kt +++ b/app/src/main/java/io/legado/app/ui/widget/text/SecondaryTextView.kt @@ -1,4 +1,4 @@ -package io.legado.app.lib.theme.view +package io.legado.app.ui.widget.text import android.content.Context import android.util.AttributeSet @@ -8,7 +8,7 @@ import io.legado.app.lib.theme.secondaryTextColor /** * @author Aidan Follestad (afollestad) */ -class ATESecondaryTextView(context: Context, attrs: AttributeSet) : +class SecondaryTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { init { diff --git a/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt new file mode 100644 index 000000000..df4da33ea --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/StrokeTextView.kt @@ -0,0 +1,50 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import androidx.appcompat.widget.AppCompatTextView +import io.legado.app.R +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore +import io.legado.app.utils.dp +import io.legado.app.utils.getCompatColor + +open class StrokeTextView(context: Context, attrs: AttributeSet?) : + AppCompatTextView(context, attrs) { + + init { + if (isInEditMode) { + background = Selector.shapeBuild() + .setCornerRadius(1.dp) + .setStrokeWidth(1.dp) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(context.getCompatColor(R.color.tv_text_secondary)) + .setSelectedStrokeColor(context.getCompatColor(R.color.colorAccent)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + this.setTextColor( + Selector.colorBuild() + .setDefaultColor(context.getCompatColor(R.color.tv_text_secondary)) + .setSelectedColor(context.getCompatColor(R.color.colorAccent)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } else { + background = Selector.shapeBuild() + .setCornerRadius(1.dp) + .setStrokeWidth(1.dp) + .setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) + .setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) + .setSelectedStrokeColor(ThemeStore.accentColor(context)) + .setPressedBgColor(context.getCompatColor(R.color.transparent30)) + .create() + this.setTextColor( + Selector.colorBuild() + .setDefaultColor(ThemeStore.textColorSecondary(context)) + .setSelectedColor(ThemeStore.accentColor(context)) + .setDisabledColor(context.getCompatColor(R.color.md_grey_500)) + .create() + ) + } + } +} diff --git a/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt b/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt new file mode 100644 index 000000000..015e848c3 --- /dev/null +++ b/app/src/main/java/io/legado/app/ui/widget/text/TextInputLayout.kt @@ -0,0 +1,18 @@ +package io.legado.app.ui.widget.text + +import android.content.Context +import android.util.AttributeSet +import com.google.android.material.textfield.TextInputLayout +import io.legado.app.lib.theme.Selector +import io.legado.app.lib.theme.ThemeStore + +class TextInputLayout(context: Context, attrs: AttributeSet?) : TextInputLayout(context, attrs) { + + init { + if (!isInEditMode) { + defaultHintTextColor = + Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create() + } + } + +} diff --git a/app/src/main/java/io/legado/app/utils/ACache.kt b/app/src/main/java/io/legado/app/utils/ACache.kt index 44f96631c..03a330d6a 100644 --- a/app/src/main/java/io/legado/app/utils/ACache.kt +++ b/app/src/main/java/io/legado/app/utils/ACache.kt @@ -171,7 +171,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) * @return JSONObject数据 */ fun getAsJSONObject(key: String): JSONObject? { - val json = getAsString(key) + val json = getAsString(key) ?: return null return try { JSONObject(json) } catch (e: Exception) { @@ -311,17 +311,17 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) fun getAsObject(key: String): Any? { val data = getAsBinary(key) if (data != null) { - var bais: ByteArrayInputStream? = null + var bis: ByteArrayInputStream? = null var ois: ObjectInputStream? = null try { - bais = ByteArrayInputStream(data) - ois = ObjectInputStream(bais) + bis = ByteArrayInputStream(data) + ois = ObjectInputStream(bis) return ois.readObject() } catch (e: Exception) { e.printStackTrace() } finally { try { - bais?.close() + bis?.close() } catch (e: IOException) { e.printStackTrace() } @@ -597,6 +597,7 @@ class ACache private constructor(cacheDir: File, max_size: Long, max_count: Int) val w = drawable.intrinsicWidth val h = drawable.intrinsicHeight // 取 drawable 的颜色格式 + @Suppress("DEPRECATION") val config = if (drawable.opacity != PixelFormat.OPAQUE) Bitmap.Config.ARGB_8888 else diff --git a/app/src/main/java/io/legado/app/utils/BatteryUtils.kt b/app/src/main/java/io/legado/app/utils/BatteryUtils.kt deleted file mode 100644 index c7d49a3aa..000000000 --- a/app/src/main/java/io/legado/app/utils/BatteryUtils.kt +++ /dev/null @@ -1,16 +0,0 @@ -package io.legado.app.utils - -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.os.BatteryManager - -object BatteryUtils { - - fun getLevel(context: Context): Int { - val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) - val batteryStatus = context.registerReceiver(null, iFilter) - - return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 - } -} diff --git a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt index 00f1d4c58..14212f442 100644 --- a/app/src/main/java/io/legado/app/utils/BitmapUtils.kt +++ b/app/src/main/java/io/legado/app/utils/BitmapUtils.kt @@ -147,7 +147,7 @@ object BitmapUtils { //图片不被压缩 fun convertViewToBitmap(view: View, bitmapWidth: Int, bitmapHeight: Int): Bitmap { - val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Bitmap.Config.ARGB_8888) + val bitmap = Bitmap.createBitmap(bitmapWidth, bitmapHeight, Config.ARGB_8888) view.draw(Canvas(bitmap)) return bitmap } diff --git a/app/src/main/java/io/legado/app/utils/ColorUtils.kt b/app/src/main/java/io/legado/app/utils/ColorUtils.kt index b592bb6c9..9eceb6724 100644 --- a/app/src/main/java/io/legado/app/utils/ColorUtils.kt +++ b/app/src/main/java/io/legado/app/utils/ColorUtils.kt @@ -4,7 +4,11 @@ import android.graphics.Color import androidx.annotation.ColorInt import androidx.annotation.FloatRange +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +@Suppress("unused") object ColorUtils { fun intToString(intColor: Int): String { @@ -52,7 +56,7 @@ object ColorUtils { @ColorInt fun adjustAlpha(@ColorInt color: Int, @FloatRange(from = 0.0, to = 1.0) factor: Float): Int { - val alpha = Math.round(Color.alpha(color) * factor) + val alpha = (Color.alpha(color) * factor).roundToInt() val red = Color.red(color) val green = Color.green(color) val blue = Color.blue(color) @@ -61,7 +65,7 @@ object ColorUtils { @ColorInt fun withAlpha(@ColorInt baseColor: Int, @FloatRange(from = 0.0, to = 1.0) alpha: Float): Int { - val a = Math.min(255, Math.max(0, (alpha * 255).toInt())) shl 24 + val a = min(255, max(0, (alpha * 255).toInt())) shl 24 val rgb = 0x00ffffff and baseColor return a + rgb } diff --git a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt index 2ab88b481..5b266a343 100644 --- a/app/src/main/java/io/legado/app/utils/ContextExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ContextExtensions.kt @@ -1,11 +1,14 @@ package io.legado.app.utils import android.annotation.SuppressLint -import android.content.Context -import android.content.Intent +import android.content.* import android.content.res.ColorStateList +import android.content.res.Configuration import android.graphics.Bitmap import android.graphics.drawable.Drawable +import android.net.Uri +import android.os.BatteryManager +import android.provider.Settings import androidx.annotation.ColorRes import androidx.annotation.DrawableRes import androidx.core.content.ContextCompat @@ -63,6 +66,19 @@ fun Context.getCompatDrawable(@DrawableRes id: Int): Drawable? = ContextCompat.g fun Context.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = ContextCompat.getColorStateList(this, id) +/** + * 系统息屏时间 + */ +fun Context.getScreenOffTime(): Int { + var screenOffTime = 0 + try { + screenOffTime = Settings.System.getInt(contentResolver, Settings.System.SCREEN_OFF_TIMEOUT) + } catch (e: Exception) { + e.printStackTrace() + } + return screenOffTime +} + fun Context.getStatusBarHeight(): Int { val resourceId = resources.getIdentifier("status_bar_height", "dimen", "android") return resources.getDimensionPixelSize(resourceId) @@ -115,11 +131,44 @@ fun Context.shareWithQr(title: String, text: String) { } } -val Context.isNightTheme: Boolean - get() = getPrefBoolean("isNightTheme") +fun Context.sendToClip(text: String) { + val clipboard = + getSystemService(Context.CLIPBOARD_SERVICE) as? ClipboardManager + val clipData = ClipData.newPlainText(null, text) + clipboard?.let { + clipboard.setPrimaryClip(clipData) + toast(R.string.copy_complete) + } +} + +fun Context.sysIsDarkMode(): Boolean { + val mode = resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK + return mode == Configuration.UI_MODE_NIGHT_YES +} -val Context.isTransparentStatusBar: Boolean - get() = getPrefBoolean("transparentStatusBar", true) +/** + * 获取电量 + */ +fun Context.getBettery(): Int { + val iFilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) + val batteryStatus = registerReceiver(null, iFilter) + return batteryStatus?.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) ?: -1 +} -val Context.isShowRSS: Boolean - get() = getPrefBoolean("showRss", true) \ No newline at end of file +fun Context.openUrl(url: String) { + val intent = Intent(Intent.ACTION_VIEW) + intent.data = Uri.parse(url) + if (intent.resolveActivity(packageManager) != null) { + try { + startActivity(intent) + } catch (e: Exception) { + toast(e.localizedMessage ?: "open url error") + } + } else { + try { + startActivity(Intent.createChooser(intent, "请选择浏览器")) + } catch (e: Exception) { + toast(e.localizedMessage ?: "open url error") + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt index 3929874ef..f4fd0e8eb 100644 --- a/app/src/main/java/io/legado/app/utils/DocumentUtils.kt +++ b/app/src/main/java/io/legado/app/utils/DocumentUtils.kt @@ -1,10 +1,49 @@ package io.legado.app.utils import android.content.Context +import android.database.Cursor import android.net.Uri +import android.provider.DocumentsContract +import androidx.documentfile.provider.DocumentFile +import java.util.* + object DocumentUtils { + fun exists(root: DocumentFile, fileName: String, vararg subDirs: String): Boolean { + val parent = getDirDocument(root, *subDirs) ?: return false + return parent.findFile(fileName)?.exists() ?: false + } + + fun createFileIfNotExist( + root: DocumentFile, + fileName: String, + mimeType: String = "", + vararg subDirs: String + ): DocumentFile? { + val parent: DocumentFile? = createFolderIfNotExist(root, *subDirs) + return parent?.createFile(mimeType, fileName) + } + + fun createFolderIfNotExist(root: DocumentFile, vararg subDirs: String): DocumentFile? { + var parent: DocumentFile? = root + for (subDirName in subDirs) { + val subDir = parent?.findFile(subDirName) + ?: parent?.createDirectory(subDirName) + parent = subDir + } + return parent + } + + fun getDirDocument(root: DocumentFile, vararg subDirs: String): DocumentFile? { + var parent = root + for (subDirName in subDirs) { + val subDir = parent.findFile(subDirName) + parent = subDir ?: return null + } + return parent + } + @JvmStatic @Throws(Exception::class) fun writeText(context: Context, data: String, fileUri: Uri): Boolean { @@ -44,5 +83,79 @@ object DocumentUtils { return null } + fun listFiles(context: Context, uri: Uri): ArrayList { + val docList = arrayListOf() + var c: Cursor? = null + try { + val childrenUri = DocumentsContract.buildChildDocumentsUriUsingTree( + uri, + DocumentsContract.getDocumentId(uri) + ) + c = context.contentResolver.query( + childrenUri, arrayOf( + DocumentsContract.Document.COLUMN_DOCUMENT_ID, + DocumentsContract.Document.COLUMN_DISPLAY_NAME, + DocumentsContract.Document.COLUMN_LAST_MODIFIED, + DocumentsContract.Document.COLUMN_SIZE, + DocumentsContract.Document.COLUMN_MIME_TYPE + ), null, null, null + ) + c?.let { + val ici = c.getColumnIndex(DocumentsContract.Document.COLUMN_DOCUMENT_ID) + val nci = c.getColumnIndex(DocumentsContract.Document.COLUMN_DISPLAY_NAME) + val sci = c.getColumnIndex(DocumentsContract.Document.COLUMN_SIZE) + val mci = c.getColumnIndex(DocumentsContract.Document.COLUMN_MIME_TYPE) + val dci = c.getColumnIndex(DocumentsContract.Document.COLUMN_LAST_MODIFIED) + c.moveToFirst() + do { + val item = DocItem( + name = c.getString(nci), + attr = c.getString(mci), + size = c.getLong(sci), + date = Date(c.getLong(dci)), + uri = DocumentsContract.buildDocumentUriUsingTree(uri, c.getString(ici)) + ) + docList.add(item) + } while (c.moveToNext()) + } + } catch (e: Exception) { + e.printStackTrace() + } finally { + c?.close() + } + return docList + } + +} + +data class DocItem( + val name: String, + val attr: String, + val size: Long, + val date: Date, + val uri: Uri +) { + val isDir: Boolean by lazy { + DocumentsContract.Document.MIME_TYPE_DIR == attr + } +} + +@Throws(Exception::class) +fun DocumentFile.writeText(context: Context, data: String) { + DocumentUtils.writeText(context, data, this.uri) +} + +@Throws(Exception::class) +fun DocumentFile.writeBytes(context: Context, data: ByteArray) { + DocumentUtils.writeBytes(context, data, this.uri) +} + +@Throws(Exception::class) +fun DocumentFile.readText(context: Context): String? { + return DocumentUtils.readText(context, this.uri) +} -} \ No newline at end of file +@Throws(Exception::class) +fun DocumentFile.readBytes(context: Context): ByteArray? { + return DocumentUtils.readBytes(context, this.uri) +} diff --git a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt index 4858113e3..406aa3815 100644 --- a/app/src/main/java/io/legado/app/utils/EncoderUtils.kt +++ b/app/src/main/java/io/legado/app/utils/EncoderUtils.kt @@ -3,6 +3,7 @@ package io.legado.app.utils import android.util.Base64 import java.nio.charset.StandardCharsets +@Suppress("unused") object EncoderUtils { fun escape(src: String): String { diff --git a/app/src/main/java/io/legado/app/help/http/EncodingDetect.java b/app/src/main/java/io/legado/app/utils/EncodingDetect.java similarity index 83% rename from app/src/main/java/io/legado/app/help/http/EncodingDetect.java rename to app/src/main/java/io/legado/app/utils/EncodingDetect.java index 2659e0cb4..29174b88e 100644 --- a/app/src/main/java/io/legado/app/help/http/EncodingDetect.java +++ b/app/src/main/java/io/legado/app/utils/EncodingDetect.java @@ -1,6 +1,7 @@ -package io.legado.app.help.http; +package io.legado.app.utils; import androidx.annotation.NonNull; + import org.jsoup.Jsoup; import org.jsoup.nodes.Document; import org.jsoup.nodes.Element; @@ -61,10 +62,10 @@ public class EncodingDetect { } } catch (Exception ignored) { } - return getJavaEncode(bytes); + return getEncode(bytes); } - public static String getJavaEncode(@NonNull byte[] bytes) { + public static String getEncode(@NonNull byte[] bytes) { int len = bytes.length > 2000 ? 2000 : bytes.length; byte[] cBytes = new byte[len]; System.arraycopy(bytes, 0, cBytes, 0, len); @@ -82,7 +83,7 @@ public class EncodingDetect { /** * 得到文件的编码 */ - public static String getJavaEncode(@NonNull String filePath) { + public static String getEncode(@NonNull String filePath) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s .detectEncoding(new File(filePath))]; @@ -101,7 +102,7 @@ public class EncodingDetect { /** * 得到文件的编码 */ - public static String getJavaEncode(@NonNull File file) { + public static String getEncode(@NonNull File file) { BytesEncodingDetect s = new BytesEncodingDetect(); String fileCode = BytesEncodingDetect.javaname[s.detectEncoding(file)]; // UTF-16LE 特殊处理 @@ -119,26 +120,23 @@ public class EncodingDetect { class BytesEncodingDetect extends Encoding { // Frequency tables to hold the GB, Big5, and EUC-TW character // frequencies - int GBFreq[][]; + private int[][] GBFreq; - int GBKFreq[][]; + private int[][] GBKFreq; - int Big5Freq[][]; + private int[][] Big5Freq; - int Big5PFreq[][]; + private int[][] Big5PFreq; - int EUC_TWFreq[][]; + private int[][] EUC_TWFreq; - int KRFreq[][]; + private int[][] KRFreq; - int JPFreq[][]; + private int[][] JPFreq; - // int UnicodeFreq[94][128]; - // public static String[] nicename; - // public static String[] codings; public boolean debug; - public BytesEncodingDetect() { + BytesEncodingDetect() { super(); debug = false; GBFreq = new int[94][94]; @@ -170,7 +168,6 @@ class BytesEncodingDetect extends Encoding { rawtext.length - byteoffset)) > 0) { byteoffset += bytesread; } - ; chinesestream.close(); guess = detectEncoding(rawtext); } catch (Exception e) { @@ -187,12 +184,12 @@ class BytesEncodingDetect extends Encoding { * score for each encoding type. The encoding type with the highest * probability is returned. */ - public int detectEncoding(File testfile) { + int detectEncoding(File testfile) { byte[] rawtext = getFileBytes(testfile); return detectEncoding(rawtext); } - public static byte[] getFileBytes(File testfile) { + static byte[] getFileBytes(File testfile) { FileInputStream chinesefile; byte[] rawtext; rawtext = new byte[2000]; @@ -214,7 +211,7 @@ class BytesEncodingDetect extends Encoding { * it a probability score for each encoding type. The encoding type with the * highest probability is returned. */ - public int detectEncoding(byte[] rawtext) { + int detectEncoding(byte[] rawtext) { int[] scores; int index, maxscore = 0; int encoding_guess = OTHER; @@ -423,15 +420,6 @@ class BytesEncodingDetect extends Encoding { && (byte) 0x30 <= rawtext[i + 3] && rawtext[i + 3] <= (byte) 0x39) { gbchars++; - /* - * totalfreq += 500; row = rawtext[i] + 256 - 0x81; if (0x40 - * <= rawtext[i+1] && rawtext[i+1] <= 0x7E) { column = - * rawtext[i+1] - 0x40; } else { column = rawtext[i+1] + 256 - * - 0x40; } //System.out.println("extended row " + row + " - * column " + column + " rawtext[i] " + rawtext[i]); if - * (GBKFreq[row][column] != 0) { gbfreq += - * GBKFreq[row][column]; } - */ } i++; } @@ -812,33 +800,12 @@ class BytesEncodingDetect extends Encoding { * Unicode, guess based on BOM // NOT VERY GENERAL, NEEDS MUCH MORE WORK */ int utf16_probability(byte[] rawtext) { - // int score = 0; - // int i, rawtextlen = 0; - // int goodbytes = 0, asciibytes = 0; if (rawtext.length > 1 && ((byte) 0xFE == rawtext[0] && (byte) 0xFF == rawtext[1]) || // Big-endian ((byte) 0xFF == rawtext[0] && (byte) 0xFE == rawtext[1])) { // Little-endian return 100; } return 0; - /* - * // Check to see if characters fit into acceptable ranges rawtextlen = - * rawtext.length; for (i = 0; i < rawtextlen; i++) { if ((rawtext[i] & - * (byte)0x7F) == rawtext[i]) { // One byte goodbytes += 1; - * asciibytes++; } else if ((rawtext[i] & (byte)0xDF) == rawtext[i]) { - * // Two bytes if (i+1 < rawtextlen && (rawtext[i+1] & (byte)0xBF) == - * rawtext[i+1]) { goodbytes += 2; i++; } } else if ((rawtext[i] & - * (byte)0xEF) == rawtext[i]) { // Three bytes if (i+2 < rawtextlen && - * (rawtext[i+1] & (byte)0xBF) == rawtext[i+1] && (rawtext[i+2] & - * (byte)0xBF) == rawtext[i+2]) { goodbytes += 3; i+=2; } } } - * - * score = (int)(100 * ((float)goodbytes/(float)rawtext.length)); // An - * all ASCII file is also a good UTF8 file, but I'd rather it // get - * identified as ASCII. Can delete following 3 lines otherwise if - * (goodbytes == asciibytes) { score = 0; } // If not above 90, reduce - * to zero to prevent coincidental matches if (score > 90) { return - * score; } else { return 0; } - */ } /* @@ -1091,22 +1058,16 @@ class BytesEncodingDetect extends Encoding { GBKFreq[i][j] = 0; } } - // for (i = 0; i < 94; i++) { - // for (j = 0; j < 158; j++) { for (i = 93; i >= 0; i--) { for (j = 157; j >= 0; j--) { Big5Freq[i][j] = 0; } } - // for (i = 0; i < 126; i++) { - // for (j = 0; j < 191; j++) { for (i = 125; i >= 0; i--) { for (j = 190; j >= 0; j--) { Big5PFreq[i][j] = 0; } } - // for (i = 0; i < 94; i++) { - // for (j = 0; j < 94; j++) { for (i = 93; i >= 0; i--) { for (j = 93; j >= 0; j--) { EUC_TWFreq[i][j] = 0; @@ -1517,76 +1478,7 @@ class BytesEncodingDetect extends Encoding { GBFreq[20][24] = 202; GBFreq[45][19] = 201; GBFreq[18][53] = 200; - /* - * GBFreq[39][0] = 199; GBFreq[40][71] = 198; GBFreq[41][27] = 197; - * GBFreq[15][69] = 196; GBFreq[42][10] = 195; GBFreq[31][89] = 194; - * GBFreq[51][28] = 193; GBFreq[41][22] = 192; GBFreq[40][43] = 191; - * GBFreq[38][6] = 190; GBFreq[37][11] = 189; GBFreq[39][60] = 188; - * GBFreq[48][47] = 187; GBFreq[46][80] = 186; GBFreq[52][49] = 185; - * GBFreq[50][48] = 184; GBFreq[25][1] = 183; GBFreq[52][29] = 182; - * GBFreq[24][66] = 181; GBFreq[23][35] = 180; GBFreq[49][72] = 179; - * GBFreq[47][45] = 178; GBFreq[45][14] = 177; GBFreq[51][70] = 176; - * GBFreq[22][30] = 175; GBFreq[49][83] = 174; GBFreq[26][79] = 173; - * GBFreq[27][41] = 172; GBFreq[51][81] = 171; GBFreq[41][54] = 170; - * GBFreq[20][4] = 169; GBFreq[29][60] = 168; GBFreq[20][27] = 167; - * GBFreq[50][15] = 166; GBFreq[41][6] = 165; GBFreq[35][34] = 164; - * GBFreq[44][87] = 163; GBFreq[46][66] = 162; GBFreq[42][37] = 161; - * GBFreq[42][24] = 160; GBFreq[54][7] = 159; GBFreq[41][14] = 158; - * GBFreq[39][83] = 157; GBFreq[16][87] = 156; GBFreq[20][59] = 155; - * GBFreq[42][12] = 154; GBFreq[47][2] = 153; GBFreq[21][32] = 152; - * GBFreq[53][29] = 151; GBFreq[22][40] = 150; GBFreq[24][58] = 149; - * GBFreq[52][88] = 148; GBFreq[29][30] = 147; GBFreq[15][91] = 146; - * GBFreq[54][72] = 145; GBFreq[51][75] = 144; GBFreq[33][67] = 143; - * GBFreq[41][50] = 142; GBFreq[27][34] = 141; GBFreq[46][17] = 140; - * GBFreq[31][74] = 139; GBFreq[42][67] = 138; GBFreq[54][87] = 137; - * GBFreq[27][14] = 136; GBFreq[16][63] = 135; GBFreq[16][5] = 134; - * GBFreq[43][23] = 133; GBFreq[23][13] = 132; GBFreq[31][12] = 131; - * GBFreq[25][57] = 130; GBFreq[38][49] = 129; GBFreq[42][69] = 128; - * GBFreq[23][80] = 127; GBFreq[29][0] = 126; GBFreq[28][2] = 125; - * GBFreq[28][17] = 124; GBFreq[17][27] = 123; GBFreq[40][16] = 122; - * GBFreq[45][1] = 121; GBFreq[36][33] = 120; GBFreq[35][23] = 119; - * GBFreq[20][86] = 118; GBFreq[29][53] = 117; GBFreq[23][88] = 116; - * GBFreq[51][87] = 115; GBFreq[54][27] = 114; GBFreq[44][36] = 113; - * GBFreq[21][45] = 112; GBFreq[53][52] = 111; GBFreq[31][53] = 110; - * GBFreq[38][47] = 109; GBFreq[27][21] = 108; GBFreq[30][42] = 107; - * GBFreq[29][10] = 106; GBFreq[35][35] = 105; GBFreq[24][56] = 104; - * GBFreq[41][29] = 103; GBFreq[18][68] = 102; GBFreq[29][24] = 101; - * GBFreq[25][84] = 100; GBFreq[35][47] = 99; GBFreq[29][56] = 98; - * GBFreq[30][44] = 97; GBFreq[53][3] = 96; GBFreq[30][63] = 95; - * GBFreq[52][52] = 94; GBFreq[54][1] = 93; GBFreq[22][48] = 92; - * GBFreq[54][66] = 91; GBFreq[21][90] = 90; GBFreq[52][47] = 89; - * GBFreq[39][25] = 88; GBFreq[39][39] = 87; GBFreq[44][37] = 86; - * GBFreq[44][76] = 85; GBFreq[46][75] = 84; GBFreq[18][37] = 83; - * GBFreq[47][42] = 82; GBFreq[19][92] = 81; GBFreq[51][27] = 80; - * GBFreq[48][83] = 79; GBFreq[23][70] = 78; GBFreq[29][9] = 77; - * GBFreq[33][79] = 76; GBFreq[52][90] = 75; GBFreq[53][6] = 74; - * GBFreq[24][36] = 73; GBFreq[25][25] = 72; GBFreq[44][26] = 71; - * GBFreq[25][36] = 70; GBFreq[29][87] = 69; GBFreq[48][0] = 68; - * GBFreq[15][40] = 67; GBFreq[17][45] = 66; GBFreq[30][14] = 65; - * GBFreq[48][38] = 64; GBFreq[23][19] = 63; GBFreq[40][42] = 62; - * GBFreq[31][63] = 61; GBFreq[16][23] = 60; GBFreq[26][21] = 59; - * GBFreq[32][76] = 58; GBFreq[23][58] = 57; GBFreq[41][37] = 56; - * GBFreq[30][43] = 55; GBFreq[47][38] = 54; GBFreq[21][46] = 53; - * GBFreq[18][33] = 52; GBFreq[52][37] = 51; GBFreq[36][8] = 50; - * GBFreq[49][24] = 49; GBFreq[15][66] = 48; GBFreq[35][77] = 47; - * GBFreq[27][58] = 46; GBFreq[35][51] = 45; GBFreq[24][69] = 44; - * GBFreq[20][54] = 43; GBFreq[24][41] = 42; GBFreq[41][0] = 41; - * GBFreq[33][71] = 40; GBFreq[23][52] = 39; GBFreq[29][67] = 38; - * GBFreq[46][51] = 37; GBFreq[46][90] = 36; GBFreq[49][33] = 35; - * GBFreq[33][28] = 34; GBFreq[37][86] = 33; GBFreq[39][22] = 32; - * GBFreq[37][37] = 31; GBFreq[29][62] = 30; GBFreq[29][50] = 29; - * GBFreq[36][89] = 28; GBFreq[42][44] = 27; GBFreq[51][82] = 26; - * GBFreq[28][83] = 25; GBFreq[15][78] = 24; GBFreq[46][62] = 23; - * GBFreq[19][69] = 22; GBFreq[51][23] = 21; GBFreq[37][69] = 20; - * GBFreq[25][5] = 19; GBFreq[51][85] = 18; GBFreq[48][77] = 17; - * GBFreq[32][46] = 16; GBFreq[53][60] = 15; GBFreq[28][57] = 14; - * GBFreq[54][82] = 13; GBFreq[54][15] = 12; GBFreq[49][54] = 11; - * GBFreq[53][87] = 10; GBFreq[27][16] = 9; GBFreq[29][34] = 8; - * GBFreq[20][44] = 7; GBFreq[42][73] = 6; GBFreq[47][71] = 5; - * GBFreq[29][37] = 4; GBFreq[25][50] = 3; GBFreq[18][84] = 2; - * GBFreq[50][45] = 1; GBFreq[48][46] = 0; - */ - // GBFreq[43][89] = -1; GBFreq[54][68] = -2; + Big5Freq[9][89] = 600; Big5Freq[11][15] = 599; Big5Freq[3][66] = 598; @@ -1987,81 +1879,7 @@ class BytesEncodingDetect extends Encoding { Big5Freq[26][124] = 203; Big5Freq[4][19] = 202; Big5Freq[9][152] = 201; - /* - * Big5Freq[5][0] = 200; Big5Freq[26][57] = 199; Big5Freq[13][155] = - * 198; Big5Freq[3][38] = 197; Big5Freq[9][155] = 196; Big5Freq[28][53] - * = 195; Big5Freq[15][71] = 194; Big5Freq[21][95] = 193; - * Big5Freq[15][112] = 192; Big5Freq[14][138] = 191; Big5Freq[8][18] = - * 190; Big5Freq[20][151] = 189; Big5Freq[37][27] = 188; - * Big5Freq[32][48] = 187; Big5Freq[23][66] = 186; Big5Freq[9][2] = 185; - * Big5Freq[13][133] = 184; Big5Freq[7][127] = 183; Big5Freq[3][11] = - * 182; Big5Freq[12][118] = 181; Big5Freq[13][101] = 180; - * Big5Freq[30][153] = 179; Big5Freq[4][65] = 178; Big5Freq[5][25] = - * 177; Big5Freq[5][140] = 176; Big5Freq[6][25] = 175; Big5Freq[4][52] = - * 174; Big5Freq[30][156] = 173; Big5Freq[16][13] = 172; Big5Freq[21][8] - * = 171; Big5Freq[19][74] = 170; Big5Freq[15][145] = 169; - * Big5Freq[9][15] = 168; Big5Freq[13][82] = 167; Big5Freq[26][86] = - * 166; Big5Freq[18][52] = 165; Big5Freq[6][109] = 164; Big5Freq[10][99] - * = 163; Big5Freq[18][101] = 162; Big5Freq[25][49] = 161; - * Big5Freq[31][79] = 160; Big5Freq[28][20] = 159; Big5Freq[12][115] = - * 158; Big5Freq[15][66] = 157; Big5Freq[11][104] = 156; - * Big5Freq[23][106] = 155; Big5Freq[34][157] = 154; Big5Freq[32][94] = - * 153; Big5Freq[29][88] = 152; Big5Freq[10][46] = 151; - * Big5Freq[13][118] = 150; Big5Freq[20][37] = 149; Big5Freq[12][30] = - * 148; Big5Freq[21][4] = 147; Big5Freq[16][33] = 146; Big5Freq[13][52] - * = 145; Big5Freq[4][7] = 144; Big5Freq[21][49] = 143; Big5Freq[3][27] - * = 142; Big5Freq[16][91] = 141; Big5Freq[5][155] = 140; - * Big5Freq[29][130] = 139; Big5Freq[3][125] = 138; Big5Freq[14][26] = - * 137; Big5Freq[15][39] = 136; Big5Freq[24][110] = 135; - * Big5Freq[7][141] = 134; Big5Freq[21][15] = 133; Big5Freq[32][104] = - * 132; Big5Freq[8][31] = 131; Big5Freq[34][112] = 130; Big5Freq[10][75] - * = 129; Big5Freq[21][23] = 128; Big5Freq[34][131] = 127; - * Big5Freq[12][3] = 126; Big5Freq[10][62] = 125; Big5Freq[9][120] = - * 124; Big5Freq[32][149] = 123; Big5Freq[8][44] = 122; Big5Freq[24][2] - * = 121; Big5Freq[6][148] = 120; Big5Freq[15][103] = 119; - * Big5Freq[36][54] = 118; Big5Freq[36][134] = 117; Big5Freq[11][7] = - * 116; Big5Freq[3][90] = 115; Big5Freq[36][73] = 114; Big5Freq[8][102] - * = 113; Big5Freq[12][87] = 112; Big5Freq[25][64] = 111; Big5Freq[9][1] - * = 110; Big5Freq[24][121] = 109; Big5Freq[5][75] = 108; - * Big5Freq[17][83] = 107; Big5Freq[18][57] = 106; Big5Freq[8][95] = - * 105; Big5Freq[14][36] = 104; Big5Freq[28][113] = 103; - * Big5Freq[12][56] = 102; Big5Freq[14][61] = 101; Big5Freq[25][138] = - * 100; Big5Freq[4][34] = 99; Big5Freq[11][152] = 98; Big5Freq[35][0] = - * 97; Big5Freq[4][15] = 96; Big5Freq[8][82] = 95; Big5Freq[20][73] = - * 94; Big5Freq[25][52] = 93; Big5Freq[24][6] = 92; Big5Freq[21][78] = - * 91; Big5Freq[17][32] = 90; Big5Freq[17][91] = 89; Big5Freq[5][76] = - * 88; Big5Freq[15][60] = 87; Big5Freq[15][150] = 86; Big5Freq[5][80] = - * 85; Big5Freq[15][81] = 84; Big5Freq[28][108] = 83; Big5Freq[18][14] = - * 82; Big5Freq[19][109] = 81; Big5Freq[28][133] = 80; Big5Freq[21][97] - * = 79; Big5Freq[5][105] = 78; Big5Freq[18][114] = 77; Big5Freq[16][95] - * = 76; Big5Freq[5][51] = 75; Big5Freq[3][148] = 74; Big5Freq[22][102] - * = 73; Big5Freq[4][123] = 72; Big5Freq[8][88] = 71; Big5Freq[25][111] - * = 70; Big5Freq[8][149] = 69; Big5Freq[9][48] = 68; Big5Freq[16][126] - * = 67; Big5Freq[33][150] = 66; Big5Freq[9][54] = 65; Big5Freq[29][104] - * = 64; Big5Freq[3][3] = 63; Big5Freq[11][49] = 62; Big5Freq[24][109] = - * 61; Big5Freq[28][116] = 60; Big5Freq[34][113] = 59; Big5Freq[5][3] = - * 58; Big5Freq[21][106] = 57; Big5Freq[4][98] = 56; Big5Freq[12][135] = - * 55; Big5Freq[16][101] = 54; Big5Freq[12][147] = 53; Big5Freq[27][55] - * = 52; Big5Freq[3][5] = 51; Big5Freq[11][101] = 50; Big5Freq[16][157] - * = 49; Big5Freq[22][114] = 48; Big5Freq[18][46] = 47; Big5Freq[4][29] - * = 46; Big5Freq[8][103] = 45; Big5Freq[16][151] = 44; Big5Freq[8][29] - * = 43; Big5Freq[15][114] = 42; Big5Freq[22][70] = 41; - * Big5Freq[13][121] = 40; Big5Freq[7][112] = 39; Big5Freq[20][83] = 38; - * Big5Freq[3][36] = 37; Big5Freq[10][103] = 36; Big5Freq[3][96] = 35; - * Big5Freq[21][79] = 34; Big5Freq[25][120] = 33; Big5Freq[29][121] = - * 32; Big5Freq[23][71] = 31; Big5Freq[21][22] = 30; Big5Freq[18][89] = - * 29; Big5Freq[25][104] = 28; Big5Freq[10][124] = 27; Big5Freq[26][4] = - * 26; Big5Freq[21][136] = 25; Big5Freq[6][112] = 24; Big5Freq[12][103] - * = 23; Big5Freq[17][66] = 22; Big5Freq[13][151] = 21; - * Big5Freq[33][152] = 20; Big5Freq[11][148] = 19; Big5Freq[13][57] = - * 18; Big5Freq[13][41] = 17; Big5Freq[7][60] = 16; Big5Freq[21][29] = - * 15; Big5Freq[9][157] = 14; Big5Freq[24][95] = 13; Big5Freq[15][148] = - * 12; Big5Freq[15][122] = 11; Big5Freq[6][125] = 10; Big5Freq[11][25] = - * 9; Big5Freq[20][55] = 8; Big5Freq[19][84] = 7; Big5Freq[21][82] = 6; - * Big5Freq[24][3] = 5; Big5Freq[13][70] = 4; Big5Freq[6][21] = 3; - * Big5Freq[21][86] = 2; Big5Freq[12][23] = 1; Big5Freq[3][85] = 0; - * EUC_TWFreq[45][90] = 600; - */ + Big5PFreq[41][122] = 600; Big5PFreq[35][0] = 599; Big5PFreq[43][15] = 598; @@ -3062,94 +2880,7 @@ class BytesEncodingDetect extends Encoding { EUC_TWFreq[74][69] = 203; EUC_TWFreq[36][82] = 202; EUC_TWFreq[46][59] = 201; - /* - * EUC_TWFreq[38][32] = 200; EUC_TWFreq[74][2] = 199; EUC_TWFreq[53][31] - * = 198; EUC_TWFreq[35][38] = 197; EUC_TWFreq[46][62] = 196; - * EUC_TWFreq[77][31] = 195; EUC_TWFreq[55][74] = 194; EUC_TWFreq[66][6] - * = 193; EUC_TWFreq[56][21] = 192; EUC_TWFreq[54][78] = 191; - * EUC_TWFreq[43][51] = 190; EUC_TWFreq[64][93] = 189; EUC_TWFreq[92][7] - * = 188; EUC_TWFreq[83][89] = 187; EUC_TWFreq[69][9] = 186; - * EUC_TWFreq[45][4] = 185; EUC_TWFreq[53][9] = 184; EUC_TWFreq[43][2] = - * 183; EUC_TWFreq[35][11] = 182; EUC_TWFreq[51][25] = 181; - * EUC_TWFreq[52][71] = 180; EUC_TWFreq[81][67] = 179; - * EUC_TWFreq[37][33] = 178; EUC_TWFreq[38][57] = 177; - * EUC_TWFreq[39][77] = 176; EUC_TWFreq[40][26] = 175; - * EUC_TWFreq[37][21] = 174; EUC_TWFreq[81][70] = 173; - * EUC_TWFreq[56][80] = 172; EUC_TWFreq[65][14] = 171; - * EUC_TWFreq[62][47] = 170; EUC_TWFreq[56][54] = 169; - * EUC_TWFreq[45][17] = 168; EUC_TWFreq[52][52] = 167; - * EUC_TWFreq[74][30] = 166; EUC_TWFreq[60][57] = 165; - * EUC_TWFreq[41][15] = 164; EUC_TWFreq[47][69] = 163; - * EUC_TWFreq[61][11] = 162; EUC_TWFreq[72][25] = 161; - * EUC_TWFreq[82][56] = 160; EUC_TWFreq[76][92] = 159; - * EUC_TWFreq[51][22] = 158; EUC_TWFreq[55][69] = 157; - * EUC_TWFreq[49][43] = 156; EUC_TWFreq[69][49] = 155; - * EUC_TWFreq[88][42] = 154; EUC_TWFreq[84][41] = 153; - * EUC_TWFreq[79][33] = 152; EUC_TWFreq[47][17] = 151; - * EUC_TWFreq[52][88] = 150; EUC_TWFreq[63][74] = 149; - * EUC_TWFreq[50][32] = 148; EUC_TWFreq[65][10] = 147; EUC_TWFreq[57][6] - * = 146; EUC_TWFreq[52][23] = 145; EUC_TWFreq[36][70] = 144; - * EUC_TWFreq[65][55] = 143; EUC_TWFreq[35][27] = 142; - * EUC_TWFreq[57][63] = 141; EUC_TWFreq[39][92] = 140; - * EUC_TWFreq[79][75] = 139; EUC_TWFreq[36][30] = 138; - * EUC_TWFreq[53][60] = 137; EUC_TWFreq[55][43] = 136; - * EUC_TWFreq[71][22] = 135; EUC_TWFreq[43][16] = 134; - * EUC_TWFreq[65][21] = 133; EUC_TWFreq[84][51] = 132; - * EUC_TWFreq[43][64] = 131; EUC_TWFreq[87][91] = 130; - * EUC_TWFreq[47][45] = 129; EUC_TWFreq[65][29] = 128; - * EUC_TWFreq[88][16] = 127; EUC_TWFreq[50][5] = 126; EUC_TWFreq[47][33] - * = 125; EUC_TWFreq[46][27] = 124; EUC_TWFreq[85][2] = 123; - * EUC_TWFreq[43][77] = 122; EUC_TWFreq[70][9] = 121; EUC_TWFreq[41][54] - * = 120; EUC_TWFreq[56][12] = 119; EUC_TWFreq[90][65] = 118; - * EUC_TWFreq[91][50] = 117; EUC_TWFreq[48][41] = 116; - * EUC_TWFreq[35][89] = 115; EUC_TWFreq[90][83] = 114; - * EUC_TWFreq[44][40] = 113; EUC_TWFreq[50][88] = 112; - * EUC_TWFreq[72][39] = 111; EUC_TWFreq[45][3] = 110; EUC_TWFreq[71][33] - * = 109; EUC_TWFreq[39][12] = 108; EUC_TWFreq[59][24] = 107; - * EUC_TWFreq[60][62] = 106; EUC_TWFreq[44][33] = 105; - * EUC_TWFreq[53][70] = 104; EUC_TWFreq[77][90] = 103; - * EUC_TWFreq[50][58] = 102; EUC_TWFreq[54][1] = 101; EUC_TWFreq[73][19] - * = 100; EUC_TWFreq[37][3] = 99; EUC_TWFreq[49][91] = 98; - * EUC_TWFreq[88][43] = 97; EUC_TWFreq[36][78] = 96; EUC_TWFreq[44][20] - * = 95; EUC_TWFreq[64][15] = 94; EUC_TWFreq[72][28] = 93; - * EUC_TWFreq[70][13] = 92; EUC_TWFreq[65][83] = 91; EUC_TWFreq[58][68] - * = 90; EUC_TWFreq[59][32] = 89; EUC_TWFreq[39][13] = 88; - * EUC_TWFreq[55][64] = 87; EUC_TWFreq[56][59] = 86; EUC_TWFreq[39][17] - * = 85; EUC_TWFreq[55][84] = 84; EUC_TWFreq[77][85] = 83; - * EUC_TWFreq[60][19] = 82; EUC_TWFreq[62][82] = 81; EUC_TWFreq[78][16] - * = 80; EUC_TWFreq[66][8] = 79; EUC_TWFreq[39][42] = 78; - * EUC_TWFreq[61][24] = 77; EUC_TWFreq[57][67] = 76; EUC_TWFreq[38][83] - * = 75; EUC_TWFreq[36][53] = 74; EUC_TWFreq[67][76] = 73; - * EUC_TWFreq[37][91] = 72; EUC_TWFreq[44][26] = 71; EUC_TWFreq[72][86] - * = 70; EUC_TWFreq[44][87] = 69; EUC_TWFreq[45][50] = 68; - * EUC_TWFreq[58][4] = 67; EUC_TWFreq[86][65] = 66; EUC_TWFreq[45][56] = - * 65; EUC_TWFreq[79][49] = 64; EUC_TWFreq[35][3] = 63; - * EUC_TWFreq[48][83] = 62; EUC_TWFreq[71][21] = 61; EUC_TWFreq[77][93] - * = 60; EUC_TWFreq[87][92] = 59; EUC_TWFreq[38][35] = 58; - * EUC_TWFreq[66][17] = 57; EUC_TWFreq[37][66] = 56; EUC_TWFreq[51][42] - * = 55; EUC_TWFreq[57][73] = 54; EUC_TWFreq[51][54] = 53; - * EUC_TWFreq[75][64] = 52; EUC_TWFreq[35][5] = 51; EUC_TWFreq[49][40] = - * 50; EUC_TWFreq[58][35] = 49; EUC_TWFreq[67][88] = 48; - * EUC_TWFreq[60][51] = 47; EUC_TWFreq[36][92] = 46; EUC_TWFreq[44][41] - * = 45; EUC_TWFreq[58][29] = 44; EUC_TWFreq[43][62] = 43; - * EUC_TWFreq[56][23] = 42; EUC_TWFreq[67][44] = 41; EUC_TWFreq[52][91] - * = 40; EUC_TWFreq[42][81] = 39; EUC_TWFreq[64][25] = 38; - * EUC_TWFreq[35][36] = 37; EUC_TWFreq[47][73] = 36; EUC_TWFreq[36][1] = - * 35; EUC_TWFreq[65][84] = 34; EUC_TWFreq[73][1] = 33; - * EUC_TWFreq[79][66] = 32; EUC_TWFreq[69][14] = 31; EUC_TWFreq[65][28] - * = 30; EUC_TWFreq[60][93] = 29; EUC_TWFreq[72][79] = 28; - * EUC_TWFreq[48][0] = 27; EUC_TWFreq[73][43] = 26; EUC_TWFreq[66][47] = - * 25; EUC_TWFreq[41][18] = 24; EUC_TWFreq[51][10] = 23; - * EUC_TWFreq[59][7] = 22; EUC_TWFreq[53][27] = 21; EUC_TWFreq[86][67] = - * 20; EUC_TWFreq[49][87] = 19; EUC_TWFreq[52][28] = 18; - * EUC_TWFreq[52][12] = 17; EUC_TWFreq[42][30] = 16; EUC_TWFreq[65][35] - * = 15; EUC_TWFreq[46][64] = 14; EUC_TWFreq[71][7] = 13; - * EUC_TWFreq[56][57] = 12; EUC_TWFreq[56][31] = 11; EUC_TWFreq[41][31] - * = 10; EUC_TWFreq[48][59] = 9; EUC_TWFreq[63][92] = 8; - * EUC_TWFreq[62][57] = 7; EUC_TWFreq[65][87] = 6; EUC_TWFreq[70][10] = - * 5; EUC_TWFreq[52][40] = 4; EUC_TWFreq[40][22] = 3; EUC_TWFreq[65][91] - * = 2; EUC_TWFreq[50][25] = 1; EUC_TWFreq[35][84] = 0; - */ + GBKFreq[52][132] = 600; GBKFreq[73][135] = 599; GBKFreq[49][123] = 598; @@ -3452,113 +3183,7 @@ class BytesEncodingDetect extends Encoding { GBKFreq[58][174] = 301; GBKFreq[80][144] = 300; GBKFreq[85][113] = 299; - /* - * GBKFreq[83][15] = 298; GBKFreq[105][80] = 297; GBKFreq[7][179] = 296; - * GBKFreq[93][4] = 295; GBKFreq[123][40] = 294; GBKFreq[85][120] = 293; - * GBKFreq[77][165] = 292; GBKFreq[86][67] = 291; GBKFreq[25][162] = - * 290; GBKFreq[77][183] = 289; GBKFreq[83][71] = 288; GBKFreq[78][99] = - * 287; GBKFreq[72][177] = 286; GBKFreq[71][97] = 285; GBKFreq[58][111] - * = 284; GBKFreq[77][175] = 283; GBKFreq[76][181] = 282; - * GBKFreq[71][142] = 281; GBKFreq[64][150] = 280; GBKFreq[5][142] = - * 279; GBKFreq[73][128] = 278; GBKFreq[73][156] = 277; GBKFreq[60][188] - * = 276; GBKFreq[64][56] = 275; GBKFreq[74][128] = 274; - * GBKFreq[48][163] = 273; GBKFreq[54][116] = 272; GBKFreq[73][127] = - * 271; GBKFreq[16][176] = 270; GBKFreq[62][149] = 269; GBKFreq[105][96] - * = 268; GBKFreq[55][186] = 267; GBKFreq[4][51] = 266; GBKFreq[48][113] - * = 265; GBKFreq[48][152] = 264; GBKFreq[23][9] = 263; GBKFreq[56][102] - * = 262; GBKFreq[11][81] = 261; GBKFreq[82][112] = 260; GBKFreq[65][85] - * = 259; GBKFreq[69][125] = 258; GBKFreq[68][31] = 257; GBKFreq[5][20] - * = 256; GBKFreq[60][176] = 255; GBKFreq[82][81] = 254; - * GBKFreq[72][107] = 253; GBKFreq[3][52] = 252; GBKFreq[71][157] = 251; - * GBKFreq[24][46] = 250; GBKFreq[69][108] = 249; GBKFreq[78][178] = - * 248; GBKFreq[9][69] = 247; GBKFreq[73][144] = 246; GBKFreq[63][187] = - * 245; GBKFreq[68][36] = 244; GBKFreq[47][151] = 243; GBKFreq[14][74] = - * 242; GBKFreq[47][114] = 241; GBKFreq[80][171] = 240; GBKFreq[75][152] - * = 239; GBKFreq[86][40] = 238; GBKFreq[93][43] = 237; GBKFreq[2][50] = - * 236; GBKFreq[62][66] = 235; GBKFreq[1][183] = 234; GBKFreq[74][124] = - * 233; GBKFreq[58][104] = 232; GBKFreq[83][106] = 231; GBKFreq[60][144] - * = 230; GBKFreq[48][99] = 229; GBKFreq[54][157] = 228; - * GBKFreq[70][179] = 227; GBKFreq[61][127] = 226; GBKFreq[57][135] = - * 225; GBKFreq[59][190] = 224; GBKFreq[77][116] = 223; GBKFreq[26][17] - * = 222; GBKFreq[60][13] = 221; GBKFreq[71][38] = 220; GBKFreq[85][177] - * = 219; GBKFreq[59][73] = 218; GBKFreq[50][150] = 217; - * GBKFreq[79][102] = 216; GBKFreq[76][118] = 215; GBKFreq[67][132] = - * 214; GBKFreq[73][146] = 213; GBKFreq[83][184] = 212; GBKFreq[86][159] - * = 211; GBKFreq[95][120] = 210; GBKFreq[23][139] = 209; - * GBKFreq[64][183] = 208; GBKFreq[85][103] = 207; GBKFreq[41][90] = - * 206; GBKFreq[87][72] = 205; GBKFreq[62][104] = 204; GBKFreq[79][168] - * = 203; GBKFreq[79][150] = 202; GBKFreq[104][20] = 201; - * GBKFreq[56][114] = 200; GBKFreq[84][26] = 199; GBKFreq[57][99] = 198; - * GBKFreq[62][154] = 197; GBKFreq[47][98] = 196; GBKFreq[61][64] = 195; - * GBKFreq[112][18] = 194; GBKFreq[123][19] = 193; GBKFreq[4][98] = 192; - * GBKFreq[47][163] = 191; GBKFreq[66][188] = 190; GBKFreq[81][85] = - * 189; GBKFreq[82][30] = 188; GBKFreq[65][83] = 187; GBKFreq[67][24] = - * 186; GBKFreq[68][179] = 185; GBKFreq[55][177] = 184; GBKFreq[2][122] - * = 183; GBKFreq[47][139] = 182; GBKFreq[79][158] = 181; - * GBKFreq[64][143] = 180; GBKFreq[100][24] = 179; GBKFreq[73][103] = - * 178; GBKFreq[50][148] = 177; GBKFreq[86][97] = 176; GBKFreq[59][116] - * = 175; GBKFreq[64][173] = 174; GBKFreq[99][91] = 173; GBKFreq[11][99] - * = 172; GBKFreq[78][179] = 171; GBKFreq[18][17] = 170; - * GBKFreq[58][185] = 169; GBKFreq[47][165] = 168; GBKFreq[67][131] = - * 167; GBKFreq[94][40] = 166; GBKFreq[74][153] = 165; GBKFreq[79][142] - * = 164; GBKFreq[57][98] = 163; GBKFreq[1][164] = 162; GBKFreq[55][168] - * = 161; GBKFreq[13][141] = 160; GBKFreq[51][31] = 159; - * GBKFreq[57][178] = 158; GBKFreq[50][189] = 157; GBKFreq[60][167] = - * 156; GBKFreq[80][34] = 155; GBKFreq[109][80] = 154; GBKFreq[85][54] = - * 153; GBKFreq[69][183] = 152; GBKFreq[67][143] = 151; GBKFreq[47][120] - * = 150; GBKFreq[45][75] = 149; GBKFreq[82][98] = 148; GBKFreq[83][22] - * = 147; GBKFreq[13][103] = 146; GBKFreq[49][174] = 145; - * GBKFreq[57][181] = 144; GBKFreq[64][127] = 143; GBKFreq[61][131] = - * 142; GBKFreq[52][180] = 141; GBKFreq[74][134] = 140; GBKFreq[84][187] - * = 139; GBKFreq[81][189] = 138; GBKFreq[47][160] = 137; - * GBKFreq[66][148] = 136; GBKFreq[7][4] = 135; GBKFreq[85][134] = 134; - * GBKFreq[88][13] = 133; GBKFreq[88][80] = 132; GBKFreq[69][166] = 131; - * GBKFreq[86][18] = 130; GBKFreq[79][141] = 129; GBKFreq[50][108] = - * 128; GBKFreq[94][69] = 127; GBKFreq[81][110] = 126; GBKFreq[69][119] - * = 125; GBKFreq[72][161] = 124; GBKFreq[106][45] = 123; - * GBKFreq[73][124] = 122; GBKFreq[94][28] = 121; GBKFreq[63][174] = - * 120; GBKFreq[3][149] = 119; GBKFreq[24][160] = 118; GBKFreq[113][94] - * = 117; GBKFreq[56][138] = 116; GBKFreq[64][185] = 115; - * GBKFreq[86][56] = 114; GBKFreq[56][150] = 113; GBKFreq[110][55] = - * 112; GBKFreq[28][13] = 111; GBKFreq[54][190] = 110; GBKFreq[8][180] = - * 109; GBKFreq[73][149] = 108; GBKFreq[80][155] = 107; GBKFreq[83][172] - * = 106; GBKFreq[67][174] = 105; GBKFreq[64][180] = 104; - * GBKFreq[84][46] = 103; GBKFreq[91][74] = 102; GBKFreq[69][134] = 101; - * GBKFreq[61][107] = 100; GBKFreq[47][171] = 99; GBKFreq[59][51] = 98; - * GBKFreq[109][74] = 97; GBKFreq[64][174] = 96; GBKFreq[52][151] = 95; - * GBKFreq[51][176] = 94; GBKFreq[80][157] = 93; GBKFreq[94][31] = 92; - * GBKFreq[79][155] = 91; GBKFreq[72][174] = 90; GBKFreq[69][113] = 89; - * GBKFreq[83][167] = 88; GBKFreq[83][122] = 87; GBKFreq[8][178] = 86; - * GBKFreq[70][186] = 85; GBKFreq[59][153] = 84; GBKFreq[84][68] = 83; - * GBKFreq[79][39] = 82; GBKFreq[47][180] = 81; GBKFreq[88][53] = 80; - * GBKFreq[57][154] = 79; GBKFreq[47][153] = 78; GBKFreq[3][153] = 77; - * GBKFreq[76][134] = 76; GBKFreq[51][166] = 75; GBKFreq[58][176] = 74; - * GBKFreq[27][138] = 73; GBKFreq[73][126] = 72; GBKFreq[76][185] = 71; - * GBKFreq[52][186] = 70; GBKFreq[81][151] = 69; GBKFreq[26][50] = 68; - * GBKFreq[76][173] = 67; GBKFreq[106][56] = 66; GBKFreq[85][142] = 65; - * GBKFreq[11][103] = 64; GBKFreq[69][159] = 63; GBKFreq[53][142] = 62; - * GBKFreq[7][6] = 61; GBKFreq[84][59] = 60; GBKFreq[86][3] = 59; - * GBKFreq[64][144] = 58; GBKFreq[1][187] = 57; GBKFreq[82][128] = 56; - * GBKFreq[3][66] = 55; GBKFreq[68][133] = 54; GBKFreq[55][167] = 53; - * GBKFreq[52][130] = 52; GBKFreq[61][133] = 51; GBKFreq[72][181] = 50; - * GBKFreq[25][98] = 49; GBKFreq[84][149] = 48; GBKFreq[91][91] = 47; - * GBKFreq[47][188] = 46; GBKFreq[68][130] = 45; GBKFreq[22][44] = 44; - * GBKFreq[81][121] = 43; GBKFreq[72][140] = 42; GBKFreq[55][133] = 41; - * GBKFreq[55][185] = 40; GBKFreq[56][105] = 39; GBKFreq[60][30] = 38; - * GBKFreq[70][103] = 37; GBKFreq[62][141] = 36; GBKFreq[70][144] = 35; - * GBKFreq[59][111] = 34; GBKFreq[54][17] = 33; GBKFreq[18][190] = 32; - * GBKFreq[65][164] = 31; GBKFreq[83][125] = 30; GBKFreq[61][121] = 29; - * GBKFreq[48][13] = 28; GBKFreq[51][189] = 27; GBKFreq[65][68] = 26; - * GBKFreq[7][0] = 25; GBKFreq[76][188] = 24; GBKFreq[85][117] = 23; - * GBKFreq[45][33] = 22; GBKFreq[78][187] = 21; GBKFreq[106][48] = 20; - * GBKFreq[59][52] = 19; GBKFreq[86][185] = 18; GBKFreq[84][121] = 17; - * GBKFreq[82][189] = 16; GBKFreq[68][156] = 15; GBKFreq[55][125] = 14; - * GBKFreq[65][175] = 13; GBKFreq[7][140] = 12; GBKFreq[50][106] = 11; - * GBKFreq[59][124] = 10; GBKFreq[67][115] = 9; GBKFreq[82][114] = 8; - * GBKFreq[74][121] = 7; GBKFreq[106][69] = 6; GBKFreq[94][27] = 5; - * GBKFreq[78][98] = 4; GBKFreq[85][186] = 3; GBKFreq[108][90] = 2; - * GBKFreq[62][160] = 1; GBKFreq[60][169] = 0; - */ + KRFreq[31][43] = 600; KRFreq[19][56] = 599; KRFreq[38][46] = 598; @@ -4766,71 +4391,47 @@ class BytesEncodingDetect extends Encoding { class Encoding { // Supported Encoding Types - public static int GB2312 = 0; - - public static int GBK = 1; - - public static int GB18030 = 2; - - public static int HZ = 3; - - public static int BIG5 = 4; - - public static int CNS11643 = 5; - - public static int UTF8 = 6; - - public static int UTF8T = 7; - - public static int UTF8S = 8; - - public static int UNICODE = 9; - - public static int UNICODET = 10; - - public static int UNICODES = 11; - - public static int ISO2022CN = 12; - - public static int ISO2022CN_CNS = 13; - - public static int ISO2022CN_GB = 14; - - public static int EUC_KR = 15; - - public static int CP949 = 16; - - public static int ISO2022KR = 17; - - public static int JOHAB = 18; - - public static int SJIS = 19; - - public static int EUC_JP = 20; - - public static int ISO2022JP = 21; - - public static int ASCII = 22; - - public static int OTHER = 23; - - public static int TOTALTYPES = 24; + static int GB2312 = 0; + static int GBK = 1; + static int GB18030 = 2; + static int HZ = 3; + static int BIG5 = 4; + static int CNS11643 = 5; + static int UTF8 = 6; + static int UTF8T = 7; + static int UTF8S = 8; + static int UNICODE = 9; + static int UNICODET = 10; + static int UNICODES = 11; + static int ISO2022CN = 12; + static int ISO2022CN_CNS = 13; + static int ISO2022CN_GB = 14; + static int EUC_KR = 15; + static int CP949 = 16; + static int ISO2022KR = 17; + static int JOHAB = 18; + static int SJIS = 19; + static int EUC_JP = 20; + static int ISO2022JP = 21; + static int ASCII = 22; + static int OTHER = 23; + static int TOTALTYPES = 24; public final static int SIMP = 0; public final static int TRAD = 1; // Names of the encodings as understood by Java - public static String[] javaname; + static String[] javaname; // Names of the encodings for human viewing - public static String[] nicename; + static String[] nicename; // Names of charsets as used in charset parameter of HTML Meta tag - public static String[] htmlname; + static String[] htmlname; // Constructor - public Encoding() { + Encoding() { javaname = new String[TOTALTYPES]; nicename = new String[TOTALTYPES]; htmlname = new String[TOTALTYPES]; diff --git a/app/src/main/java/io/legado/app/utils/FileUtils.kt b/app/src/main/java/io/legado/app/utils/FileUtils.kt index 39148fe76..a9408dd10 100644 --- a/app/src/main/java/io/legado/app/utils/FileUtils.kt +++ b/app/src/main/java/io/legado/app/utils/FileUtils.kt @@ -1,206 +1,103 @@ package io.legado.app.utils -import android.content.ContentUris -import android.content.Context -import android.net.Uri import android.os.Environment -import android.os.storage.StorageManager -import android.provider.DocumentsContract -import android.provider.MediaStore -import android.util.Log -import androidx.core.content.ContextCompat +import io.legado.app.App import java.io.File import java.io.IOException -import java.lang.reflect.Array -import java.util.* - +@Suppress("unused") object FileUtils { - fun getFileByPath(filePath: String): File? { - return if (filePath.isBlank()) null else File(filePath) + fun exists(root: File, fileName: String, vararg subDirs: String): Boolean { + return getFile(root, fileName, subDirs = *subDirs).exists() } - fun getSdCardPath(): String { - var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath - - try { - sdCardDirectory = File(sdCardDirectory).canonicalPath - } catch (ioe: IOException) { - ioe.printStackTrace() - } - - return sdCardDirectory + fun createFileIfNotExist(root: File, fileName: String, vararg subDirs: String): File { + val filePath = getPath(root, fileName, *subDirs) + return createFileIfNotExist(filePath) } - fun getStorageData(pContext: Context): ArrayList? { - - val storageManager = pContext.getSystemService(Context.STORAGE_SERVICE) as StorageManager - - try { - val getVolumeList = storageManager.javaClass.getMethod("getVolumeList") - - val storageVolumeClazz = Class.forName("android.os.storage.StorageVolume") - val getPath = storageVolumeClazz.getMethod("getPath") - - val invokeVolumeList = getVolumeList.invoke(storageManager) - val length = Array.getLength(invokeVolumeList) - - val list = ArrayList() - for (i in 0 until length) { - val storageVolume = Array.get(invokeVolumeList, i)//得到StorageVolume对象 - val path = getPath.invoke(storageVolume) as String - - list.add(path) - } - return list - } catch (e: Exception) { - e.printStackTrace() - } - - return null + fun createFolderIfNotExist(root: File, vararg subDirs: String): File { + val filePath = root.absolutePath + File.separator + subDirs.joinToString(File.separator) + return createFolderIfNotExist(filePath) } - - fun getExtSdCardPaths(con: Context): ArrayList { - val paths = ArrayList() - val files = ContextCompat.getExternalFilesDirs(con, "external") - val firstFile = files[0] - for (file in files) { - if (file != null && file != firstFile) { - val index = file.absolutePath.lastIndexOf("/Android/data") - if (index < 0) { - Log.w("", "Unexpected external file dir: " + file.absolutePath) - } else { - var path = file.absolutePath.substring(0, index) - try { - path = File(path).canonicalPath - } catch (e: IOException) { - // Keep non-canonical path. - } - - paths.add(path) - } - } + fun createFolderIfNotExist(filePath: String): File { + val file = File(filePath) + //如果文件夹不存在,就创建它 + if (!file.exists()) { + file.mkdirs() } - return paths - } - - fun getPath(context: Context, uri: Uri): String? { - // DocumentProvider - if (DocumentsContract.isDocumentUri(context, uri)) { - // ExternalStorageProvider - if (isExternalStorageDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":") - val type = split[0] - - if ("primary".equals(type, ignoreCase = true)) { - return Environment.getExternalStorageDirectory().toString() + "/" + split[1] - } - - } else if (isDownloadsDocument(uri)) { - val id = DocumentsContract.getDocumentId(uri) - val split = id.split(":") - val type = split[0] - if ("raw".equals(type, ignoreCase = true)) { - //处理某些机型(比如Google Pixel )ID是raw:/storage/emulated/0/Download/c20f8664da05ab6b4644913048ea8c83.mp4 - return split[1] - } - - val contentUri = ContentUris.withAppendedId( - Uri.parse("content://downloads/public_downloads"), java.lang.Long.valueOf(id) - ) - - return getDataColumn(context, contentUri, null, null) - } else if (isMediaDocument(uri)) { - val docId = DocumentsContract.getDocumentId(uri) - val split = docId.split(":".toRegex()) - - val contentUri: Uri = when (split[0]) { - "image" -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI - "video" -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI - "audio" -> MediaStore.Audio.Media.EXTERNAL_CONTENT_URI - else -> uri - } - - val selection = "_id=?" - val selectionArgs = arrayOf(split[1]) - - return getDataColumn(context, contentUri, selection, selectionArgs) - }// MediaProvider - // DownloadsProvider - } else if ("content".equals(uri.scheme, ignoreCase = true)) { - // Return the remote address - return if (isGooglePhotosUri(uri)) - uri.lastPathSegment - else - getDataColumn(context, uri, null, null) - } else if ("file".equals(uri.scheme, ignoreCase = true)) { - return uri.path - }// File - // MediaStore (and general) - return null + return file } - private fun getDataColumn( - context: Context, uri: Uri, selection: String?, - selectionArgs: kotlin.Array? - ): String? { - - val column = "_data" - val projection = arrayOf(column) - + @Synchronized + fun createFileIfNotExist(filePath: String): File { + val file = File(filePath) try { - context.contentResolver.query( - uri, - projection, - selection, - selectionArgs, - null - )?.use { cursor -> - if (cursor.moveToFirst()) { - val index = cursor.getColumnIndexOrThrow(column) - return cursor.getString(index) + if (!file.exists()) { + //创建父类文件夹 + file.parent?.let { + createFolderIfNotExist(it) } + //创建文件 + file.createNewFile() } - } catch (e: Exception) { + } catch (e: IOException) { e.printStackTrace() } + return file + } - return null + fun getFile(root: File, fileName: String, vararg subDirs: String): File { + val filePath = getPath(root, fileName, *subDirs) + return File(filePath) } - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is ExternalStorageProvider. - */ - private fun isExternalStorageDocument(uri: Uri): Boolean { - return "com.android.externalstorage.documents" == uri.authority + fun getDirFile(root: File, vararg subDirs: String): File { + val filePath = getPath(root, subDirs = *subDirs) + return File(filePath) } - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is DownloadsProvider. - */ - private fun isDownloadsDocument(uri: Uri): Boolean { - return "com.android.providers.downloads.documents" == uri.authority + fun getPath(root: File, fileName: String? = null, vararg subDirs: String): String { + return if (fileName.isNullOrEmpty()) { + root.absolutePath + File.separator + subDirs.joinToString(File.separator) + } else { + root.absolutePath + File.separator + subDirs.joinToString(File.separator) + File.separator + fileName + } } - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is MediaProvider. - */ - private fun isMediaDocument(uri: Uri): Boolean { - return "com.android.providers.media.documents" == uri.authority + //递归删除文件夹下的数据 + @Synchronized + fun deleteFile(filePath: String) { + val file = File(filePath) + if (!file.exists()) return + + if (file.isDirectory) { + val files = file.listFiles() + files?.forEach { subFile -> + val path = subFile.path + deleteFile(path) + } + } + //删除文件 + file.delete() } - /** - * @param uri The Uri to check. - * @return Whether the Uri authority is Google Photos. - */ - private fun isGooglePhotosUri(uri: Uri): Boolean { - return "com.google.android.apps.photos.content" == uri.authority + fun getCachePath(): String { + return App.INSTANCE.externalCacheDir?.absolutePath + ?: App.INSTANCE.cacheDir.absolutePath + } + + fun getSdCardPath(): String { + @Suppress("DEPRECATION") + var sdCardDirectory = Environment.getExternalStorageDirectory().absolutePath + try { + sdCardDirectory = File(sdCardDirectory).canonicalPath + } catch (ioe: IOException) { + ioe.printStackTrace() + } + return sdCardDirectory } } diff --git a/app/src/main/java/io/legado/app/utils/FloatExtensions.kt b/app/src/main/java/io/legado/app/utils/FloatExtensions.kt index af9576c00..4e3c7f9ed 100644 --- a/app/src/main/java/io/legado/app/utils/FloatExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/FloatExtensions.kt @@ -2,14 +2,13 @@ package io.legado.app.utils import android.content.res.Resources - -val Float.dp: Float // [xxhdpi](360 -> 1080) +val Float.dp: Float get() = android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_DIP, this, Resources.getSystem().displayMetrics ) -val Float.sp: Float // [xxhdpi](360 -> 1080) +val Float.sp: Float get() = android.util.TypedValue.applyDimension( android.util.TypedValue.COMPLEX_UNIT_SP, this, Resources.getSystem().displayMetrics ) diff --git a/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt index f233929fb..de7f10b96 100644 --- a/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/FragmentExtensions.kt @@ -13,6 +13,7 @@ import org.jetbrains.anko.connectivityManager import org.jetbrains.anko.defaultSharedPreferences import org.jetbrains.anko.internals.AnkoInternals +@Suppress("DEPRECATION") fun Fragment.isOnline() = requireContext().connectivityManager.activeNetworkInfo?.isConnected == true fun Fragment.getPrefBoolean(key: String, defValue: Boolean = false) = @@ -54,17 +55,9 @@ fun Fragment.getCompatDrawable(@DrawableRes id: Int): Drawable? = requireContext fun Fragment.getCompatColorStateList(@ColorRes id: Int): ColorStateList? = requireContext().getCompatColorStateList(id) -val Fragment.isNightTheme: Boolean - get() = getPrefBoolean("isNightTheme") - -val Fragment.isTransparentStatusBar: Boolean - get() = getPrefBoolean("transparentStatusBar") - - inline fun Fragment.startActivity(vararg params: Pair) = AnkoInternals.internalStartActivity(requireActivity(), T::class.java, params) - inline fun Fragment.startActivityForResult(requestCode: Int, vararg params: Pair) = startActivityForResult(AnkoInternals.createIntent(requireActivity(), T::class.java, params), requestCode) diff --git a/app/src/main/java/io/legado/app/utils/LogUtils.kt b/app/src/main/java/io/legado/app/utils/LogUtils.kt index 1662974a7..e70813dee 100644 --- a/app/src/main/java/io/legado/app/utils/LogUtils.kt +++ b/app/src/main/java/io/legado/app/utils/LogUtils.kt @@ -2,14 +2,12 @@ package io.legado.app.utils import android.annotation.SuppressLint import io.legado.app.App -import io.legado.app.help.FileHelp -import java.io.File import java.text.SimpleDateFormat import java.util.* import java.util.logging.* import java.util.logging.Formatter - +@Suppress("unused") object LogUtils { const val TIME_PATTERN = "yyyy-MM-dd HH:mm:ss" @@ -25,18 +23,17 @@ object LogUtils { private val logger: Logger by lazy { Logger.getGlobal().apply { - addHandler(fileHandler) + fileHandler?.let { + addHandler(it) + } } } private val fileHandler by lazy { - val logFolder = FileHelp.getCachePath() + File.separator + "logs" - FileHelp.getFolder(logFolder) - FileHandler( - logFolder + File.separator + "app.log", - 10240, - 10 - ).apply { + val root = App.INSTANCE.externalCacheDir ?: return@lazy null + val logFolder = FileUtils.createFolderIfNotExist(root, "logs") + val logPath = FileUtils.getPath(logFolder, "appLog") + FileHandler(logPath, 10240, 10).apply { formatter = object : Formatter() { override fun format(record: LogRecord): String { // 设置文件输出格式 @@ -52,7 +49,7 @@ object LogUtils { } fun upLevel() { - fileHandler.level = if (App.INSTANCE.getPrefBoolean("recordLog")) { + fileHandler?.level = if (App.INSTANCE.getPrefBoolean("recordLog")) { Level.INFO } else { Level.OFF diff --git a/app/src/main/java/io/legado/app/utils/MD5Utils.kt b/app/src/main/java/io/legado/app/utils/MD5Utils.kt index a83345bc6..f0044a999 100644 --- a/app/src/main/java/io/legado/app/utils/MD5Utils.kt +++ b/app/src/main/java/io/legado/app/utils/MD5Utils.kt @@ -6,7 +6,7 @@ import java.security.NoSuchAlgorithmException /** * 将字符串转化为MD5 */ - +@Suppress("unused") object MD5Utils { fun md5Encode(str: String?): String? { 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 616aa0006..0c59f05a3 100644 --- a/app/src/main/java/io/legado/app/utils/NetworkUtils.kt +++ b/app/src/main/java/io/legado/app/utils/NetworkUtils.kt @@ -8,6 +8,7 @@ import java.net.URL import java.util.* import java.util.regex.Pattern +@Suppress("unused") object NetworkUtils { fun getUrl(response: Response<*>): String { val networkResponse = response.raw().networkResponse diff --git a/app/src/main/java/io/legado/app/utils/RealPathUtil.kt b/app/src/main/java/io/legado/app/utils/RealPathUtil.kt new file mode 100644 index 000000000..5ded2f823 --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RealPathUtil.kt @@ -0,0 +1,173 @@ +package io.legado.app.utils + +import android.annotation.SuppressLint +import android.content.ContentUris +import android.content.Context +import android.database.Cursor +import android.net.Uri +import android.os.Build +import android.os.Environment +import android.provider.DocumentsContract +import android.provider.MediaStore +import java.io.File +import java.io.FileInputStream +import java.io.FileOutputStream +import java.io.IOException + +@Suppress("unused") +object RealPathUtil { + /** + * Method for return file path of Gallery image + * @return path of the selected image file from gallery + */ + private var filePathUri: Uri? = null + + @Suppress("DEPRECATION") + fun getPath( + context: Context, + uri: Uri + ): String? { + //check here to KITKAT or new version + @SuppressLint("ObsoleteSdkInt") + val isKitKat = Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT + filePathUri = uri + // DocumentProvider + if (isKitKat && DocumentsContract.isDocumentUri(context, uri)) { // ExternalStorageProvider + if (isExternalStorageDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + if ("primary".equals(type, ignoreCase = true)) { + return Environment.getExternalStorageDirectory().toString() + "/" + split[1] + } + } else if (isDownloadsDocument(uri)) { + val id = DocumentsContract.getDocumentId(uri) + val contentUri = ContentUris.withAppendedId( + Uri.parse("content://downloads/public_downloads"), + java.lang.Long.valueOf(id) + ) + //return getDataColumn(context, uri, null, null); + return getDataColumn(context, contentUri, null, null) + } else if (isMediaDocument(uri)) { + val docId = DocumentsContract.getDocumentId(uri) + val split = docId.split(":").toTypedArray() + val type = split[0] + var contentUri: Uri? = null + if ("image" == type) { + contentUri = MediaStore.Images.Media.EXTERNAL_CONTENT_URI + } else if ("video" == type) { + contentUri = MediaStore.Video.Media.EXTERNAL_CONTENT_URI + } else if ("audio" == type) { + contentUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI + } + val selection = "_id=?" + val selectionArgs = arrayOf( + split[1] + ) + return getDataColumn(context, contentUri, selection, selectionArgs) + } + } else if ("content".equals( + uri.scheme, + ignoreCase = true + ) + ) { // Return the remote address + return if (isGooglePhotosUri(uri)) uri.lastPathSegment else getDataColumn( + context, + uri, + null, + null + ) + } else if ("file".equals(uri.scheme, ignoreCase = true)) { + return uri.path + } + return null + } + + /** + * Get the value of the data column for this Uri. This is useful for + * MediaStore Uris, and other file-based ContentProviders. + * + * @param context The context. + * @param uri The Uri to query. + * @param selection (Optional) Filter used in the query. + * @param selectionArgs (Optional) Selection arguments used in the query. + * @return The value of the _data column, which is typically a file path. + */ + private fun getDataColumn( + context: Context, uri: Uri?, selection: String?, + selectionArgs: Array? + ): String? { + var cursor: Cursor? = null + val column = "_data" + val projection = arrayOf( + column + ) + var input: FileInputStream? = null + var output: FileOutputStream? = null + try { + cursor = + context.contentResolver.query(uri!!, projection, selection, selectionArgs, null) + if (cursor != null && cursor.moveToFirst()) { + val index = cursor.getColumnIndexOrThrow(column) + return cursor.getString(index) + } + } catch (e: IllegalArgumentException) { + e.printStackTrace() + val file = File(context.cacheDir, "tmp") + val filePath = file.absolutePath + try { + val pfd = + context.contentResolver.openFileDescriptor(filePathUri!!, "r") + ?: return null + val fd = pfd.fileDescriptor + input = FileInputStream(fd) + output = FileOutputStream(filePath) + var read: Int + val bytes = ByteArray(4096) + while (input.read(bytes).also { read = it } != -1) { + output.write(bytes, 0, read) + } + input.close() + output.close() + return File(filePath).absolutePath + } catch (ignored: IOException) { + ignored.printStackTrace() + } + } finally { + cursor?.close() + } + return null + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is ExternalStorageProvider. + */ + private fun isExternalStorageDocument(uri: Uri): Boolean { + return "com.android.externalstorage.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is DownloadsProvider. + */ + private fun isDownloadsDocument(uri: Uri): Boolean { + return "com.android.providers.downloads.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is MediaProvider. + */ + private fun isMediaDocument(uri: Uri): Boolean { + return "com.android.providers.media.documents" == uri.authority + } + + /** + * @param uri The Uri to check. + * @return Whether the Uri authority is Google Photos. + */ + private fun isGooglePhotosUri(uri: Uri): Boolean { + return "com.google.android.apps.photos.content" == uri.authority + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt b/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt new file mode 100644 index 000000000..7dbed87ce --- /dev/null +++ b/app/src/main/java/io/legado/app/utils/RecyclerViewExtensions.kt @@ -0,0 +1,15 @@ +package io.legado.app.utils + +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.DividerItemDecoration +import androidx.recyclerview.widget.RecyclerView +import io.legado.app.R + + +fun RecyclerView.getVerticalDivider(): DividerItemDecoration { + return DividerItemDecoration(context, DividerItemDecoration.VERTICAL).apply { + ContextCompat.getDrawable(context, R.drawable.ic_divider)?.let { + this.setDrawable(it) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/Snackbars.kt b/app/src/main/java/io/legado/app/utils/Snackbars.kt index f60def01f..2706bd7ca 100644 --- a/app/src/main/java/io/legado/app/utils/Snackbars.kt +++ b/app/src/main/java/io/legado/app/utils/Snackbars.kt @@ -4,139 +4,13 @@ import android.view.View import androidx.annotation.StringRes import com.google.android.material.snackbar.Snackbar -/** - * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.snackbar(Int)' instead.", ReplaceWith("view.snackbar(message)")) -inline fun snackbar(view: View, message: Int) = Snackbar - .make(view, message, Snackbar.LENGTH_SHORT) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.longSnackbar(Int)' instead.", ReplaceWith("view.longSnackbar(message)")) -inline fun longSnackbar(view: View, message: Int) = Snackbar - .make(view, message, Snackbar.LENGTH_LONG) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.indefiniteSnackbar(Int)' instead.", ReplaceWith("view.indefiniteSnackbar(message)")) -inline fun indefiniteSnackbar(view: View, message: Int) = Snackbar - .make(view, message, Snackbar.LENGTH_INDEFINITE) - .apply { show() } - -/** - * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.snackbar(CharSequence)' instead.", ReplaceWith("view.snackbar(message)")) -inline fun snackbar(view: View, message: CharSequence) = Snackbar - .make(view, message, Snackbar.LENGTH_SHORT) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.longSnackbar(CharSequence)' instead.", ReplaceWith("view.longSnackbar(message)")) -inline fun longSnackbar(view: View, message: CharSequence) = Snackbar - .make(view, message, Snackbar.LENGTH_LONG) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.indefiniteSnackbar(CharSequence)' instead.", ReplaceWith("view.indefiniteSnackbar(message)")) -inline fun indefiniteSnackbar(view: View, message: CharSequence) = Snackbar - .make(view, message, Snackbar.LENGTH_INDEFINITE) - .apply { show() } - -/** - * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.snackbar(Int, Int, (View) -> Unit)' instead.", ReplaceWith("view.snackbar(message, actionText, action)")) -inline fun snackbar(view: View, message: Int, actionText: Int, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_SHORT) - .setAction(actionText, action) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.longSnackbar(Int, Int, (View) -> Unit)' instead.", ReplaceWith("view.longSnackbar(message, actionText, action)")) -inline fun longSnackbar(view: View, message: Int, actionText: Int, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_LONG) - .setAction(actionText, action) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. - * - * @param message the message text resource. - */ -@Deprecated("Use 'View.indefiniteSnackbar(Int, Int, (View) -> Unit)' instead.", ReplaceWith("view.indefiniteSnackbar(message, actionText, action)")) -inline fun indefiniteSnackbar(view: View, message: Int, actionText: Int, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_INDEFINITE) - .setAction(actionText, action) - .apply { show() } - -/** - * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.snackbar(CharSequence, CharSequence, (View) -> Unit)' instead.", ReplaceWith("view.snackbar(message, actionText, action)")) -inline fun snackbar(view: View, message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_SHORT) - .setAction(actionText, action) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_LONG] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.longSnackbar(CharSequence, CharSequence, (View) -> Unit)' instead.", ReplaceWith("view.longSnackbar(message, actionText, action)")) -inline fun longSnackbar(view: View, message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_LONG) - .setAction(actionText, action) - .apply { show() } - -/** - * Display Snackbar with the [Snackbar.LENGTH_INDEFINITE] duration. - * - * @param message the message text. - */ -@Deprecated("Use 'View.indefiniteSnackbar(CharSequence, CharSequence, (View) -> Unit)' instead.", ReplaceWith("view.indefiniteSnackbar(message, actionText, action)")) -inline fun indefiniteSnackbar(view: View, message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar - .make(view, message, Snackbar.LENGTH_INDEFINITE) - .setAction(actionText, action) - .apply { show() } - /** * Display the Snackbar with the [Snackbar.LENGTH_SHORT] duration. * * @param message the message text resource. */ @JvmName("snackbar2") -inline fun View.snackbar(@StringRes message: Int) = Snackbar +fun View.snackbar(@StringRes message: Int) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .apply { show() } @@ -146,7 +20,7 @@ inline fun View.snackbar(@StringRes message: Int) = Snackbar * @param message the message text resource. */ @JvmName("longSnackbar2") -inline fun View.longSnackbar(@StringRes message: Int) = Snackbar +fun View.longSnackbar(@StringRes message: Int) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .apply { show() } @@ -156,7 +30,7 @@ inline fun View.longSnackbar(@StringRes message: Int) = Snackbar * @param message the message text resource. */ @JvmName("indefiniteSnackbar2") -inline fun View.indefiniteSnackbar(@StringRes message: Int) = Snackbar +fun View.indefiniteSnackbar(@StringRes message: Int) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .apply { show() } @@ -166,7 +40,7 @@ inline fun View.indefiniteSnackbar(@StringRes message: Int) = Snackbar * @param message the message text. */ @JvmName("snackbar2") -inline fun View.snackbar(message: CharSequence) = Snackbar +fun View.snackbar(message: CharSequence) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .apply { show() } @@ -176,7 +50,7 @@ inline fun View.snackbar(message: CharSequence) = Snackbar * @param message the message text. */ @JvmName("longSnackbar2") -inline fun View.longSnackbar(message: CharSequence) = Snackbar +fun View.longSnackbar(message: CharSequence) = Snackbar .make(this, message, Snackbar.LENGTH_LONG) .apply { show() } @@ -186,7 +60,7 @@ inline fun View.longSnackbar(message: CharSequence) = Snackbar * @param message the message text. */ @JvmName("indefiniteSnackbar2") -inline fun View.indefiniteSnackbar(message: CharSequence) = Snackbar +fun View.indefiniteSnackbar(message: CharSequence) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .apply { show() } @@ -196,7 +70,7 @@ inline fun View.indefiniteSnackbar(message: CharSequence) = Snackbar * @param message the message text resource. */ @JvmName("snackbar2") -inline fun View.snackbar(message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) = Snackbar +fun View.snackbar(message: Int, @StringRes actionText: Int, action: (View) -> Unit) = Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .setAction(actionText, action) .apply { show() } @@ -207,7 +81,8 @@ inline fun View.snackbar(message: Int, @StringRes actionText: Int, noinline acti * @param message the message text resource. */ @JvmName("longSnackbar2") -inline fun View.longSnackbar(@StringRes message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) = Snackbar +fun View.longSnackbar(@StringRes message: Int, @StringRes actionText: Int, action: (View) -> Unit) = + Snackbar .make(this, message, Snackbar.LENGTH_LONG) .setAction(actionText, action) .apply { show() } @@ -218,7 +93,8 @@ inline fun View.longSnackbar(@StringRes message: Int, @StringRes actionText: Int * @param message the message text resource. */ @JvmName("indefiniteSnackbar2") -inline fun View.indefiniteSnackbar(@StringRes message: Int, @StringRes actionText: Int, noinline action: (View) -> Unit) = Snackbar +fun View.indefiniteSnackbar(@StringRes message: Int, @StringRes actionText: Int, action: (View) -> Unit) = + Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .setAction(actionText, action) .apply { show() } @@ -229,7 +105,8 @@ inline fun View.indefiniteSnackbar(@StringRes message: Int, @StringRes actionTex * @param message the message text. */ @JvmName("snackbar2") -inline fun View.snackbar(message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar +fun View.snackbar(message: CharSequence, actionText: CharSequence, action: (View) -> Unit) = + Snackbar .make(this, message, Snackbar.LENGTH_SHORT) .setAction(actionText, action) .apply { show() } @@ -240,7 +117,8 @@ inline fun View.snackbar(message: CharSequence, actionText: CharSequence, noinli * @param message the message text. */ @JvmName("longSnackbar2") -inline fun View.longSnackbar(message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar +fun View.longSnackbar(message: CharSequence, actionText: CharSequence, action: (View) -> Unit) = + Snackbar .make(this, message, Snackbar.LENGTH_LONG) .setAction(actionText, action) .apply { show() } @@ -251,7 +129,11 @@ inline fun View.longSnackbar(message: CharSequence, actionText: CharSequence, no * @param message the message text. */ @JvmName("indefiniteSnackbar2") -inline fun View.indefiniteSnackbar(message: CharSequence, actionText: CharSequence, noinline action: (View) -> Unit) = Snackbar +fun View.indefiniteSnackbar( + message: CharSequence, + actionText: CharSequence, + action: (View) -> Unit +) = Snackbar .make(this, message, Snackbar.LENGTH_INDEFINITE) .setAction(actionText, action) .apply { show() } \ 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 987a0fce8..63954162e 100644 --- a/app/src/main/java/io/legado/app/utils/StringExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/StringExtensions.kt @@ -1,37 +1,44 @@ package io.legado.app.utils -// import org.apache.commons.text.StringEscapeUtils - fun String?.safeTrim() = if (this.isNullOrBlank()) null else this.trim() -fun String?.isAbsUrl() = if (this.isNullOrBlank()) false else this.startsWith("http://", true) - || this.startsWith("https://", true) - -fun String?.isJson(): Boolean = this?.run { - val str = this.trim() - when { - str.startsWith("{") && str.endsWith("}") -> true - str.startsWith("[") && str.endsWith("]") -> true - else -> false - } -} ?: false - -fun String?.isJsonObject(): Boolean = this?.run { - val str = this.trim() - str.startsWith("{") && str.endsWith("}") -} ?: false - -fun String?.isJsonArray(): Boolean = this?.run { - val str = this.trim() - str.startsWith("[") && str.endsWith("]") -} ?: false - -fun String?.htmlFormat(): String = if (this.isNullOrBlank()) "" else - this.replace("(?i)<(br[\\s/]*|/*p\\b.*?|/*div\\b.*?)>".toRegex(), "\n")// 替换特定标签为换行符 - .replace("<[script>]*.*?>| ".toRegex(), "")// 删除script标签对和空格转义符 - .replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 - .replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 - .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 +fun String?.isContentPath(): Boolean = this?.startsWith("content://") == true + +fun String?.isAbsUrl() = + this?.let { + it.startsWith("http://", true) + || it.startsWith("https://", true) + } ?: false + +fun String?.isJson(): Boolean = + this?.run { + val str = this.trim() + when { + str.startsWith("{") && str.endsWith("}") -> true + str.startsWith("[") && str.endsWith("]") -> true + else -> false + } + } ?: false + +fun String?.isJsonObject(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("{") && str.endsWith("}") + } ?: false + +fun String?.isJsonArray(): Boolean = + this?.run { + val str = this.trim() + str.startsWith("[") && str.endsWith("]") + } ?: false + +fun String?.htmlFormat(): String = + this?.replace("(?i)<(br[\\s/]*|/*p\\b.*?|/*div\\b.*?)>".toRegex(), "\n") + ?.replace("<[script>]*.*?>| ".toRegex(), "") + ?.replace("\\s*\\n+\\s*".toRegex(), "\n  ") + ?.replace("^[\\n\\s]+".toRegex(), "  ") + ?.replace("[\\n\\s]+$".toRegex(), "") + ?: "" fun String.splitNotBlank(vararg delimiter: String): Array = run { this.split(*delimiter).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() @@ -41,7 +48,4 @@ fun String.splitNotBlank(regex: Regex, limit: Int = 0): Array = run { this.split(regex, limit).map { it.trim() }.filterNot { it.isBlank() }.toTypedArray() } -fun String.startWithIgnoreCase(start: String): Boolean { - return if (this.isBlank()) false else startsWith(start, true) -} diff --git a/app/src/main/java/io/legado/app/utils/StringUtils.kt b/app/src/main/java/io/legado/app/utils/StringUtils.kt index c9f80866f..dda534eed 100644 --- a/app/src/main/java/io/legado/app/utils/StringUtils.kt +++ b/app/src/main/java/io/legado/app/utils/StringUtils.kt @@ -2,13 +2,17 @@ package io.legado.app.utils import android.annotation.SuppressLint import android.text.TextUtils.isEmpty +import java.text.DecimalFormat import java.text.ParseException import java.text.SimpleDateFormat import java.util.* import java.util.regex.Matcher import java.util.regex.Pattern import kotlin.math.abs +import kotlin.math.log10 +import kotlin.math.pow +@Suppress("unused") object StringUtils { private const val HOUR_OF_DAY = 24 private const val DAY_OF_YESTERDAY = 2 @@ -41,16 +45,18 @@ object StringUtils { //将时间转换成日期 fun dateConvert(time: Long, pattern: String): String { val date = Date(time) - @SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) + @SuppressLint("SimpleDateFormat") + val format = SimpleDateFormat(pattern) return format.format(date) } //将日期转换成昨天、今天、明天 fun dateConvert(source: String, pattern: String): String { - @SuppressLint("SimpleDateFormat") val format = SimpleDateFormat(pattern) + @SuppressLint("SimpleDateFormat") + val format = SimpleDateFormat(pattern) val calendar = Calendar.getInstance() try { - val date = format.parse(source) + val date = format.parse(source) ?: return "" val curTime = calendar.timeInMillis calendar.time = date //将MISC 转换成 sec @@ -62,13 +68,18 @@ object StringUtils { //如果没有时间 if (oldHour == 0) { //比日期:昨天今天和明天 - if (difDate == 0L) { - return "今天" - } else if (difDate < DAY_OF_YESTERDAY) { - return "昨天" - } else { - @SuppressLint("SimpleDateFormat") val convertFormat = SimpleDateFormat("yyyy-MM-dd") - return convertFormat.format(date) + return when { + difDate == 0L -> { + "今天" + } + difDate < DAY_OF_YESTERDAY -> { + "昨天" + } + else -> { + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") + convertFormat.format(date) + } } } @@ -78,7 +89,8 @@ object StringUtils { difHour < HOUR_OF_DAY -> difHour.toString() + "小时前" difDate < DAY_OF_YESTERDAY -> "昨天" else -> { - @SuppressLint("SimpleDateFormat") val convertFormat = SimpleDateFormat("yyyy-MM-dd") + @SuppressLint("SimpleDateFormat") + val convertFormat = SimpleDateFormat("yyyy-MM-dd") convertFormat.format(date) } } @@ -89,6 +101,19 @@ object StringUtils { return "" } + fun toSize(length: Long): String { + if (length <= 0) return "0" + val units = arrayOf("b", "kb", "M", "G", "T") + //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 + //计算单位的,原理是利用lg,公式是 lg(1024^n) = nlg(1024),最后 nlg(1024)/lg(1024) = n。 + val digitGroups = + (log10(length.toDouble()) / log10(1024.0)).toInt() + //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 + //计算原理是,size/单位值。单位值指的是:比如说b = 1024,KB = 1024^2 + return DecimalFormat("#,##0.##") + .format(length / 1024.0.pow(digitGroups.toDouble())) + " " + units[digitGroups] + } + @SuppressLint("DefaultLocale") fun toFirstCapital(str: String): String { return str.substring(0, 1).toUpperCase() + str.substring(1) @@ -242,7 +267,7 @@ object StringUtils { val m = p.matcher(data) val buf = StringBuffer(data.length) while (m.find()) { - val ch = Integer.parseInt(m.group(1), 16).toChar().toString() + val ch = Integer.parseInt(m.group(1)!!, 16).toChar().toString() m.appendReplacement(buf, Matcher.quoteReplacement(ch)) } m.appendTail(buf) @@ -254,9 +279,9 @@ object StringUtils { "(?i)<(br[\\s/]*|/*p.*?|/*div.*?)>".toRegex(), "\n" )// 替换特定标签为换行符 - .replace("<[script>]*.*?>| ".toRegex(), "")// 删除script标签对和空格转义符 - .replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 - .replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 - .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 + .replace("<[script>]*.*?>| ".toRegex(), "")// 删除script标签对和空格转义符 + .replace("\\s*\\n+\\s*".toRegex(), "\n  ")// 移除空行,并增加段前缩进2个汉字 + .replace("^[\\n\\s]+".toRegex(), "  ")//移除开头空行,并增加段前缩进2个汉字 + .replace("[\\n\\s]+$".toRegex(), "") //移除尾部空行 } } diff --git a/app/src/main/java/io/legado/app/utils/SystemUtils.kt b/app/src/main/java/io/legado/app/utils/SystemUtils.kt index 91121c000..c29381bbf 100644 --- a/app/src/main/java/io/legado/app/utils/SystemUtils.kt +++ b/app/src/main/java/io/legado/app/utils/SystemUtils.kt @@ -9,6 +9,8 @@ import android.net.Uri import android.os.PowerManager import android.provider.Settings + +@Suppress("unused") object SystemUtils { fun getScreenOffTime(context: Context): Int { diff --git a/app/src/main/java/io/legado/app/utils/Toasts.kt b/app/src/main/java/io/legado/app/utils/Toasts.kt index 16c032fa0..4348b4daf 100644 --- a/app/src/main/java/io/legado/app/utils/Toasts.kt +++ b/app/src/main/java/io/legado/app/utils/Toasts.kt @@ -11,25 +11,25 @@ import org.jetbrains.anko.toast * * @param message the message text resource. */ -inline fun Fragment.toast(message: Int) = requireActivity().toast(message) +fun Fragment.toast(message: Int) = requireActivity().toast(message) /** * Display the simple Toast message with the [Toast.LENGTH_SHORT] duration. * * @param message the message text. */ -inline fun Fragment.toast(message: CharSequence) = requireActivity().toast(message) +fun Fragment.toast(message: CharSequence) = requireActivity().toast(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text resource. */ -inline fun Fragment.longToast(message: Int) = requireActivity().longToast(message) +fun Fragment.longToast(message: Int) = requireActivity().longToast(message) /** * Display the simple Toast message with the [Toast.LENGTH_LONG] duration. * * @param message the message text. */ -inline fun Fragment.longToast(message: CharSequence) = requireActivity().longToast(message) +fun Fragment.longToast(message: CharSequence) = requireActivity().longToast(message) diff --git a/app/src/main/java/io/legado/app/utils/UTF8BOMFighter.kt b/app/src/main/java/io/legado/app/utils/UTF8BOMFighter.kt index 5ced93613..3f502f8b6 100644 --- a/app/src/main/java/io/legado/app/utils/UTF8BOMFighter.kt +++ b/app/src/main/java/io/legado/app/utils/UTF8BOMFighter.kt @@ -1,5 +1,6 @@ package io.legado.app.utils +@Suppress("unused") object UTF8BOMFighter { private val UTF8_BOM_BYTES = byteArrayOf(0xEF.toByte(), 0xBB.toByte(), 0xBF.toByte()) diff --git a/app/src/main/java/io/legado/app/utils/UriExtensions.kt b/app/src/main/java/io/legado/app/utils/UriExtensions.kt index 297a5a612..672259dc8 100644 --- a/app/src/main/java/io/legado/app/utils/UriExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/UriExtensions.kt @@ -2,16 +2,14 @@ package io.legado.app.utils import android.content.Context import android.net.Uri -import androidx.documentfile.provider.DocumentFile import java.io.File - @Throws(Exception::class) fun Uri.readBytes(context: Context): ByteArray? { - if (DocumentFile.isDocumentUri(context, this)) { + if (this.toString().isContentPath()) { return DocumentUtils.readBytes(context, this) } else { - val path = FileUtils.getPath(context, this) + val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { return File(path).readBytes() } @@ -21,10 +19,10 @@ fun Uri.readBytes(context: Context): ByteArray? { @Throws(Exception::class) fun Uri.readText(context: Context): String? { - if (DocumentFile.isDocumentUri(context, this)) { + if (this.toString().isContentPath()) { return DocumentUtils.readText(context, this) } else { - val path = FileUtils.getPath(context, this) + val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { return File(path).readText() } @@ -34,10 +32,10 @@ fun Uri.readText(context: Context): String? { @Throws(Exception::class) fun Uri.writeBytes(context: Context, byteArray: ByteArray): Boolean { - if (DocumentFile.isDocumentUri(context, this)) { + if (this.toString().isContentPath()) { return DocumentUtils.writeBytes(context, byteArray, this) } else { - val path = FileUtils.getPath(context, this) + val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { File(path).writeBytes(byteArray) return true @@ -48,10 +46,10 @@ fun Uri.writeBytes(context: Context, byteArray: ByteArray): Boolean { @Throws(Exception::class) fun Uri.writeText(context: Context, text: String): Boolean { - if (DocumentFile.isDocumentUri(context, this)) { + if (this.toString().isContentPath()) { return DocumentUtils.writeText(context, text, this) } else { - val path = FileUtils.getPath(context, this) + val path = RealPathUtil.getPath(context, this) if (path?.isNotEmpty() == true) { File(path).writeText(text) return true 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 81cdafe81..8eb3cb1f6 100644 --- a/app/src/main/java/io/legado/app/utils/ViewExtensions.kt +++ b/app/src/main/java/io/legado/app/utils/ViewExtensions.kt @@ -24,7 +24,7 @@ private tailrec fun getCompatActivity(context: Context?): AppCompatActivity? { val View.activity: AppCompatActivity? get() = getCompatActivity(context) -inline fun View.hideSoftInput() = run { +fun View.hideSoftInput() = run { val imm = App.INSTANCE.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager imm?.let { imm.hideSoftInputFromWindow(this.windowToken, 0) @@ -49,6 +49,14 @@ fun View.visible() { visibility = VISIBLE } +fun View.visible(visible: Boolean) { + if (visible && visibility != VISIBLE) { + visibility = VISIBLE + } else if (!visible && visibility == VISIBLE) { + visibility = INVISIBLE + } +} + fun View.screenshot(): Bitmap? { return runCatching { val screenshot = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888) diff --git a/app/src/main/java/io/legado/app/utils/ViewModelKt.kt b/app/src/main/java/io/legado/app/utils/ViewModelKt.kt index b777c8bbf..9e1f74f24 100644 --- a/app/src/main/java/io/legado/app/utils/ViewModelKt.kt +++ b/app/src/main/java/io/legado/app/utils/ViewModelKt.kt @@ -3,14 +3,16 @@ package io.legado.app.utils import androidx.appcompat.app.AppCompatActivity import androidx.fragment.app.Fragment import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProviders +import androidx.lifecycle.ViewModelProvider -fun AppCompatActivity.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz) +fun AppCompatActivity.getViewModel(clazz: Class) = + ViewModelProvider(this).get(clazz) -fun Fragment.getViewModel(clazz: Class) = ViewModelProviders.of(this).get(clazz) +fun Fragment.getViewModel(clazz: Class) = + ViewModelProvider(this).get(clazz) /** * 与activity数据同步 */ fun Fragment.getViewModelOfActivity(clazz: Class) = - ViewModelProviders.of(requireActivity()).get(clazz) \ No newline at end of file + ViewModelProvider(requireActivity()).get(clazz) \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/utils/ZipUtils.kt b/app/src/main/java/io/legado/app/utils/ZipUtils.kt index eab9951c7..a8ec51d45 100644 --- a/app/src/main/java/io/legado/app/utils/ZipUtils.kt +++ b/app/src/main/java/io/legado/app/utils/ZipUtils.kt @@ -7,14 +7,7 @@ import java.util.zip.ZipEntry import java.util.zip.ZipFile import java.util.zip.ZipOutputStream -/** - *
- * author: Blankj
- * blog  : http://blankj.com
- * time  : 2016/08/27
- * desc  : utils about zip
-
* - */ +@Suppress("unused") object ZipUtils { /** @@ -57,7 +50,7 @@ object ZipUtils { } return true } finally { - if (zos != null) { + zos?.let { zos.finish() zos.close() } @@ -89,7 +82,7 @@ object ZipUtils { } return true } finally { - if (zos != null) { + zos?.let { zos.finish() zos.close() } @@ -261,7 +254,7 @@ object ZipUtils { val files = ArrayList() val zip = ZipFile(zipFile) val entries = zip.entries() - try { + zip.use { if (isSpace(keyword)) { while (entries.hasMoreElements()) { val entry = entries.nextElement() as ZipEntry @@ -285,8 +278,6 @@ object ZipUtils { } } } - } finally { - zip.close() } return files } diff --git a/app/src/main/java/io/legado/app/web/ReadMe.md b/app/src/main/java/io/legado/app/web/ReadMe.md index cd5544c3e..aee2e2dc6 100644 --- a/app/src/main/java/io/legado/app/web/ReadMe.md +++ b/app/src/main/java/io/legado/app/web/ReadMe.md @@ -1 +1,5 @@ -# web服务 \ No newline at end of file +# web服务 + +* controller 数据操作 +* HttpServer http服务 +* WebSocketServer 持续通讯服务 \ No newline at end of file diff --git a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt b/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt index 87bcb3e1c..961407998 100644 --- a/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt +++ b/app/src/main/java/io/legado/app/web/controller/BookshelfController.kt @@ -13,7 +13,7 @@ class BookshelfController { val bookshelf: ReturnData get() { - val books = App.db.bookDao().allBooks + val books = App.db.bookDao().all val returnData = ReturnData() return if (books.isEmpty()) { returnData.setErrorMsg("还没有添加小说") diff --git a/app/src/main/res/anim/moprogress_bottom_in.xml b/app/src/main/res/anim/moprogress_bottom_in.xml deleted file mode 100644 index 59904f594..000000000 --- a/app/src/main/res/anim/moprogress_bottom_in.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_bottom_out.xml b/app/src/main/res/anim/moprogress_bottom_out.xml deleted file mode 100644 index 2348594fc..000000000 --- a/app/src/main/res/anim/moprogress_bottom_out.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_in.xml b/app/src/main/res/anim/moprogress_in.xml deleted file mode 100644 index 8e922153b..000000000 --- a/app/src/main/res/anim/moprogress_in.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_in_bottom_right.xml b/app/src/main/res/anim/moprogress_in_bottom_right.xml deleted file mode 100644 index 93348f5a8..000000000 --- a/app/src/main/res/anim/moprogress_in_bottom_right.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_in_top_right.xml b/app/src/main/res/anim/moprogress_in_top_right.xml deleted file mode 100644 index 81bb2e919..000000000 --- a/app/src/main/res/anim/moprogress_in_top_right.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_out.xml b/app/src/main/res/anim/moprogress_out.xml deleted file mode 100644 index 546619cc2..000000000 --- a/app/src/main/res/anim/moprogress_out.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_out_bottom_right.xml b/app/src/main/res/anim/moprogress_out_bottom_right.xml deleted file mode 100644 index d0d1eaffa..000000000 --- a/app/src/main/res/anim/moprogress_out_bottom_right.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/anim/moprogress_out_top_right.xml b/app/src/main/res/anim/moprogress_out_top_right.xml deleted file mode 100644 index 310b29d7e..000000000 --- a/app/src/main/res/anim/moprogress_out_top_right.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/selector_image.xml b/app/src/main/res/color/selector_image.xml new file mode 100644 index 000000000..60fb16ed2 --- /dev/null +++ b/app/src/main/res/color/selector_image.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable-v24/ic_launcher_foreground.xml b/app/src/main/res/drawable-v24/ic_launcher_0.xml similarity index 100% rename from app/src/main/res/drawable-v24/ic_launcher_foreground.xml rename to app/src/main/res/drawable-v24/ic_launcher_0.xml diff --git a/app/src/main/res/drawable-v24/ic_launcher_1.xml b/app/src/main/res/drawable-v24/ic_launcher_1.xml new file mode 100644 index 000000000..bead863ac --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_1.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_2.xml b/app/src/main/res/drawable-v24/ic_launcher_2.xml new file mode 100644 index 000000000..b4343492c --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_2.xml @@ -0,0 +1,24 @@ + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_2b.xml b/app/src/main/res/drawable-v24/ic_launcher_2b.xml new file mode 100644 index 000000000..be66fb2a7 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_2b.xml @@ -0,0 +1,56 @@ + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable-v24/ic_launcher_3.xml b/app/src/main/res/drawable-v24/ic_launcher_3.xml new file mode 100644 index 000000000..a79648990 --- /dev/null +++ b/app/src/main/res/drawable-v24/ic_launcher_3.xml @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/drawable/bg_shadow_bottom_night.xml b/app/src/main/res/drawable/bg_shadow_bottom_night.xml index 85d10b753..039ec1696 100644 --- a/app/src/main/res/drawable/bg_shadow_bottom_night.xml +++ b/app/src/main/res/drawable/bg_shadow_bottom_night.xml @@ -2,7 +2,7 @@ \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cursor_left.xml b/app/src/main/res/drawable/ic_cursor_left.xml new file mode 100644 index 000000000..1656763c3 --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_left.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_cursor_right.xml b/app/src/main/res/drawable/ic_cursor_right.xml new file mode 100644 index 000000000..99734ea19 --- /dev/null +++ b/app/src/main/res/drawable/ic_cursor_right.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_folder_open.xml b/app/src/main/res/drawable/ic_folder_open.xml new file mode 100644 index 000000000..4235565f3 --- /dev/null +++ b/app/src/main/res/drawable/ic_folder_open.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_launcher_background.xml b/app/src/main/res/drawable/ic_launcher_background.xml deleted file mode 100644 index 02c622c4e..000000000 --- a/app/src/main/res/drawable/ic_launcher_background.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_qq_group.xml b/app/src/main/res/drawable/ic_qq_group.xml deleted file mode 100644 index 81d4c8597..000000000 --- a/app/src/main/res/drawable/ic_qq_group.xml +++ /dev/null @@ -1,11 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_select_all.xml b/app/src/main/res/drawable/ic_select_all.xml deleted file mode 100644 index 7211a6157..000000000 --- a/app/src/main/res/drawable/ic_select_all.xml +++ /dev/null @@ -1,23 +0,0 @@ - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable/image_cover_default.jpg b/app/src/main/res/drawable/image_cover_default.jpg index 6504a4e7e..0b502033c 100644 Binary files a/app/src/main/res/drawable/image_cover_default.jpg and b/app/src/main/res/drawable/image_cover_default.jpg differ diff --git a/app/src/main/res/drawable/item_bg_dark.xml b/app/src/main/res/drawable/item_bg_dark.xml deleted file mode 100644 index 054b06f9a..000000000 --- a/app/src/main/res/drawable/item_bg_dark.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/item_bg_light.xml b/app/src/main/res/drawable/item_bg_light.xml deleted file mode 100644 index ef3428fe4..000000000 --- a/app/src/main/res/drawable/item_bg_light.xml +++ /dev/null @@ -1,3 +0,0 @@ - - \ No newline at end of file diff --git a/app/src/main/res/drawable/recyclerview_divider_horizontal.xml b/app/src/main/res/drawable/recyclerview_divider_horizontal.xml new file mode 100644 index 000000000..d6b50f883 --- /dev/null +++ b/app/src/main/res/drawable/recyclerview_divider_horizontal.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/recyclerview_item_divider.xml b/app/src/main/res/drawable/recyclerview_divider_vertical.xml similarity index 100% rename from app/src/main/res/drawable/recyclerview_item_divider.xml rename to app/src/main/res/drawable/recyclerview_divider_vertical.xml diff --git a/app/src/main/res/layout/activity_about.xml b/app/src/main/res/layout/activity_about.xml index 65bacf179..1eaa9471d 100644 --- a/app/src/main/res/layout/activity_about.xml +++ b/app/src/main/res/layout/activity_about.xml @@ -11,18 +11,14 @@ android:id="@+id/title_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - app:title="@string/about" /> - - + app:title="@string/about"> - + + diff --git a/app/src/main/res/layout/activity_arrange_book.xml b/app/src/main/res/layout/activity_arrange_book.xml new file mode 100644 index 000000000..1be7bdb41 --- /dev/null +++ b/app/src/main/res/layout/activity_arrange_book.xml @@ -0,0 +1,25 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_info.xml b/app/src/main/res/layout/activity_book_info.xml index 53f69d1fb..493ce446b 100644 --- a/app/src/main/res/layout/activity_book_info.xml +++ b/app/src/main/res/layout/activity_book_info.xml @@ -87,7 +87,7 @@ android:textColor="@color/md_white_1000" tools:ignore="NestedWeights" /> - + app:radius="2dp" /> - - - - - - - - - + android:visibility="gone" /> @@ -226,69 +186,47 @@ android:layout_width="match_parent" android:layout_height="wrap_content" android:padding="12dp" + android:singleLine="true" android:text="@string/toc_s" android:textColor="@color/tv_text_default" android:textSize="16sp" /> - - - - - - - - - - - + android:includeFontPadding="false" + android:text="@string/remove_from_bookshelf" + android:textColor="@color/tv_text_default" + android:textSize="15sp" /> - + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_info_edit.xml b/app/src/main/res/layout/activity_book_info_edit.xml index ce6b0aff1..0301bcfb6 100644 --- a/app/src/main/res/layout/activity_book_info_edit.xml +++ b/app/src/main/res/layout/activity_book_info_edit.xml @@ -42,47 +42,47 @@ android:orientation="vertical" android:padding="5dp"> - - - + - - - + - - - + - - - - - - + diff --git a/app/src/main/res/layout/activity_book_read.xml b/app/src/main/res/layout/activity_book_read.xml index 245e4ac49..35d8d5f45 100644 --- a/app/src/main/res/layout/activity_book_read.xml +++ b/app/src/main/res/layout/activity_book_read.xml @@ -1,24 +1,34 @@ - - - + + + + + android:visibility="gone" /> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_book_source.xml b/app/src/main/res/layout/activity_book_source.xml index 839d228fb..090be7b00 100644 --- a/app/src/main/res/layout/activity_book_source.xml +++ b/app/src/main/res/layout/activity_book_source.xml @@ -16,7 +16,8 @@ + android:layout_height="0dp" + android:layout_weight="1"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_donate.xml b/app/src/main/res/layout/activity_donate.xml index 0956dd71b..522b6af92 100644 --- a/app/src/main/res/layout/activity_donate.xml +++ b/app/src/main/res/layout/activity_donate.xml @@ -1,238 +1,21 @@ + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/ll_content" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:orientation="vertical"> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + android:id="@+id/title_bar" + android:layout_width="match_parent" + android:layout_height="wrap_content" + app:subtitle="您的支持是我更新的动力" + app:title="@string/donate" /> + + diff --git a/app/src/main/res/layout/activity_import_book.xml b/app/src/main/res/layout/activity_import_book.xml index d829e291c..092290eb2 100644 --- a/app/src/main/res/layout/activity_import_book.xml +++ b/app/src/main/res/layout/activity_import_book.xml @@ -1,7 +1,76 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 6b4eb4487..a21e6de1c 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -21,5 +21,4 @@ app:layout_constraintBottom_toTopOf="@+id/bottom_navigation_view" app:layout_constraintTop_toTopOf="parent" /> - - + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_replace_rule.xml b/app/src/main/res/layout/activity_replace_rule.xml index cefeba9d0..dbcc248a5 100644 --- a/app/src/main/res/layout/activity_replace_rule.xml +++ b/app/src/main/res/layout/activity_replace_rule.xml @@ -1,9 +1,10 @@ - + android:layout_weight="1" /> - + + + diff --git a/app/src/main/res/layout/activity_rss_favorites.xml b/app/src/main/res/layout/activity_rss_favorites.xml new file mode 100644 index 000000000..7740c1981 --- /dev/null +++ b/app/src/main/res/layout/activity_rss_favorites.xml @@ -0,0 +1,25 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_rss_source.xml b/app/src/main/res/layout/activity_rss_source.xml index e3c5bd53e..527a9444f 100644 --- a/app/src/main/res/layout/activity_rss_source.xml +++ b/app/src/main/res/layout/activity_rss_source.xml @@ -16,7 +16,8 @@ + android:layout_height="0dp" + android:layout_weight="1"> + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_source_login.xml b/app/src/main/res/layout/activity_source_login.xml new file mode 100644 index 000000000..e2b27076c --- /dev/null +++ b/app/src/main/res/layout/activity_source_login.xml @@ -0,0 +1,17 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_book_group_picker.xml b/app/src/main/res/layout/dialog_book_group_picker.xml new file mode 100644 index 000000000..43d0c5d3b --- /dev/null +++ b/app/src/main/res/layout/dialog_book_group_picker.xml @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_change_source.xml b/app/src/main/res/layout/dialog_change_source.xml index 21683d69d..1199d4538 100644 --- a/app/src/main/res/layout/dialog_change_source.xml +++ b/app/src/main/res/layout/dialog_change_source.xml @@ -9,7 +9,7 @@ android:id="@+id/tool_bar" android:layout_width="match_parent" android:layout_height="wrap_content" - android:background="@color/background" + android:background="@color/background_menu" android:elevation="5dp" app:displayHomeAsUp="false" app:fitStatusBar="false" diff --git a/app/src/main/res/layout/dialog_edit_text.xml b/app/src/main/res/layout/dialog_edit_text.xml index 2cf3da5dd..2e270b07a 100644 --- a/app/src/main/res/layout/dialog_edit_text.xml +++ b/app/src/main/res/layout/dialog_edit_text.xml @@ -7,7 +7,7 @@ android:layout_marginBottom="48dp" android:overScrollMode="ifContentScrolls"> - @@ -15,12 +16,13 @@ android:layout_height="24dp" android:paddingLeft="10dp" android:paddingRight="10dp" - android:background="@color/background" + android:background="@color/background_card" android:elevation="5dp" /> + android:layout_height="match_parent" + android:background="@color/background_card"> + android:layout_height="wrap_content"> diff --git a/app/src/main/res/layout/dialog_number_picker.xml b/app/src/main/res/layout/dialog_number_picker.xml new file mode 100644 index 000000000..5a471be84 --- /dev/null +++ b/app/src/main/res/layout/dialog_number_picker.xml @@ -0,0 +1,12 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_page_key.xml b/app/src/main/res/layout/dialog_page_key.xml index 9ee5fd289..fce43c2d0 100644 --- a/app/src/main/res/layout/dialog_page_key.xml +++ b/app/src/main/res/layout/dialog_page_key.xml @@ -3,6 +3,7 @@ android:orientation="vertical" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@color/background" android:padding="16dp"> - - - + - - - + + + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> @@ -159,8 +172,11 @@ + android:tint="@color/tv_text_default" + tools:ignore="UnusedAttribute" /> + - - - - + + - - - + + - - - - - - - + app:max="45" + app:title="@string/text_size" + app:layout_constraintTop_toBottomOf="@+id/tv_text_bold" /> - - - - - - + app:max="100" + app:title="@string/text_letter_spacing" + app:layout_constraintTop_toBottomOf="@+id/dsb_text_size" /> - - - - - - - - - - - - - + app:max="50" + app:title="@string/line_size" + app:layout_constraintTop_toBottomOf="@+id/dsb_text_letter_spacing" /> - - - - - - - - - - + + app:layout_constraintTop_toBottomOf="@+id/dsb_paragraph_spacing" /> @@ -231,8 +156,8 @@ android:id="@+id/rg_page_anim" android:layout_width="match_parent" android:layout_height="wrap_content" - android:paddingLeft="16dp" - android:paddingRight="16dp" + android:paddingLeft="10dp" + android:paddingRight="10dp" android:orientation="horizontal" android:gravity="center" app:layout_constraintTop_toBottomOf="@id/tv_page_anim"> @@ -308,12 +233,37 @@ + app:layout_constraintTop_toBottomOf="@+id/vw_bg_fg1" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/tv_share_layout" /> + + + + - - - - - - - - - - \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_read_padding.xml b/app/src/main/res/layout/dialog_read_padding.xml index 22bb10e50..648f062a3 100644 --- a/app/src/main/res/layout/dialog_read_padding.xml +++ b/app/src/main/res/layout/dialog_read_padding.xml @@ -1,206 +1,117 @@ - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - - - + + + + - - - - - - - - - - - - + app:title="@string/padding_right" + app:max="100" /> \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_recycler_view.xml b/app/src/main/res/layout/dialog_recycler_view.xml index 6ac7d8c01..1fce14eef 100644 --- a/app/src/main/res/layout/dialog_recycler_view.xml +++ b/app/src/main/res/layout/dialog_recycler_view.xml @@ -6,11 +6,13 @@ diff --git a/app/src/main/res/layout/dialog_replace_edit.xml b/app/src/main/res/layout/dialog_replace_edit.xml index 4f6a23c6e..dddd8a3e2 100644 --- a/app/src/main/res/layout/dialog_replace_edit.xml +++ b/app/src/main/res/layout/dialog_replace_edit.xml @@ -7,6 +7,7 @@ @@ -23,41 +24,41 @@ android:orientation="vertical" android:padding="10dp"> - - - + - - - + - - - + - - - + - - - + diff --git a/app/src/main/res/layout/dialog_text_view.xml b/app/src/main/res/layout/dialog_text_view.xml index 8338a642a..3a2b347c3 100644 --- a/app/src/main/res/layout/dialog_text_view.xml +++ b/app/src/main/res/layout/dialog_text_view.xml @@ -1,5 +1,5 @@ - + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_toc_regex_edit.xml b/app/src/main/res/layout/dialog_toc_regex_edit.xml new file mode 100644 index 000000000..07c0816a4 --- /dev/null +++ b/app/src/main/res/layout/dialog_toc_regex_edit.xml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_arrange_book.xml b/app/src/main/res/layout/item_arrange_book.xml new file mode 100644 index 000000000..0ce5072d6 --- /dev/null +++ b/app/src/main/res/layout/item_arrange_book.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_book_source.xml b/app/src/main/res/layout/item_book_source.xml index 7d9513d98..3ae65e3b9 100644 --- a/app/src/main/res/layout/item_book_source.xml +++ b/app/src/main/res/layout/item_book_source.xml @@ -1,11 +1,10 @@ - + android:textColor="@color/tv_text_default" + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@id/swt_enabled" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + tools:ignore="RtlSymmetry" + app:layout_constraintRight_toLeftOf="@id/iv_edit" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> + android:tint="@color/tv_text_default" + app:layout_constraintRight_toLeftOf="@id/iv_menu_more" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintBottom_toBottomOf="parent" /> - \ No newline at end of file + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_bookshelf_grid.xml b/app/src/main/res/layout/item_bookshelf_grid.xml index 20d5ab3e2..666d67098 100644 --- a/app/src/main/res/layout/item_bookshelf_grid.xml +++ b/app/src/main/res/layout/item_bookshelf_grid.xml @@ -29,7 +29,7 @@ app:layout_constraintRight_toRightOf="@id/iv_cover" app:layout_constraintTop_toTopOf="@id/iv_cover"> - - - - + android:orientation="vertical"> - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_download.xml b/app/src/main/res/layout/item_download.xml index f23794e94..8413da785 100644 --- a/app/src/main/res/layout/item_download.xml +++ b/app/src/main/res/layout/item_download.xml @@ -1,23 +1,59 @@ - + android:layout_width="0dp" + android:layout_height="wrap_content" + android:singleLine="true" + app:layout_constraintRight_toLeftOf="@+id/iv_download" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintLeft_toLeftOf="parent" /> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:singleLine="true" + app:layout_constraintRight_toLeftOf="@+id/iv_download" + app:layout_constraintTop_toBottomOf="@+id/tv_name" + app:layout_constraintLeft_toLeftOf="parent" /> + android:layout_width="0dp" + android:layout_height="wrap_content" + android:singleLine="true" + app:layout_constraintRight_toLeftOf="@+id/iv_download" + app:layout_constraintTop_toBottomOf="@id/tv_author" + app:layout_constraintLeft_toLeftOf="parent" /> - \ No newline at end of file + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_fillet_text.xml b/app/src/main/res/layout/item_fillet_text.xml new file mode 100644 index 000000000..331bd8a2c --- /dev/null +++ b/app/src/main/res/layout/item_fillet_text.xml @@ -0,0 +1,15 @@ + + diff --git a/app/src/main/res/layout/item_group_manage.xml b/app/src/main/res/layout/item_group_manage.xml index d7d85630a..763a19acd 100644 --- a/app/src/main/res/layout/item_group_manage.xml +++ b/app/src/main/res/layout/item_group_manage.xml @@ -4,6 +4,7 @@ android:layout_height="wrap_content" android:background="@color/background" android:padding="8dp" + android:gravity="center_vertical" android:orientation="horizontal"> + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_icon_preference.xml b/app/src/main/res/layout/item_icon_preference.xml index b8231284c..543a0841c 100644 --- a/app/src/main/res/layout/item_icon_preference.xml +++ b/app/src/main/res/layout/item_icon_preference.xml @@ -11,13 +11,14 @@ android:layout_height="match_parent" android:adjustViewBounds="true" android:contentDescription="ICON" - android:padding="6dip" + android:padding="10dip" tools:ignore="HardcodedText" /> + tools:ignore="RtlHardcoded,RtlSymmetry" /> \ No newline at end of file diff --git a/app/src/main/res/layout/item_import_book.xml b/app/src/main/res/layout/item_import_book.xml index f14841ad0..1ad9742bd 100644 --- a/app/src/main/res/layout/item_import_book.xml +++ b/app/src/main/res/layout/item_import_book.xml @@ -1,8 +1,90 @@ + android:layout_height="60dp" + android:background="?attr/selectableItemBackground" + android:orientation="horizontal" + android:baselineAligned="false"> + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/item_rss_article.xml b/app/src/main/res/layout/item_rss_article.xml index f67f4d694..df31c7d70 100644 --- a/app/src/main/res/layout/item_rss_article.xml +++ b/app/src/main/res/layout/item_rss_article.xml @@ -10,6 +10,8 @@ android:id="@+id/tv_title" android:layout_width="0dp" android:layout_height="0dp" + android:maxLines="2" + android:ellipsize="end" android:text="@string/app_name" android:textColor="@color/tv_text_default" android:textSize="16sp" diff --git a/app/src/main/res/layout/item_search.xml b/app/src/main/res/layout/item_search.xml index 940315b98..73154072b 100644 --- a/app/src/main/res/layout/item_search.xml +++ b/app/src/main/res/layout/item_search.xml @@ -19,7 +19,7 @@ app:layout_constraintTop_toTopOf="parent" tools:ignore="UnusedAttribute" /> - - - - - - - - - - + android:orientation="horizontal" /> - - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/item_text.xml b/app/src/main/res/layout/item_text.xml index 331bd8a2c..e37188e47 100644 --- a/app/src/main/res/layout/item_text.xml +++ b/app/src/main/res/layout/item_text.xml @@ -4,8 +4,7 @@ android:id="@+id/text_view" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:layout_margin="3dp" - android:background="@drawable/selector_fillet_btn_bg" + android:background="?attr/selectableItemBackground" android:ellipsize="end" android:gravity="center" android:padding="5dp" diff --git a/app/src/main/res/layout/item_toc_regex.xml b/app/src/main/res/layout/item_toc_regex.xml new file mode 100644 index 000000000..6872044c4 --- /dev/null +++ b/app/src/main/res/layout/item_toc_regex.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/popup_action_menu.xml b/app/src/main/res/layout/popup_action_menu.xml new file mode 100644 index 000000000..b5273a77a --- /dev/null +++ b/app/src/main/res/layout/popup_action_menu.xml @@ -0,0 +1,8 @@ + + diff --git a/app/src/main/res/layout/view_book_page.xml b/app/src/main/res/layout/view_book_page.xml index 410efe662..81c9e09d4 100644 --- a/app/src/main/res/layout/view_book_page.xml +++ b/app/src/main/res/layout/view_book_page.xml @@ -1,14 +1,12 @@ + android:orientation="vertical"> + + + android:layout_weight="1" /> + + diff --git a/app/src/main/res/layout/view_detail_seek_bar.xml b/app/src/main/res/layout/view_detail_seek_bar.xml new file mode 100644 index 000000000..af87a83bb --- /dev/null +++ b/app/src/main/res/layout/view_detail_seek_bar.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/view_load_more.xml b/app/src/main/res/layout/view_load_more.xml index 21b0491c9..c4a7ea04f 100644 --- a/app/src/main/res/layout/view_load_more.xml +++ b/app/src/main/res/layout/view_load_more.xml @@ -1,5 +1,5 @@ - - \ No newline at end of file + \ No newline at end of file diff --git a/app/src/main/res/layout/view_select_action_bar.xml b/app/src/main/res/layout/view_select_action_bar.xml new file mode 100644 index 000000000..e725a57b7 --- /dev/null +++ b/app/src/main/res/layout/view_select_action_bar.xml @@ -0,0 +1,57 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/about.xml b/app/src/main/res/menu/about.xml index 7dd8f1e86..f13fe31ae 100644 --- a/app/src/main/res/menu/about.xml +++ b/app/src/main/res/menu/about.xml @@ -1,9 +1,18 @@ - + + \ No newline at end of file diff --git a/app/src/main/res/menu/arrange_book.xml b/app/src/main/res/menu/arrange_book.xml new file mode 100644 index 000000000..25f88a1fc --- /dev/null +++ b/app/src/main/res/menu/arrange_book.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/arrange_book_sel.xml b/app/src/main/res/menu/arrange_book_sel.xml new file mode 100644 index 000000000..1985d09cd --- /dev/null +++ b/app/src/main/res/menu/arrange_book_sel.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/book_source.xml b/app/src/main/res/menu/book_source.xml index 5740683bd..649060e9f 100644 --- a/app/src/main/res/menu/book_source.xml +++ b/app/src/main/res/menu/book_source.xml @@ -3,59 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/res/menu/book_source_item.xml b/app/src/main/res/menu/book_source_item.xml new file mode 100644 index 000000000..a3cdcd258 --- /dev/null +++ b/app/src/main/res/menu/book_source_item.xml @@ -0,0 +1,16 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/book_source_sel.xml b/app/src/main/res/menu/book_source_sel.xml new file mode 100644 index 000000000..dff64669a --- /dev/null +++ b/app/src/main/res/menu/book_source_sel.xml @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/change_source.xml b/app/src/main/res/menu/change_source.xml new file mode 100644 index 000000000..9c24f2dfa --- /dev/null +++ b/app/src/main/res/menu/change_source.xml @@ -0,0 +1,19 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/content_select_action.xml b/app/src/main/res/menu/content_select_action.xml index d83b82ef3..52ac3e0f6 100644 --- a/app/src/main/res/menu/content_select_action.xml +++ b/app/src/main/res/menu/content_select_action.xml @@ -1,9 +1,16 @@ - + + android:title="@string/replace" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/explore_item.xml b/app/src/main/res/menu/explore_item.xml new file mode 100644 index 000000000..8cc2fe0c3 --- /dev/null +++ b/app/src/main/res/menu/explore_item.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/import_book.xml b/app/src/main/res/menu/import_book.xml new file mode 100644 index 000000000..8ace963eb --- /dev/null +++ b/app/src/main/res/menu/import_book.xml @@ -0,0 +1,11 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/import_book_sel.xml b/app/src/main/res/menu/import_book_sel.xml new file mode 100644 index 000000000..1985d09cd --- /dev/null +++ b/app/src/main/res/menu/import_book_sel.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_explore.xml b/app/src/main/res/menu/main_explore.xml new file mode 100644 index 000000000..5d429a443 --- /dev/null +++ b/app/src/main/res/menu/main_explore.xml @@ -0,0 +1,15 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/main_rss.xml b/app/src/main/res/menu/main_rss.xml index 4403621bd..97343e877 100644 --- a/app/src/main/res/menu/main_rss.xml +++ b/app/src/main/res/menu/main_rss.xml @@ -5,7 +5,7 @@ diff --git a/app/src/main/res/menu/read_book.xml b/app/src/main/res/menu/read_book.xml index 2994a6dbe..5d776e519 100644 --- a/app/src/main/res/menu/read_book.xml +++ b/app/src/main/res/menu/read_book.xml @@ -3,7 +3,7 @@ xmlns:tools="http://schemas.android.com/tools" tools:context=".ui.main.MainActivity"> - + @@ -80,9 +80,9 @@ app:showAsAction="never" /> - - - - - - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/replace_rule_sel.xml b/app/src/main/res/menu/replace_rule_sel.xml new file mode 100644 index 000000000..000686775 --- /dev/null +++ b/app/src/main/res/menu/replace_rule_sel.xml @@ -0,0 +1,20 @@ + + + + + + + + + + diff --git a/app/src/main/res/menu/rss_read.xml b/app/src/main/res/menu/rss_read.xml index f32d01d2e..7afb7f128 100644 --- a/app/src/main/res/menu/rss_read.xml +++ b/app/src/main/res/menu/rss_read.xml @@ -4,7 +4,7 @@ diff --git a/app/src/main/res/menu/rss_source.xml b/app/src/main/res/menu/rss_source.xml index 36bbef025..d2ac21cec 100644 --- a/app/src/main/res/menu/rss_source.xml +++ b/app/src/main/res/menu/rss_source.xml @@ -3,49 +3,6 @@ xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools"> - - - - - - - - - - - - - - - - - - - - - - + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/rss_source_sel.xml b/app/src/main/res/menu/rss_source_sel.xml new file mode 100644 index 000000000..360f456b5 --- /dev/null +++ b/app/src/main/res/menu/rss_source_sel.xml @@ -0,0 +1,31 @@ + + + + + + + + + + + + + + diff --git a/app/src/main/res/menu/txt_toc_regex.xml b/app/src/main/res/menu/txt_toc_regex.xml new file mode 100644 index 000000000..38a50e8cc --- /dev/null +++ b/app/src/main/res/menu/txt_toc_regex.xml @@ -0,0 +1,18 @@ + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index eca70cfe5..e8409cf31 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -1,5 +1,5 @@ - - + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml deleted file mode 100644 index eca70cfe5..000000000 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher1.xml b/app/src/main/res/mipmap-anydpi-v26/launcher1.xml new file mode 100644 index 000000000..57e845ad3 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/launcher1.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher2.xml b/app/src/main/res/mipmap-anydpi-v26/launcher2.xml new file mode 100644 index 000000000..409e0d965 --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/launcher2.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher3.xml b/app/src/main/res/mipmap-anydpi-v26/launcher3.xml new file mode 100644 index 000000000..24428243f --- /dev/null +++ b/app/src/main/res/mipmap-anydpi-v26/launcher3.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png deleted file mode 100644 index 9e96002d2..000000000 Binary files a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png deleted file mode 100644 index e27af4a03..000000000 Binary files a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png deleted file mode 100644 index 9d6a0e1ac..000000000 Binary files a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png deleted file mode 100644 index eac246932..000000000 Binary files a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png deleted file mode 100644 index 6fe033b04..000000000 Binary files a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png and /dev/null differ diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml index 4933cfa2b..18ab012e0 100644 --- a/app/src/main/res/values-night/colors.xml +++ b/app/src/main/res/values-night/colors.xml @@ -4,9 +4,9 @@ @color/md_blue_grey_700 @color/md_deep_orange_800 - @color/md_grey_800 - #353535 - #282828 + @color/md_grey_900 + @color/md_grey_850 + @color/md_grey_800 #69000000 diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml deleted file mode 100644 index 78554f9eb..000000000 --- a/app/src/main/res/values-zh/strings.xml +++ /dev/null @@ -1,5 +0,0 @@ - - - - - \ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml index 7534dcab1..87fd0d8e0 100644 --- a/app/src/main/res/values/arrays.xml +++ b/app/src/main/res/values/arrays.xml @@ -63,6 +63,18 @@ @string/jf_convert_f + + 跟随系统 + 亮色主题 + 暗色主题 + + + + 0 + 1 + 2 + + 自动 黑色 @@ -78,13 +90,19 @@ 常亮 - + 0 60 120 180 -1 - + + + + @string/default_path + @string/sys_folder_picker + @string/app_folder_picker + @string/screen_unspecified @@ -115,13 +133,23 @@ + iconMain icon1 icon2 + icon3 - @string/icon_main - @string/icon_book + ic_launcher + launcher1 + launcher2 + launcher3 + + + + 关闭 + 繁体转简体 + 简体转繁体 \ No newline at end of file diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 297ba1065..e42a82a0d 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -1,6 +1,8 @@ + + @@ -65,6 +67,11 @@ + + + + + @@ -97,7 +104,7 @@ - + @@ -126,6 +133,8 @@ + + @@ -137,12 +146,12 @@ - - + + - + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index d150286a4..054222265 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -7,16 +7,15 @@ #FF578FCC #FF212227 - #FF272731 #eb4333 #439b53 #00000000 - @color/md_grey_100 - #dedede - #fcfcfc + @color/md_grey_50 + @color/md_grey_100 + @color/md_grey_300 #00000000 #30000000 diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index 08f52ee5c..000000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #EC5436 - \ No newline at end of file diff --git a/app/src/main/res/values/ids.xml b/app/src/main/res/values/ids.xml index 016946f36..cc2d9c170 100644 --- a/app/src/main/res/values/ids.xml +++ b/app/src/main/res/values/ids.xml @@ -1,8 +1,5 @@ - - - diff --git a/app/src/main/res/values/pref_key_value.xml b/app/src/main/res/values/pref_key_value.xml index 71c26bc29..7bcded4da 100644 --- a/app/src/main/res/values/pref_key_value.xml +++ b/app/src/main/res/values/pref_key_value.xml @@ -13,11 +13,6 @@ downloadPath checkUpdate - ic_launcher_round - book_launcher_round - - Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/62.0.2357.134 Safari/537.36 - https://gitee.com/alanskycn/yuedu/blob/master/Rule/README.md https://github.com/gedoor/legado https://github.com/gedoor/legado/graphs/contributors diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 3ceda2af3..3b121d53f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -24,7 +24,7 @@ 点击重试 正在加载 - + 提醒 编辑 删除 替换 @@ -33,9 +33,10 @@ 启用 替换净化-搜索 书架 - 收藏 - 已收藏 - 未收藏 + 收藏夹 + 收藏 + 已收藏 + 未收藏 订阅 全部 最近阅读 @@ -77,7 +78,7 @@ 共%s个Text文件 加载中… 重试 - Web服务 + Web 服务 web编辑书源 http://%s:%d 离线下载 @@ -85,7 +86,7 @@ 下载选择的章节到本地 换源 - \u3000\u3000这是一款开源的阅读软件,你可以fork我们的代码自己编译APK。欢迎提交代码帮助改善应用。\n\u3000\u3000公众号[开源阅读软件]! + \u3000\u3000这是一款使用Kotlin全新开发的开源的阅读软件,欢迎您的加入。关注公众号[开源阅读软件]! 阅读3.0下载地址:\nhttps://play.google.com/store/apps/details?id=io.legado.app @@ -204,6 +205,8 @@ 替换规则名称 选择操作 全选 + 全选(%d/%d) + 取消(%d/%d) 深色模式 启动页 开始下载 @@ -211,7 +214,7 @@ 暂无任务 已下载 %d/%d 导入选择书籍 - 更新和搜索线程数,如感觉卡顿请减小线程数,量力而行 + 更新和搜索线程数,太多会卡顿 切换图标 删除书籍 开始阅读 @@ -251,6 +254,7 @@ 文字颜色和背景(长按自定义) 沉浸式状态栏 还剩%d章未下载 + 没有选择 长按输入颜色值 加载中… 追更区 @@ -316,21 +320,21 @@ 替换范围,选填书名或者源名 分组 内容缓存路径 - 清理缓存 + 清理缓存 系统文件选择器 新版本 下载更新 朗读时音量键翻页 Tip边距跟随边距调整 允许更新 - 反转选择 + 反选 搜索书名、作者 书名、作者、URL 常见问题 显示所有发现 关闭则只显示勾选源的发现 - 更新目录 - txt目录正则 + 更新目录 + Txt目录正则 设置编码 倒序-顺序 排序 @@ -433,6 +437,7 @@ 扫描二维码 选中时点击可弹出菜单 主题 + 主题模式 默认主题 恢复主题为默认配色 加入QQ群 @@ -447,12 +452,15 @@ 切换显示样式 导入本地书籍需存储权限 夜间模式 + E-Ink 模式 本软件需要存储权限来存储备份书籍信息 再按一次退出程序 导入本地书籍需存储权限 网络连接不可用 + 确认 + 是否确认删除? 是否删除全部书籍? 是否同时删除已下载的书籍目录? 扫描二维码需相机权限 @@ -478,7 +486,7 @@ 二字符缩进 三字符缩进 四字符缩进 - 选择SD卡 + 选择文件夹 没有发现,可以在书源里添加。 恢复默认 自定义缓存路径需要存储权限 @@ -522,8 +530,6 @@ 切换软件显示在桌面的图标 帮助 我的 - - ]]> 阅读 %d%% %d分钟 @@ -537,6 +543,7 @@ 分组管理 分组选择 编辑分组 + 移入分组 添加分组 新建替换 分组 @@ -547,6 +554,8 @@ 启用所选 禁用所选 导出所选 + 导出 + 加载目录 TTS 输入你的WebDav授权密码 输入你的服务器地址 @@ -571,6 +580,7 @@ 音频 转到后台 正在导入 + 正在导出 自定义翻页按键 上一页按键 下一页按键 @@ -582,5 +592,38 @@ 文字太多,生成二维码失败 分享RSS源 分享书源 - + 自动切换夜间模式 + 夜间模式跟随系统 + 上级 + 在线朗读音色 + (%d/%d) + 显示订阅 + 服务已停止 + 正在启动服务\n具体信息查看通知栏 + 默认路径 + 系统文件夹选择器 + 自带选择器\n(Android10以上因权限限制可能无法使用) + Android10以上因权限限制可能无法读写文件 + 长按文字在操作菜单中显示阅读·搜索 + 文字操作显示搜索 + 记录日志 + 中文简繁体转换 + 图标为矢量图标,Android8.0以前不支持 + 朗读设置 + 主界面 + 长按选择文本 + 页眉 + 正文 + 页角 + 文本选择结束位置 + 文本选择开始位置 + 共用布局 + 标题居中 + 浏览器 + 导入默认规则 + 名称 + 正则 + 更多菜单 + + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index ec0b50735..d5c9e7104 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -41,7 +41,7 @@