Merge remote-tracking branch 'source/master'
This commit is contained in:
@@ -54,6 +54,7 @@ Legado is a free and open source novel reader for Android.
|
|||||||
* [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
|
* [Coolapk](https://www.coolapk.com/apk/io.legado.app.release)
|
||||||
* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
|
* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
|
||||||
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
|
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
|
||||||
|
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
|
||||||
|
|
||||||
|
|
||||||
#### IOS-苹果
|
#### IOS-苹果
|
||||||
|
|||||||
@@ -1,30 +1,37 @@
|
|||||||
# 阅读API
|
# 阅读API
|
||||||
|
|
||||||
## 对于Web的配置
|
## 对于Web的配置
|
||||||
您需要先在设置中启用"Web 服务"。
|
|
||||||
|
您需要先在设置中启用"Web 服务"。
|
||||||
|
|
||||||
## 使用
|
## 使用
|
||||||
|
|
||||||
### Web
|
### Web
|
||||||
|
|
||||||
以下说明假设您的操作在本机进行,且开放端口为1234。
|
以下说明假设您的操作在本机进行,且开放端口为1234。
|
||||||
如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。
|
如果您要从远程计算机访问[阅读](),请将`127.0.0.1`替换成手机IP。
|
||||||
|
|
||||||
#### 插入单个书源
|
#### 插入单个书源
|
||||||
|
|
||||||
|
请求BODY内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/saveSource
|
URL = http://127.0.0.1:1234/saveSource
|
||||||
Method = POST
|
Method = POST
|
||||||
```
|
```
|
||||||
|
|
||||||
请求BODY内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
|
|
||||||
|
|
||||||
#### 插入多个书源or订阅源
|
#### 插入多个书源or订阅源
|
||||||
|
|
||||||
|
请求BODY内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/saveBookSources
|
URL = http://127.0.0.1:1234/saveBookSources
|
||||||
URL = http://127.0.0.1:1234/saveRssSources
|
URL = http://127.0.0.1:1234/saveRssSources
|
||||||
Method = POST
|
Method = POST
|
||||||
```
|
```
|
||||||
|
|
||||||
请求BODY内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
|
||||||
|
|
||||||
#### 获取书源
|
#### 获取书源
|
||||||
|
|
||||||
```
|
```
|
||||||
@@ -43,151 +50,172 @@ Method = GET
|
|||||||
|
|
||||||
#### 删除多个书源or订阅源
|
#### 删除多个书源or订阅源
|
||||||
|
|
||||||
|
请求BODY内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/deleteBookSources
|
URL = http://127.0.0.1:1234/deleteBookSources
|
||||||
URL = http://127.0.0.1:1234/deleteRssSources
|
URL = http://127.0.0.1:1234/deleteRssSources
|
||||||
Method = POST
|
Method = POST
|
||||||
```
|
```
|
||||||
|
|
||||||
请求BODY内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
|
||||||
|
|
||||||
#### 插入书籍
|
#### 插入书籍
|
||||||
|
|
||||||
|
请求BODY内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/saveBook
|
URL = http://127.0.0.1:1234/saveBook
|
||||||
Method = POST
|
Method = POST
|
||||||
```
|
```
|
||||||
|
|
||||||
请求BODY内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
|
|
||||||
|
|
||||||
#### 获取所有书籍
|
#### 获取所有书籍
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/getBookshelf
|
URL = http://127.0.0.1:1234/getBookshelf
|
||||||
Method = GET
|
Method = GET
|
||||||
```
|
```
|
||||||
|
|
||||||
获取APP内的所有书籍。
|
获取APP内的所有书籍。
|
||||||
|
|
||||||
#### 获取书籍章节列表
|
#### 获取书籍章节列表
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/getChapterList?url=xxx
|
URL = http://127.0.0.1:1234/getChapterList?url=xxx
|
||||||
Method = GET
|
Method = GET
|
||||||
```
|
```
|
||||||
|
|
||||||
获取指定图书的章节列表。
|
获取指定图书的章节列表。
|
||||||
|
|
||||||
#### 获取书籍内容
|
#### 获取书籍内容
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
|
URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
|
||||||
Method = GET
|
Method = GET
|
||||||
```
|
```
|
||||||
|
|
||||||
获取指定图书的第`index`章节的文本内容。
|
获取指定图书的第`index`章节的文本内容。
|
||||||
|
|
||||||
#### 获取封面
|
#### 获取封面
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = http://127.0.0.1:1234/cover?path=xxxxx
|
URL = http://127.0.0.1:1234/cover?path=xxxxx
|
||||||
Method = GET
|
Method = GET
|
||||||
```
|
```
|
||||||
|
|
||||||
|
#### 保存书籍进度
|
||||||
|
|
||||||
|
请求BODY内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookProgress.kt)。
|
||||||
|
|
||||||
|
```
|
||||||
|
URL = http://127.0.0.1:1234/saveBookProgress
|
||||||
|
Method = POST
|
||||||
|
```
|
||||||
|
|
||||||
### Content Provider
|
### Content Provider
|
||||||
|
|
||||||
* 需声明`io.legado.READ_WRITE`权限
|
* 需声明`io.legado.READ_WRITE`权限
|
||||||
* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
|
* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
|
||||||
* 以下出现的`providerHost`请自行替换
|
* 以下出现的`providerHost`请自行替换
|
||||||
|
|
||||||
#### 插入单个书源or订阅源
|
#### 插入单个书源or订阅源
|
||||||
|
|
||||||
|
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/bookSource/insert
|
URL = content://providerHost/bookSource/insert
|
||||||
URL = content://providerHost/rssSource/insert
|
URL = content://providerHost/rssSource/insert
|
||||||
Method = insert
|
Method = insert
|
||||||
```
|
```
|
||||||
|
|
||||||
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
|
|
||||||
|
|
||||||
#### 插入多个书源or订阅源
|
#### 插入多个书源or订阅源
|
||||||
|
|
||||||
|
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/bookSources/insert
|
URL = content://providerHost/bookSources/insert
|
||||||
URL = content://providerHost/rssSources/insert
|
URL = content://providerHost/rssSources/insert
|
||||||
Method = insert
|
Method = insert
|
||||||
```
|
```
|
||||||
|
|
||||||
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
|
||||||
|
|
||||||
#### 获取书源or订阅源
|
#### 获取书源or订阅源
|
||||||
|
|
||||||
|
获取指定URL对应的书源信息。
|
||||||
|
用`Cursor.getString(0)`取出返回结果。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/bookSource/query?url=xxx
|
URL = content://providerHost/bookSource/query?url=xxx
|
||||||
URL = content://providerHost/rssSource/query?url=xxx
|
URL = content://providerHost/rssSource/query?url=xxx
|
||||||
Method = query
|
Method = query
|
||||||
```
|
```
|
||||||
|
|
||||||
获取指定URL对应的书源信息。
|
|
||||||
用`Cursor.getString(0)`取出返回结果。
|
|
||||||
|
|
||||||
#### 获取所有书源or订阅源
|
#### 获取所有书源or订阅源
|
||||||
|
|
||||||
|
获取APP内的所有订阅源。
|
||||||
|
用`Cursor.getString(0)`取出返回结果。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/bookSources/query
|
URL = content://providerHost/bookSources/query
|
||||||
URL = content://providerHost/rssSources/query
|
URL = content://providerHost/rssSources/query
|
||||||
Method = query
|
Method = query
|
||||||
```
|
```
|
||||||
|
|
||||||
获取APP内的所有书源。
|
|
||||||
用`Cursor.getString(0)`取出返回结果。
|
|
||||||
|
|
||||||
#### 删除多个书源or订阅源
|
#### 删除多个书源or订阅源
|
||||||
|
|
||||||
|
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/bookSources/delete
|
URL = content://providerHost/bookSources/delete
|
||||||
URL = content://providerHost/rssSources/delete
|
URL = content://providerHost/rssSources/delete
|
||||||
Method = delete
|
Method = delete
|
||||||
```
|
```
|
||||||
|
|
||||||
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt),**为数组格式**。
|
|
||||||
|
|
||||||
#### 插入书籍
|
#### 插入书籍
|
||||||
|
|
||||||
|
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
||||||
|
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/book/insert
|
URL = content://providerHost/book/insert
|
||||||
Method = insert
|
Method = insert
|
||||||
```
|
```
|
||||||
|
|
||||||
创建`Key="json"`的`ContentValues`,内容为`JSON`字符串,
|
|
||||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/Book.kt)。
|
|
||||||
|
|
||||||
#### 获取所有书籍
|
#### 获取所有书籍
|
||||||
|
|
||||||
|
获取APP内的所有书籍。
|
||||||
|
用`Cursor.getString(0)`取出返回结果。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/books/query
|
URL = content://providerHost/books/query
|
||||||
Method = query
|
Method = query
|
||||||
```
|
```
|
||||||
|
|
||||||
获取APP内的所有书籍。
|
#### 获取书籍章节列表
|
||||||
|
|
||||||
|
获取指定图书的章节列表。
|
||||||
用`Cursor.getString(0)`取出返回结果。
|
用`Cursor.getString(0)`取出返回结果。
|
||||||
|
|
||||||
#### 获取书籍章节列表
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/book/chapter/query?url=xxx
|
URL = content://providerHost/book/chapter/query?url=xxx
|
||||||
Method = query
|
Method = query
|
||||||
```
|
```
|
||||||
|
|
||||||
获取指定图书的章节列表。
|
|
||||||
用`Cursor.getString(0)`取出返回结果。
|
|
||||||
|
|
||||||
#### 获取书籍内容
|
#### 获取书籍内容
|
||||||
|
|
||||||
|
获取指定图书的第`index`章节的文本内容。
|
||||||
|
用`Cursor.getString(0)`取出返回结果。
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/book/content/query?url=xxx&index=1
|
URL = content://providerHost/book/content/query?url=xxx&index=1
|
||||||
Method = query
|
Method = query
|
||||||
```
|
```
|
||||||
获取指定图书的第`index`章节的文本内容。
|
|
||||||
用`Cursor.getString(0)`取出返回结果。
|
|
||||||
|
|
||||||
#### 获取封面
|
#### 获取封面
|
||||||
|
|
||||||
```
|
```
|
||||||
URL = content://providerHost/book/cover/query?path=xxxx
|
URL = content://providerHost/book/cover/query?path=xxxx
|
||||||
Method = query
|
Method = query
|
||||||
|
|||||||
+6
-4
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
|||||||
apply plugin: 'kotlin-parcelize'
|
apply plugin: 'kotlin-parcelize'
|
||||||
apply plugin: 'kotlin-kapt'
|
apply plugin: 'kotlin-kapt'
|
||||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||||
//apply plugin: 'com.google.gms.google-services'
|
apply plugin: 'com.google.gms.google-services'
|
||||||
apply from: 'download.gradle'
|
apply from: 'download.gradle'
|
||||||
|
|
||||||
static def releaseTime() {
|
static def releaseTime() {
|
||||||
@@ -179,8 +179,10 @@ dependencies {
|
|||||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
|
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
|
||||||
|
|
||||||
//firebase
|
//firebase
|
||||||
//implementation platform('com.google.firebase:firebase-bom:29.1.0')
|
implementation platform('com.google.firebase:firebase-bom:30.0.1')
|
||||||
//implementation 'com.google.firebase:firebase-analytics-ktx'
|
implementation 'com.google.firebase:firebase-analytics-ktx:21.0.0'
|
||||||
|
implementation platform('com.google.firebase:firebase-bom:30.0.1')
|
||||||
|
implementation 'com.google.firebase:firebase-perf-ktx:20.0.6'
|
||||||
|
|
||||||
//media
|
//media
|
||||||
implementation("androidx.media:media:1.6.0")
|
implementation("androidx.media:media:1.6.0")
|
||||||
@@ -219,7 +221,7 @@ dependencies {
|
|||||||
implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
|
implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
|
||||||
|
|
||||||
//Glide
|
//Glide
|
||||||
def glideVersion = "4.13.1"
|
def glideVersion = "4.13.2"
|
||||||
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
implementation("com.github.bumptech.glide:glide:$glideVersion")
|
||||||
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
kapt("com.github.bumptech.glide:compiler:$glideVersion")
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
package="io.legado.app">
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.INTERNET" />
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
<uses-permission android:name="android.permission.ACCESS_MEDIA_LOCATION" />
|
||||||
|
|||||||
@@ -10,7 +10,11 @@
|
|||||||
@Json: json规则,直接写时以$.开头可省略@Json
|
@Json: json规则,直接写时以$.开头可省略@Json
|
||||||
: regex规则,不可省略,只可以用在书籍列表和目录列表
|
: regex规则,不可省略,只可以用在书籍列表和目录列表
|
||||||
```
|
```
|
||||||
|
* 书源类型: 文件
|
||||||
|
> 对于类似知轩藏书提供文件整合下载的网站,可以'在书源详情的下载URL规则获取文件链接,支持多个链接,阅读会自动下载并导入
|
||||||
|
|
||||||
|
* CookieJar
|
||||||
|
> 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站
|
||||||
* 登录UI
|
* 登录UI
|
||||||
> 不使用内置webView登录网站,需要使用`登录URL`规则实现登录逻辑,可使用`登录检查JS`检查登录结果
|
> 不使用内置webView登录网站,需要使用`登录URL`规则实现登录逻辑,可使用`登录检查JS`检查登录结果
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -11,6 +11,25 @@
|
|||||||
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
|
* 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
|
||||||
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
|
* 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
|
||||||
|
|
||||||
|
**2022/05/16**
|
||||||
|
|
||||||
|
* 添加firebase性能监测
|
||||||
|
* 清除cookie时清除webView的cookie
|
||||||
|
|
||||||
|
**2022/05/15**
|
||||||
|
|
||||||
|
* 源编辑添加cookieJar选项
|
||||||
|
* 源编辑菜单里添加清除cookie,如果之前能用的书源不能用了,可以关闭cookieJar后点下菜单里的清除cookie后再试
|
||||||
|
* 书源支持文件类型 by Xwite
|
||||||
|
* 启用firebase收集崩溃日志
|
||||||
|
* web端阅读实现无限滚动 by Xwite
|
||||||
|
* 进入阅读界面时如果本地书籍有更新自动刷新目录
|
||||||
|
|
||||||
|
**2022/05/11**
|
||||||
|
|
||||||
|
* 修复替换报错的bug
|
||||||
|
* 优化目录界面替换
|
||||||
|
|
||||||
**2022/05/10**
|
**2022/05/10**
|
||||||
|
|
||||||
* 更新cronet: 101.0.4951.61
|
* 更新cronet: 101.0.4951.61
|
||||||
|
|||||||
@@ -25,7 +25,7 @@
|
|||||||
<div>
|
<div>
|
||||||
<div>源类型 :</div>
|
<div>源类型 :</div>
|
||||||
<textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
|
<textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
|
||||||
placeholder="<必填>0:文本 1:音频 2:图片"></textarea>
|
placeholder="<必填>0:文本 1:音频 2:图片 3:文件(只提供下载的网站)"></textarea>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<div>源名称 :</div>
|
<div>源名称 :</div>
|
||||||
|
|||||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
@@ -1,3 +1,3 @@
|
|||||||
<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.b93f38f9.css" rel="prefetch"><link href="css/detail.7f9a50b2.css" rel="prefetch"><link href="js/about.66694f8c.js" rel="prefetch"><link href="js/detail.08ed5833.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.9f1876f9.js" rel="preload" as="script"><link href="js/chunk-vendors.7abad833.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
|
<!DOCTYPE html><html lang="en" style="padding: 0;height:100%"><head><meta charset="utf-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width,initial-scale=1,maximum-scale=1,user-scalable=0"><link rel="icon" href="../favicon.ico" type="image/x-icon"><link rel="shortcut icon" href="../favicon.ico" type="image/x-icon"><title>Legado Bookshelf</title><link href="css/about.65a00131.css" rel="prefetch"><link href="css/detail.12a39aa7.css" rel="prefetch"><link href="js/about.2456ab2e.js" rel="prefetch"><link href="js/detail.8e2caf8f.js" rel="prefetch"><link href="css/app.e4c919b7.css" rel="preload" as="style"><link href="css/chunk-vendors.bd1373b6.css" rel="preload" as="style"><link href="js/app.a68b7203.js" rel="preload" as="script"><link href="js/chunk-vendors.f36dda93.js" rel="preload" as="script"><link href="css/chunk-vendors.bd1373b6.css" rel="stylesheet"><link href="css/app.e4c919b7.css" rel="stylesheet"></head><style>body::-webkit-scrollbar {
|
||||||
display: none;
|
display: none;
|
||||||
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.7abad833.js"></script><script src="js/app.9f1876f9.js"></script></body></html>
|
}</style><body style="margin: 0;height:100%"><noscript><strong>We're sorry but yd-web-tool doesn't work properly without JavaScript enabled. Please enable it to continue.</strong></noscript><div id="app"></div><script src="js/chunk-vendors.f36dda93.js"></script><script src="js/app.a68b7203.js"></script></body></html>
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+1
-1
File diff suppressed because one or more lines are too long
+2
-2
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -21,7 +21,8 @@ class ReaderProvider : ContentProvider() {
|
|||||||
private enum class RequestCode {
|
private enum class RequestCode {
|
||||||
SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
|
SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
|
||||||
SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
|
SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
|
||||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover
|
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover,
|
||||||
|
SaveBookProgress
|
||||||
}
|
}
|
||||||
|
|
||||||
private val postBodyKey = "json"
|
private val postBodyKey = "json"
|
||||||
@@ -86,6 +87,9 @@ class ReaderProvider : ContentProvider() {
|
|||||||
RequestCode.SaveBook -> values?.let {
|
RequestCode.SaveBook -> values?.let {
|
||||||
BookController.saveBook(values.getAsString(postBodyKey))
|
BookController.saveBook(values.getAsString(postBodyKey))
|
||||||
}
|
}
|
||||||
|
RequestCode.SaveBookProgress -> values?.let {
|
||||||
|
BookController.saveBookProgress(values.getAsString(postBodyKey))
|
||||||
|
}
|
||||||
else -> throw IllegalStateException(
|
else -> throw IllegalStateException(
|
||||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,33 +1,26 @@
|
|||||||
package io.legado.app.api.controller
|
package io.legado.app.api.controller
|
||||||
|
|
||||||
import android.net.Uri
|
|
||||||
import android.util.Base64
|
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import androidx.documentfile.provider.DocumentFile
|
|
||||||
import io.legado.app.api.ReturnData
|
import io.legado.app.api.ReturnData
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookProgress
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.help.BookHelp
|
import io.legado.app.help.BookHelp
|
||||||
import io.legado.app.help.CacheManager
|
import io.legado.app.help.CacheManager
|
||||||
import io.legado.app.help.ContentProcessor
|
import io.legado.app.help.ContentProcessor
|
||||||
import io.legado.app.help.config.AppConfig
|
|
||||||
import io.legado.app.help.glide.ImageLoader
|
import io.legado.app.help.glide.ImageLoader
|
||||||
import io.legado.app.help.storage.AppWebDav
|
import io.legado.app.help.storage.AppWebDav
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
import io.legado.app.model.localBook.EpubFile
|
|
||||||
import io.legado.app.model.localBook.LocalBook
|
import io.legado.app.model.localBook.LocalBook
|
||||||
import io.legado.app.model.localBook.UmdFile
|
|
||||||
import io.legado.app.model.webBook.WebBook
|
import io.legado.app.model.webBook.WebBook
|
||||||
import io.legado.app.ui.book.read.page.provider.ImageProvider
|
import io.legado.app.ui.book.read.page.provider.ImageProvider
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.runBlocking
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
|
|
||||||
object BookController {
|
object BookController {
|
||||||
|
|
||||||
@@ -177,7 +170,6 @@ object BookController {
|
|||||||
var content: String? = BookHelp.getContent(book, chapter)
|
var content: String? = BookHelp.getContent(book, chapter)
|
||||||
if (content != null) {
|
if (content != null) {
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||||
saveBookReadIndex(book, index)
|
|
||||||
content = runBlocking {
|
content = runBlocking {
|
||||||
contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
|
contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
|
||||||
.joinToString("\n")
|
.joinToString("\n")
|
||||||
@@ -190,7 +182,6 @@ object BookController {
|
|||||||
content = runBlocking {
|
content = runBlocking {
|
||||||
WebBook.getContentAwait(this, bookSource, book, chapter).let {
|
WebBook.getContentAwait(this, bookSource, book, chapter).let {
|
||||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||||
saveBookReadIndex(book, index)
|
|
||||||
contentProcessor.getContent(book, chapter, it, includeTitle = false)
|
contentProcessor.getContent(book, chapter, it, includeTitle = false)
|
||||||
.joinToString("\n")
|
.joinToString("\n")
|
||||||
}
|
}
|
||||||
@@ -222,20 +213,26 @@ object BookController {
|
|||||||
/**
|
/**
|
||||||
* 保存进度
|
* 保存进度
|
||||||
*/
|
*/
|
||||||
private fun saveBookReadIndex(book: Book, index: Int) {
|
fun saveBookProgress(postData: String?): ReturnData {
|
||||||
book.durChapterIndex = index
|
val returnData = ReturnData()
|
||||||
book.durChapterTime = System.currentTimeMillis()
|
GSON.fromJsonObject<BookProgress>(postData)
|
||||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
|
.onFailure { it.printOnDebug() }
|
||||||
book.durChapterTitle = it.title
|
.getOrNull()?.let { bookProgress ->
|
||||||
}
|
appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
|
||||||
appDb.bookDao.update(book)
|
book.durChapterIndex = bookProgress.durChapterIndex
|
||||||
AppWebDav.uploadBookProgress(book)
|
book.durChapterPos = bookProgress.durChapterPos
|
||||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
book.durChapterTitle = bookProgress.durChapterTitle
|
||||||
ReadBook.book = book
|
book.durChapterTime = bookProgress.durChapterTime
|
||||||
ReadBook.durChapterIndex = index
|
appDb.bookDao.update(book)
|
||||||
ReadBook.clearTextChapter()
|
AppWebDav.uploadBookProgress(bookProgress)
|
||||||
ReadBook.loadContent(true)
|
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||||
}
|
ReadBook.book = book
|
||||||
|
ReadBook.durChapterIndex = book.durChapterIndex
|
||||||
|
}
|
||||||
|
return returnData.setData("")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return returnData.setErrorMsg("格式不对")
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -248,45 +245,7 @@ object BookController {
|
|||||||
val fileData = parameters["fileData"]?.firstOrNull()
|
val fileData = parameters["fileData"]?.firstOrNull()
|
||||||
?: return returnData.setErrorMsg("fileData 不能为空")
|
?: return returnData.setErrorMsg("fileData 不能为空")
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
LocalBook.importFileOnLine(fileData, fileName)
|
||||||
if (defaultBookTreeUri.isNullOrBlank()) return returnData.setErrorMsg("没有设置书籍保存位置!")
|
|
||||||
val treeUri = Uri.parse(defaultBookTreeUri)
|
|
||||||
val fileBytes =
|
|
||||||
Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
|
|
||||||
val uri = if (treeUri.isContentScheme()) {
|
|
||||||
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
|
||||||
var doc = treeDoc!!.findFile(fileName)
|
|
||||||
if (doc == null) {
|
|
||||||
doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
|
|
||||||
?: throw SecurityException("Permission Denial")
|
|
||||||
}
|
|
||||||
appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
|
|
||||||
oStream.write(fileBytes)
|
|
||||||
}
|
|
||||||
doc.uri
|
|
||||||
} else {
|
|
||||||
val treeFile = File(treeUri.path!!)
|
|
||||||
val file = treeFile.getFile(fileName)
|
|
||||||
FileOutputStream(file).use { oStream ->
|
|
||||||
oStream.write(fileBytes)
|
|
||||||
}
|
|
||||||
Uri.fromFile(file)
|
|
||||||
}
|
|
||||||
val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
|
|
||||||
val book = Book(
|
|
||||||
bookUrl = uri.toString(),
|
|
||||||
name = nameAuthor.first,
|
|
||||||
author = nameAuthor.second,
|
|
||||||
originName = fileName,
|
|
||||||
coverUrl = FileUtils.getPath(
|
|
||||||
appCtx.externalFiles,
|
|
||||||
"covers",
|
|
||||||
"${MD5Utils.md5Encode16(uri.toString())}.jpg"
|
|
||||||
)
|
|
||||||
)
|
|
||||||
if (book.isEpub()) EpubFile.upBookInfo(book)
|
|
||||||
if (book.isUmd()) UmdFile.upBookInfo(book)
|
|
||||||
appDb.bookDao.insert(book)
|
|
||||||
}.onFailure {
|
}.onFailure {
|
||||||
return when (it) {
|
return when (it) {
|
||||||
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")
|
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")
|
||||||
|
|||||||
@@ -5,11 +5,12 @@ import androidx.annotation.IntDef
|
|||||||
object BookType {
|
object BookType {
|
||||||
const val default = 0 // 0 文本
|
const val default = 0 // 0 文本
|
||||||
const val audio = 1 // 1 音频
|
const val audio = 1 // 1 音频
|
||||||
const val image = 2 //图片
|
const val image = 2 // 2 图片
|
||||||
|
const val file = 3 // 3 只提供下载服务的网站
|
||||||
const val local = "loc_book"
|
const val local = "loc_book"
|
||||||
|
|
||||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||||
@Retention(AnnotationRetention.SOURCE)
|
@Retention(AnnotationRetention.SOURCE)
|
||||||
@IntDef(default, audio, image)
|
@IntDef(default, audio, image, file)
|
||||||
annotation class Type
|
annotation class Type
|
||||||
}
|
}
|
||||||
@@ -28,4 +28,5 @@ object EventBus {
|
|||||||
const val TIP_COLOR = "tipColor"
|
const val TIP_COLOR = "tipColor"
|
||||||
const val SOURCE_CHANGED = "sourceChanged"
|
const val SOURCE_CHANGED = "sourceChanged"
|
||||||
const val SEARCH_RESULT = "searchResult"
|
const val SEARCH_RESULT = "searchResult"
|
||||||
|
const val BOOK_URL_CHANGED = "bookUrlChanged"
|
||||||
}
|
}
|
||||||
@@ -20,7 +20,7 @@ val appDb by lazy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Database(
|
@Database(
|
||||||
version = 48,
|
version = 49,
|
||||||
exportSchema = true,
|
exportSchema = true,
|
||||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||||
@@ -32,7 +32,8 @@ val appDb by lazy {
|
|||||||
AutoMigration(from = 44, to = 45),
|
AutoMigration(from = 44, to = 45),
|
||||||
AutoMigration(from = 45, to = 46),
|
AutoMigration(from = 45, to = 46),
|
||||||
AutoMigration(from = 46, to = 47),
|
AutoMigration(from = 46, to = 47),
|
||||||
AutoMigration(from = 47, to = 48)
|
AutoMigration(from = 47, to = 48),
|
||||||
|
AutoMigration(from = 48, to = 49)
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
abstract class AppDatabase : RoomDatabase() {
|
abstract class AppDatabase : RoomDatabase() {
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ interface BaseSource : JsExtensions {
|
|||||||
var loginUrl: String? // 登录地址
|
var loginUrl: String? // 登录地址
|
||||||
var loginUi: String? // 登录UI
|
var loginUi: String? // 登录UI
|
||||||
var header: String? // 请求头
|
var header: String? // 请求头
|
||||||
|
var enabledCookieJar: Boolean? //启用cookieJar
|
||||||
|
|
||||||
fun getTag(): String
|
fun getTag(): String
|
||||||
|
|
||||||
|
|||||||
@@ -154,6 +154,10 @@ data class Book(
|
|||||||
@IgnoredOnParcel
|
@IgnoredOnParcel
|
||||||
override var tocHtml: String? = null
|
override var tocHtml: String? = null
|
||||||
|
|
||||||
|
@Ignore
|
||||||
|
@IgnoredOnParcel
|
||||||
|
var downloadUrls: List<String>? = null
|
||||||
|
|
||||||
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
|
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
|
||||||
|
|
||||||
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import androidx.room.Ignore
|
|||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.exception.RegexTimeoutException
|
import io.legado.app.exception.RegexTimeoutException
|
||||||
@@ -15,6 +16,7 @@ import io.legado.app.help.config.AppConfig
|
|||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import kotlinx.parcelize.IgnoredOnParcel
|
import kotlinx.parcelize.IgnoredOnParcel
|
||||||
import kotlinx.parcelize.Parcelize
|
import kotlinx.parcelize.Parcelize
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
@@ -93,7 +95,7 @@ data class BookChapter(
|
|||||||
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
|
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (useReplace && replaceRules != null) {
|
if (useReplace && replaceRules != null) kotlin.run {
|
||||||
replaceRules.forEach { item ->
|
replaceRules.forEach { item ->
|
||||||
if (item.pattern.isNotEmpty()) {
|
if (item.pattern.isNotEmpty()) {
|
||||||
try {
|
try {
|
||||||
@@ -112,7 +114,10 @@ data class BookChapter(
|
|||||||
} catch (e: RegexTimeoutException) {
|
} catch (e: RegexTimeoutException) {
|
||||||
item.isEnabled = false
|
item.isEnabled = false
|
||||||
appDb.replaceRuleDao.update(item)
|
appDb.replaceRuleDao.update(item)
|
||||||
|
} catch (e: CancellationException) {
|
||||||
|
return@run
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
|
AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e)
|
||||||
appCtx.toastOnUi("${item.name}替换出错")
|
appCtx.toastOnUi("${item.name}替换出错")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import kotlinx.parcelize.Parcelize
|
|||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.InputStream
|
import java.io.InputStream
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
@Parcelize
|
@Parcelize
|
||||||
@TypeConverters(BookSource.Converters::class)
|
@TypeConverters(BookSource.Converters::class)
|
||||||
@Entity(
|
@Entity(
|
||||||
@@ -27,7 +28,7 @@ data class BookSource(
|
|||||||
var bookSourceName: String = "",
|
var bookSourceName: String = "",
|
||||||
// 分组
|
// 分组
|
||||||
var bookSourceGroup: String? = null,
|
var bookSourceGroup: String? = null,
|
||||||
// 类型,0 文本,1 音频, 2 图片
|
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
|
||||||
@BookType.Type
|
@BookType.Type
|
||||||
var bookSourceType: Int = 0,
|
var bookSourceType: Int = 0,
|
||||||
// 详情页url正则
|
// 详情页url正则
|
||||||
@@ -38,6 +39,9 @@ data class BookSource(
|
|||||||
var enabled: Boolean = true,
|
var enabled: Boolean = true,
|
||||||
// 启用发现
|
// 启用发现
|
||||||
var enabledExplore: Boolean = true,
|
var enabledExplore: Boolean = true,
|
||||||
|
// 启用okhttp CookieJAr 自动保存每次请求的cookie
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
override var enabledCookieJar: Boolean? = false,
|
||||||
// 并发率
|
// 并发率
|
||||||
override var concurrentRate: String? = null,
|
override var concurrentRate: String? = null,
|
||||||
// 请求头
|
// 请求头
|
||||||
@@ -158,7 +162,7 @@ data class BookSource(
|
|||||||
|
|
||||||
fun removeGroup(groups: String): BookSource {
|
fun removeGroup(groups: String): BookSource {
|
||||||
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
|
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
|
||||||
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
|
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
|
||||||
bookSourceGroup = TextUtils.join(",", it)
|
bookSourceGroup = TextUtils.join(",", it)
|
||||||
}
|
}
|
||||||
return this
|
return this
|
||||||
@@ -190,6 +194,7 @@ data class BookSource(
|
|||||||
&& equal(bookSourceComment, source.bookSourceComment)
|
&& equal(bookSourceComment, source.bookSourceComment)
|
||||||
&& enabled == source.enabled
|
&& enabled == source.enabled
|
||||||
&& enabledExplore == source.enabledExplore
|
&& enabledExplore == source.enabledExplore
|
||||||
|
&& enabledCookieJar == source.enabledCookieJar
|
||||||
&& equal(header, source.header)
|
&& equal(header, source.header)
|
||||||
&& loginUrl == source.loginUrl
|
&& loginUrl == source.loginUrl
|
||||||
&& equal(exploreUrl, source.exploreUrl)
|
&& equal(exploreUrl, source.exploreUrl)
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ data class HttpTTS(
|
|||||||
override var loginUrl: String? = null,
|
override var loginUrl: String? = null,
|
||||||
override var loginUi: String? = null,
|
override var loginUi: String? = null,
|
||||||
override var header: String? = null,
|
override var header: String? = null,
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
override var enabledCookieJar: Boolean? = false,
|
||||||
var loginCheckJs: String? = null,
|
var loginCheckJs: String? = null,
|
||||||
@ColumnInfo(defaultValue = "0")
|
@ColumnInfo(defaultValue = "0")
|
||||||
var lastUpdateTime: Long = System.currentTimeMillis()
|
var lastUpdateTime: Long = System.currentTimeMillis()
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.legado.app.data.entities
|
package io.legado.app.data.entities
|
||||||
|
|
||||||
import android.os.Parcelable
|
import android.os.Parcelable
|
||||||
|
import androidx.room.ColumnInfo
|
||||||
import androidx.room.Entity
|
import androidx.room.Entity
|
||||||
import androidx.room.Index
|
import androidx.room.Index
|
||||||
import androidx.room.PrimaryKey
|
import androidx.room.PrimaryKey
|
||||||
@@ -19,6 +20,8 @@ data class RssSource(
|
|||||||
var sourceGroup: String? = null,
|
var sourceGroup: String? = null,
|
||||||
var sourceComment: String? = null,
|
var sourceComment: String? = null,
|
||||||
var enabled: Boolean = true,
|
var enabled: Boolean = true,
|
||||||
|
@ColumnInfo(defaultValue = "0")
|
||||||
|
override var enabledCookieJar: Boolean? = false,
|
||||||
override var concurrentRate: String? = null, //并发率
|
override var concurrentRate: String? = null, //并发率
|
||||||
override var header: String? = null, // 请求头
|
override var header: String? = null, // 请求头
|
||||||
override var loginUrl: String? = null, // 登录地址
|
override var loginUrl: String? = null, // 登录地址
|
||||||
@@ -150,6 +153,7 @@ data class RssSource(
|
|||||||
style = doc.readString("$.style"),
|
style = doc.readString("$.style"),
|
||||||
enableJs = doc.readBool("$.enableJs") ?: true,
|
enableJs = doc.readBool("$.enableJs") ?: true,
|
||||||
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
||||||
|
enabledCookieJar = doc.readBool("$.enabledCookieJar") ?: false,
|
||||||
customOrder = doc.readInt("$.customOrder") ?: 0
|
customOrder = doc.readInt("$.customOrder") ?: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,5 +16,6 @@ data class BookInfoRule(
|
|||||||
var coverUrl: String? = null,
|
var coverUrl: String? = null,
|
||||||
var tocUrl: String? = null,
|
var tocUrl: String? = null,
|
||||||
var wordCount: String? = null,
|
var wordCount: String? = null,
|
||||||
var canReName: String? = null
|
var canReName: String? = null,
|
||||||
|
var downloadUrls: String? = null
|
||||||
) : Parcelable
|
) : Parcelable
|
||||||
@@ -12,6 +12,7 @@ import io.legado.app.help.config.ReadBookConfig
|
|||||||
import io.legado.app.utils.msg
|
import io.legado.app.utils.msg
|
||||||
import io.legado.app.utils.replace
|
import io.legado.app.utils.replace
|
||||||
import io.legado.app.utils.toastOnUi
|
import io.legado.app.utils.toastOnUi
|
||||||
|
import kotlinx.coroutines.CancellationException
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import java.util.concurrent.CopyOnWriteArrayList
|
import java.util.concurrent.CopyOnWriteArrayList
|
||||||
@@ -135,7 +136,7 @@ class ContentProcessor private constructor(
|
|||||||
var mContent = content
|
var mContent = content
|
||||||
getContentReplaceRules().forEach { item ->
|
getContentReplaceRules().forEach { item ->
|
||||||
if (item.pattern.isNotEmpty()) {
|
if (item.pattern.isNotEmpty()) {
|
||||||
kotlin.runCatching {
|
try {
|
||||||
mContent = if (item.isRegex) {
|
mContent = if (item.isRegex) {
|
||||||
mContent.replace(
|
mContent.replace(
|
||||||
item.pattern.toRegex(),
|
item.pattern.toRegex(),
|
||||||
@@ -145,18 +146,15 @@ class ContentProcessor private constructor(
|
|||||||
} else {
|
} else {
|
||||||
mContent.replace(item.pattern, item.replacement)
|
mContent.replace(item.pattern, item.replacement)
|
||||||
}
|
}
|
||||||
}.onFailure {
|
} catch (e: RegexTimeoutException) {
|
||||||
when (it) {
|
item.isEnabled = false
|
||||||
is RegexTimeoutException -> {
|
appDb.replaceRuleDao.update(item)
|
||||||
item.isEnabled = false
|
return item.name + e.msg
|
||||||
appDb.replaceRuleDao.update(item)
|
} catch (e: CancellationException) {
|
||||||
return item.name + it.msg
|
return mContent
|
||||||
}
|
} catch (e: Exception) {
|
||||||
else -> {
|
AppLog.put("${item.name}替换出错\n替换内容\n${mContent}", e)
|
||||||
AppLog.put("${item.name}替换出错\n${it.localizedMessage}", it)
|
appCtx.toastOnUi("${item.name}替换出错")
|
||||||
appCtx.toastOnUi("${item.name}替换出错")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -197,12 +197,10 @@ interface JsExtensions {
|
|||||||
*js实现读取cookie
|
*js实现读取cookie
|
||||||
*/
|
*/
|
||||||
fun getCookie(tag: String, key: String? = null): String {
|
fun getCookie(tag: String, key: String? = null): String {
|
||||||
val cookie = CookieStore.getCookie(tag)
|
|
||||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
|
||||||
return if (key != null) {
|
return if (key != null) {
|
||||||
cookieMap[key] ?: ""
|
CookieStore.getKey(tag, key)
|
||||||
} else {
|
} else {
|
||||||
cookie
|
CookieStore.getCookie(tag)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -470,10 +468,8 @@ interface JsExtensions {
|
|||||||
* @return zip指定文件的数据
|
* @return zip指定文件的数据
|
||||||
*/
|
*/
|
||||||
fun getZipByteArrayContent(url: String, path: String): ByteArray? {
|
fun getZipByteArrayContent(url: String, path: String): ByteArray? {
|
||||||
val bytes = if (url.startsWith("http://") || url.startsWith("https://")) {
|
val bytes = if (url.isAbsUrl()) {
|
||||||
runBlocking {
|
AnalyzeUrl(url, source = getSource()).getByteArray()
|
||||||
return@runBlocking okHttpClient.newCallResponseBody { url(url) }.bytes()
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
StringUtils.hexStringToByte(url)
|
StringUtils.hexStringToByte(url)
|
||||||
}
|
}
|
||||||
@@ -517,7 +513,7 @@ interface JsExtensions {
|
|||||||
str.isAbsUrl() -> runBlocking {
|
str.isAbsUrl() -> runBlocking {
|
||||||
var x = CacheManager.getByteArray(key)
|
var x = CacheManager.getByteArray(key)
|
||||||
if (x == null) {
|
if (x == null) {
|
||||||
x = okHttpClient.newCallResponseBody { url(str) }.bytes()
|
x = AnalyzeUrl(str, source = getSource()).getByteArray()
|
||||||
x.let {
|
x.let {
|
||||||
CacheManager.put(key, it)
|
CacheManager.put(key, it)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +1,47 @@
|
|||||||
package io.legado.app.help
|
package io.legado.app.help
|
||||||
|
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
|
|
||||||
object ReplaceAnalyzer {
|
object ReplaceAnalyzer {
|
||||||
|
|
||||||
fun jsonToReplaceRules(json: String): List<ReplaceRule> {
|
fun jsonToReplaceRules(json: String): Result<MutableList<ReplaceRule>> {
|
||||||
val replaceRules = mutableListOf<ReplaceRule>()
|
return kotlin.runCatching {
|
||||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
val replaceRules = mutableListOf<ReplaceRule>()
|
||||||
for (item in items) {
|
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||||
val jsonItem = jsonPath.parse(item)
|
for (item in items) {
|
||||||
jsonToReplaceRule(jsonItem.jsonString())?.let {
|
val jsonItem = jsonPath.parse(item)
|
||||||
if (it.isValid()) {
|
jsonToReplaceRule(jsonItem.jsonString()).getOrThrow().let {
|
||||||
replaceRules.add(it)
|
if (it.isValid()) {
|
||||||
|
replaceRules.add(it)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
replaceRules
|
||||||
}
|
}
|
||||||
return replaceRules
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun jsonToReplaceRule(json: String): ReplaceRule? {
|
fun jsonToReplaceRule(json: String): Result<ReplaceRule> {
|
||||||
val replaceRule: ReplaceRule? = GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
return runCatching {
|
||||||
runCatching {
|
val replaceRule: ReplaceRule? =
|
||||||
|
GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
||||||
if (replaceRule == null || replaceRule.pattern.isBlank()) {
|
if (replaceRule == null || replaceRule.pattern.isBlank()) {
|
||||||
val jsonItem = jsonPath.parse(json.trim())
|
val jsonItem = jsonPath.parse(json.trim())
|
||||||
val rule = ReplaceRule()
|
val rule = ReplaceRule()
|
||||||
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
|
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
|
||||||
rule.pattern = jsonItem.readString("$.regex") ?: ""
|
rule.pattern = jsonItem.readString("$.regex") ?: ""
|
||||||
if (rule.pattern.isEmpty()) return null
|
if (rule.pattern.isEmpty()) throw NoStackTraceException("格式不对")
|
||||||
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
|
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
|
||||||
rule.replacement = jsonItem.readString("$.replacement") ?: ""
|
rule.replacement = jsonItem.readString("$.replacement") ?: ""
|
||||||
rule.isRegex = jsonItem.readBool("$.isRegex") == true
|
rule.isRegex = jsonItem.readBool("$.isRegex") == true
|
||||||
rule.scope = jsonItem.readString("$.useTo")
|
rule.scope = jsonItem.readString("$.useTo")
|
||||||
rule.isEnabled = jsonItem.readBool("$.enable") == true
|
rule.isEnabled = jsonItem.readBool("$.enable") == true
|
||||||
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
|
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
|
||||||
return rule
|
return@runCatching rule
|
||||||
}
|
}
|
||||||
|
return@runCatching replaceRule
|
||||||
}
|
}
|
||||||
return replaceRule
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,7 @@ object SourceAnalyzer {
|
|||||||
source.customOrder = sourceAny.customOrder
|
source.customOrder = sourceAny.customOrder
|
||||||
source.enabled = sourceAny.enabled
|
source.enabled = sourceAny.enabled
|
||||||
source.enabledExplore = sourceAny.enabledExplore
|
source.enabledExplore = sourceAny.enabledExplore
|
||||||
|
source.enabledCookieJar = sourceAny.enabledCookieJar
|
||||||
source.concurrentRate = sourceAny.concurrentRate
|
source.concurrentRate = sourceAny.concurrentRate
|
||||||
source.header = sourceAny.header
|
source.header = sourceAny.header
|
||||||
source.loginUrl = when (sourceAny.loginUrl) {
|
source.loginUrl = when (sourceAny.loginUrl) {
|
||||||
@@ -219,6 +220,7 @@ object SourceAnalyzer {
|
|||||||
var customOrder: Int = 0, // 手动排序编号
|
var customOrder: Int = 0, // 手动排序编号
|
||||||
var enabled: Boolean = true, // 是否启用
|
var enabled: Boolean = true, // 是否启用
|
||||||
var enabledExplore: Boolean = true, // 启用发现
|
var enabledExplore: Boolean = true, // 启用发现
|
||||||
|
var enabledCookieJar: Boolean = false, // 启用CookieJar
|
||||||
var concurrentRate: String? = null, // 并发率
|
var concurrentRate: String? = null, // 并发率
|
||||||
var header: String? = null, // 请求头
|
var header: String? = null, // 请求头
|
||||||
var loginUrl: Any? = null, // 登录规则
|
var loginUrl: Any? = null, // 登录规则
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ object SourceVerificationHelp {
|
|||||||
startBrowser(source, url, title, true)
|
startBrowser(source, url, title, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitUserInput: Boolean = false
|
var waitUserInput = false
|
||||||
while(CacheManager.get(key) == null) {
|
while(CacheManager.get(key) == null) {
|
||||||
if (!waitUserInput) {
|
if (!waitUserInput) {
|
||||||
AppLog.putDebug("等待返回验证结果...")
|
AppLog.putDebug("等待返回验证结果...")
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import org.apache.commons.text.StringEscapeUtils
|
|||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.lang.ref.WeakReference
|
import java.lang.ref.WeakReference
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 后台webView
|
* 后台webView
|
||||||
@@ -50,14 +51,14 @@ class BackstageWebView(
|
|||||||
|
|
||||||
override fun onError(error: Throwable) {
|
override fun onError(error: Throwable) {
|
||||||
if (!block.isCompleted)
|
if (!block.isCompleted)
|
||||||
block.cancel(error)
|
block.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
runOnUI {
|
runOnUI {
|
||||||
try {
|
try {
|
||||||
load()
|
load()
|
||||||
} catch (error: Throwable) {
|
} catch (error: Throwable) {
|
||||||
block.cancel(error)
|
block.resumeWithException(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ package io.legado.app.help.http
|
|||||||
import android.text.TextUtils
|
import android.text.TextUtils
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Cookie
|
import io.legado.app.data.entities.Cookie
|
||||||
import io.legado.app.help.http.api.CookieManager
|
|
||||||
import io.legado.app.help.CacheManager
|
import io.legado.app.help.CacheManager
|
||||||
|
import io.legado.app.help.http.api.CookieManager
|
||||||
import io.legado.app.utils.NetworkUtils
|
import io.legado.app.utils.NetworkUtils
|
||||||
|
|
||||||
object CookieStore : CookieManager {
|
object CookieStore : CookieManager {
|
||||||
@@ -44,7 +44,7 @@ object CookieStore : CookieManager {
|
|||||||
CacheManager.getFromMemory("${domain}_cookie")?.let { return it }
|
CacheManager.getFromMemory("${domain}_cookie")?.let { return it }
|
||||||
val cookieBean = appDb.cookieDao.get(domain)
|
val cookieBean = appDb.cookieDao.get(domain)
|
||||||
val cookie = cookieBean?.cookie ?: ""
|
val cookie = cookieBean?.cookie ?: ""
|
||||||
CacheManager.putMemory(url, cookie ?: "")
|
CacheManager.putMemory(url, cookie)
|
||||||
return cookie
|
return cookie
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -56,8 +56,9 @@ object CookieStore : CookieManager {
|
|||||||
|
|
||||||
override fun removeCookie(url: String) {
|
override fun removeCookie(url: String) {
|
||||||
val domain = NetworkUtils.getSubDomain(url)
|
val domain = NetworkUtils.getSubDomain(url)
|
||||||
CacheManager.deleteMemory("${domain}_cookie")
|
|
||||||
appDb.cookieDao.delete(domain)
|
appDb.cookieDao.delete(domain)
|
||||||
|
CacheManager.deleteMemory("${domain}_cookie")
|
||||||
|
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun cookieToMap(cookie: String): MutableMap<String, String> {
|
override fun cookieToMap(cookie: String): MutableMap<String, String> {
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package io.legado.app.help.http
|
package io.legado.app.help.http
|
||||||
|
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.help.CacheManager
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.http.cronet.CronetInterceptor
|
import io.legado.app.help.http.cronet.CronetInterceptor
|
||||||
import io.legado.app.help.http.cronet.CronetLoader
|
import io.legado.app.help.http.cronet.CronetLoader
|
||||||
|
import io.legado.app.utils.NetworkUtils
|
||||||
import okhttp3.*
|
import okhttp3.*
|
||||||
import java.net.InetSocketAddress
|
import java.net.InetSocketAddress
|
||||||
import java.net.Proxy
|
import java.net.Proxy
|
||||||
@@ -23,7 +25,10 @@ val cookieJar by lazy {
|
|||||||
|
|
||||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||||
cookies.forEach {
|
cookies.forEach {
|
||||||
CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
|
//CookieStore.replaceCookie(url.toString(), "${it.name}=${it.value}")
|
||||||
|
//临时保存 书源启用cookie选项再添加到数据库
|
||||||
|
val domain = NetworkUtils.getSubDomain(url.toString())
|
||||||
|
CacheManager.putMemory("${domain}_cookieJar", "${it.name}=${it.value}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -194,15 +194,28 @@ object AppWebDav {
|
|||||||
Coroutine.async {
|
Coroutine.async {
|
||||||
val bookProgress = BookProgress(book)
|
val bookProgress = BookProgress(book)
|
||||||
val json = GSON.toJson(bookProgress)
|
val json = GSON.toJson(bookProgress)
|
||||||
val url = getProgressUrl(book)
|
val url = getProgressUrl(book.name, book.author)
|
||||||
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
||||||
}.onError {
|
}.onError {
|
||||||
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getProgressUrl(book: Book): String {
|
fun uploadBookProgress(bookProgress: BookProgress) {
|
||||||
return bookProgressUrl + book.name + "_" + book.author + ".json"
|
val authorization = authorization ?: return
|
||||||
|
if (!syncBookProgress) return
|
||||||
|
if (!NetworkUtils.isAvailable()) return
|
||||||
|
Coroutine.async {
|
||||||
|
val json = GSON.toJson(bookProgress)
|
||||||
|
val url = getProgressUrl(bookProgress.name, bookProgress.author)
|
||||||
|
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
||||||
|
}.onError {
|
||||||
|
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getProgressUrl(name: String, author: String): String {
|
||||||
|
return bookProgressUrl + name + "_" + author + ".json"
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -210,7 +223,7 @@ object AppWebDav {
|
|||||||
*/
|
*/
|
||||||
suspend fun getBookProgress(book: Book): BookProgress? {
|
suspend fun getBookProgress(book: Book): BookProgress? {
|
||||||
authorization?.let {
|
authorization?.let {
|
||||||
val url = getProgressUrl(book)
|
val url = getProgressUrl(book.name, book.author)
|
||||||
kotlin.runCatching {
|
kotlin.runCatching {
|
||||||
WebDav(url, it).download().let { byteArray ->
|
WebDav(url, it).download().let { byteArray ->
|
||||||
val json = String(byteArray)
|
val json = String(byteArray)
|
||||||
|
|||||||
@@ -98,9 +98,12 @@ object ImportOldData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun importOldReplaceRule(json: String): Int {
|
private fun importOldReplaceRule(json: String): Int {
|
||||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(json)
|
val rules = ReplaceAnalyzer.jsonToReplaceRules(json).getOrNull()
|
||||||
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
rules?.let {
|
||||||
return rules.size
|
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
||||||
|
return rules.size
|
||||||
|
}
|
||||||
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun fromOldBooks(json: String): List<Book> {
|
private fun fromOldBooks(json: String): List<Book> {
|
||||||
|
|||||||
@@ -22,40 +22,36 @@ import kotlinx.coroutines.withContext
|
|||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.File
|
||||||
import java.io.FileInputStream
|
import java.io.FileInputStream
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
|
||||||
object Restore {
|
object Restore {
|
||||||
|
|
||||||
suspend fun restore(context: Context, path: String) {
|
suspend fun restore(context: Context, path: String) {
|
||||||
withContext(IO) {
|
kotlin.runCatching {
|
||||||
if (path.isContentScheme()) {
|
if (path.isContentScheme()) {
|
||||||
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
|
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
|
||||||
for (fileName in Backup.backupFileNames) {
|
if (Backup.backupFileNames.contains(doc.name)) {
|
||||||
if (doc.name == fileName) {
|
context.contentResolver.openInputStream(doc.uri)?.use { inputStream ->
|
||||||
DocumentUtils.readText(context, doc.uri).let {
|
val file = File("${Backup.backupPath}${File.separator}${doc.name}")
|
||||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName")
|
FileOutputStream(file).use { outputStream ->
|
||||||
.writeText(it)
|
inputStream.copyTo(outputStream)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
try {
|
val dir = File(path)
|
||||||
val file = File(path)
|
for (fileName in Backup.backupFileNames) {
|
||||||
for (fileName in Backup.backupFileNames) {
|
val file = dir.getFile(fileName)
|
||||||
file.getFile(fileName).let {
|
if (file.exists()) {
|
||||||
if (it.exists()) {
|
val target = File("${Backup.backupPath}${File.separator}$fileName")
|
||||||
it.copyTo(
|
file.copyTo(target, true)
|
||||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName"),
|
|
||||||
true
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
|
||||||
e.printOnDebug()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}.onFailure {
|
||||||
|
AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)
|
||||||
}
|
}
|
||||||
restoreDatabase()
|
restoreDatabase()
|
||||||
restoreConfig()
|
restoreConfig()
|
||||||
@@ -129,7 +125,7 @@ object Restore {
|
|||||||
ThemeConfig.upConfig()
|
ThemeConfig.upConfig()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printOnDebug()
|
AppLog.put("恢复主题出错\n${e.localizedMessage}", e)
|
||||||
}
|
}
|
||||||
if (!BackupConfig.ignoreReadConfig) {
|
if (!BackupConfig.ignoreReadConfig) {
|
||||||
//恢复阅读界面配置
|
//恢复阅读界面配置
|
||||||
@@ -142,7 +138,7 @@ object Restore {
|
|||||||
ReadBookConfig.initConfigs()
|
ReadBookConfig.initConfigs()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printOnDebug()
|
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
val file =
|
val file =
|
||||||
@@ -153,7 +149,7 @@ object Restore {
|
|||||||
ReadBookConfig.initShareConfig()
|
ReadBookConfig.initShareConfig()
|
||||||
}
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
e.printOnDebug()
|
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->
|
Preferences.getSharedPreferences(appCtx, path, "config")?.all?.let { map ->
|
||||||
|
|||||||
+2
-2
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
@@ -118,7 +118,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
|
|||||||
}
|
}
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
val v = io.legado.app.ui.widget.prefs.Preference.bindView<ColorPanelView>(
|
val v = io.legado.app.lib.prefs.Preference.bindView<ColorPanelView>(
|
||||||
context, holder, icon, title, summary, widgetLayoutResource,
|
context, holder, icon, title, summary, widgetLayoutResource,
|
||||||
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
|
io.legado.app.R.id.cpv_preference_preview_color_panel, 30, 30
|
||||||
)
|
)
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.AttributeSet
|
||||||
|
import android.widget.TextView
|
||||||
|
import androidx.preference.EditTextPreference.OnBindEditTextListener
|
||||||
|
import androidx.preference.PreferenceViewHolder
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
|
import io.legado.app.utils.applyTint
|
||||||
|
|
||||||
|
class EditTextPreference(context: Context, attrs: AttributeSet) :
|
||||||
|
androidx.preference.EditTextPreference(context, attrs) {
|
||||||
|
|
||||||
|
private var mOnBindEditTextListener: OnBindEditTextListener? = null
|
||||||
|
private val onBindEditTextListener = OnBindEditTextListener { editText ->
|
||||||
|
editText.applyTint(context.accentColor)
|
||||||
|
mOnBindEditTextListener?.onBindEditText(editText)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
// isPersistent = true
|
||||||
|
layoutResource = R.layout.view_preference
|
||||||
|
super.setOnBindEditTextListener(onBindEditTextListener)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
|
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
|
||||||
|
super.onBindViewHolder(holder)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun setOnBindEditTextListener(onBindEditTextListener: OnBindEditTextListener?) {
|
||||||
|
mOnBindEditTextListener = onBindEditTextListener
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+10
-1
@@ -1,9 +1,11 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat
|
import androidx.preference.PreferenceDialogFragmentCompat
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
import io.legado.app.lib.theme.filletBackground
|
import io.legado.app.lib.theme.filletBackground
|
||||||
|
|
||||||
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
||||||
@@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val dialog = super.onCreateDialog(savedInstanceState)
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||||
|
dialog.window?.decorView?.post {
|
||||||
|
(dialog as AlertDialog).run {
|
||||||
|
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.content.ContextWrapper
|
import android.content.ContextWrapper
|
||||||
+10
-1
@@ -1,9 +1,11 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.ListPreferenceDialogFragmentCompat
|
import androidx.preference.ListPreferenceDialogFragmentCompat
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat
|
import androidx.preference.PreferenceDialogFragmentCompat
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
import io.legado.app.lib.theme.filletBackground
|
import io.legado.app.lib.theme.filletBackground
|
||||||
|
|
||||||
class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
||||||
@@ -23,6 +25,13 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val dialog = super.onCreateDialog(savedInstanceState)
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||||
|
dialog.window?.decorView?.post {
|
||||||
|
(dialog as AlertDialog).run {
|
||||||
|
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
+10
-1
@@ -1,9 +1,11 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.app.Dialog
|
import android.app.Dialog
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.appcompat.app.AlertDialog
|
||||||
import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
|
import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
|
||||||
import androidx.preference.PreferenceDialogFragmentCompat
|
import androidx.preference.PreferenceDialogFragmentCompat
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
import io.legado.app.lib.theme.filletBackground
|
import io.legado.app.lib.theme.filletBackground
|
||||||
|
|
||||||
class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
|
class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
|
||||||
@@ -25,6 +27,13 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
|
|||||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||||
val dialog = super.onCreateDialog(savedInstanceState)
|
val dialog = super.onCreateDialog(savedInstanceState)
|
||||||
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
dialog.window?.setBackgroundDrawable(requireContext().filletBackground)
|
||||||
|
dialog.window?.decorView?.post {
|
||||||
|
(dialog as AlertDialog).run {
|
||||||
|
getButton(AlertDialog.BUTTON_NEGATIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_POSITIVE)?.setTextColor(accentColor)
|
||||||
|
getButton(AlertDialog.BUTTON_NEUTRAL)?.setTextColor(accentColor)
|
||||||
|
}
|
||||||
|
}
|
||||||
return dialog
|
return dialog
|
||||||
}
|
}
|
||||||
|
|
||||||
+3
-9
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
@@ -25,14 +25,8 @@ class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference
|
|||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||||
val v = Preference.bindView<TextView>(
|
val v = Preference.bindView<TextView>(
|
||||||
context,
|
context, holder, icon, title, summary, widgetLayoutResource,
|
||||||
holder,
|
R.id.text_view, isBottomBackground = isBottomBackground
|
||||||
icon,
|
|
||||||
title,
|
|
||||||
summary,
|
|
||||||
widgetLayoutResource,
|
|
||||||
R.id.text_view,
|
|
||||||
isBottomBackground = isBottomBackground
|
|
||||||
)
|
)
|
||||||
if (v is TextView) {
|
if (v is TextView) {
|
||||||
v.text = entry
|
v.text = entry
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.graphics.drawable.Drawable
|
import android.graphics.drawable.Drawable
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
package io.legado.app.lib.prefs
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
+8
-9
@@ -1,13 +1,13 @@
|
|||||||
package io.legado.app.base
|
package io.legado.app.lib.prefs.fragment
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.*
|
import androidx.preference.*
|
||||||
import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
|
import io.legado.app.lib.prefs.EditTextPreferenceDialog
|
||||||
import io.legado.app.ui.widget.prefs.ListPreferenceDialog
|
import io.legado.app.lib.prefs.ListPreferenceDialog
|
||||||
import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
|
import io.legado.app.lib.prefs.MultiSelectListPreferenceDialog
|
||||||
|
|
||||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
abstract class PreferenceFragment : PreferenceFragmentCompat() {
|
||||||
|
|
||||||
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
|
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
val f: DialogFragment = when (preference) {
|
val dialogFragment: DialogFragment = when (preference) {
|
||||||
is EditTextPreference -> {
|
is EditTextPreference -> {
|
||||||
EditTextPreferenceDialog.newInstance(preference.getKey())
|
EditTextPreferenceDialog.newInstance(preference.getKey())
|
||||||
}
|
}
|
||||||
@@ -54,10 +54,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
f.setTargetFragment(this, 0)
|
dialogFragment.setTargetFragment(this, 0)
|
||||||
|
|
||||||
f.show(parentFragmentManager, dialogFragmentTag)
|
dialogFragment.show(parentFragmentManager, dialogFragmentTag)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -150,6 +150,7 @@ object BookCover {
|
|||||||
override var loginUrl: String? = null,
|
override var loginUrl: String? = null,
|
||||||
override var loginUi: String? = null,
|
override var loginUi: String? = null,
|
||||||
override var header: String? = null,
|
override var header: String? = null,
|
||||||
|
override var enabledCookieJar: Boolean? = false,
|
||||||
) : BaseSource {
|
) : BaseSource {
|
||||||
|
|
||||||
override fun getTag(): String {
|
override fun getTag(): String {
|
||||||
|
|||||||
@@ -131,9 +131,9 @@ object CacheBook {
|
|||||||
|
|
||||||
class CacheBookModel(var bookSource: BookSource, var book: Book) {
|
class CacheBookModel(var bookSource: BookSource, var book: Book) {
|
||||||
|
|
||||||
private val waitDownloadSet = hashSetOf<Int>()
|
private val waitDownloadSet = linkedSetOf<Int>()
|
||||||
private val onDownloadSet = hashSetOf<Int>()
|
private val onDownloadSet = linkedSetOf<Int>()
|
||||||
private val successDownloadSet = hashSetOf<Int>()
|
private val successDownloadSet = linkedSetOf<Int>()
|
||||||
private val errorDownloadMap = hashMapOf<Int, Int>()
|
private val errorDownloadMap = hashMapOf<Int, Int>()
|
||||||
|
|
||||||
val waitCount get() = waitDownloadSet.size
|
val waitCount get() = waitDownloadSet.size
|
||||||
@@ -174,7 +174,7 @@ object CacheBook {
|
|||||||
}
|
}
|
||||||
onDownloadSet.remove(index)
|
onDownloadSet.remove(index)
|
||||||
//重试3次
|
//重试3次
|
||||||
if (errorDownloadMap[index] ?: 0 < 3) {
|
if ((errorDownloadMap[index] ?: 0) < 3) {
|
||||||
waitDownloadSet.add(index)
|
waitDownloadSet.add(index)
|
||||||
} else {
|
} else {
|
||||||
AppLog.put(
|
AppLog.put(
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package io.legado.app.model
|
|||||||
|
|
||||||
import android.annotation.SuppressLint
|
import android.annotation.SuppressLint
|
||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.entities.*
|
import io.legado.app.data.entities.*
|
||||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||||
import io.legado.app.model.rss.Rss
|
import io.legado.app.model.rss.Rss
|
||||||
@@ -238,7 +239,11 @@ object Debug {
|
|||||||
.onSuccess {
|
.onSuccess {
|
||||||
log(debugSource, "︽详情页解析完成")
|
log(debugSource, "︽详情页解析完成")
|
||||||
log(debugSource, showTime = false)
|
log(debugSource, showTime = false)
|
||||||
tocDebug(scope, bookSource, book)
|
if (book.type != BookType.file) {
|
||||||
|
tocDebug(scope, bookSource, book)
|
||||||
|
} else {
|
||||||
|
log(debugSource, "≡文件类书源跳过解析目录", state = 1000)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.onError {
|
.onError {
|
||||||
log(debugSource, it.msg, state = -1)
|
log(debugSource, it.msg, state = -1)
|
||||||
|
|||||||
@@ -69,6 +69,7 @@ class AnalyzeUrl(
|
|||||||
private var retry: Int = 0
|
private var retry: Int = 0
|
||||||
private var useWebView: Boolean = false
|
private var useWebView: Boolean = false
|
||||||
private var webJs: String? = null
|
private var webJs: String? = null
|
||||||
|
private val enabledCookieJar = source?.enabledCookieJar ?: false
|
||||||
|
|
||||||
init {
|
init {
|
||||||
if (!mUrl.isDataUrl()) {
|
if (!mUrl.isDataUrl()) {
|
||||||
@@ -519,17 +520,26 @@ class AnalyzeUrl(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
*设置cookie urlOption的优先级大于书源保存的cookie
|
*设置cookie 优先级
|
||||||
|
* urlOption临时cookie > 数据库cookie = okhttp CookieJar保存在内存中的cookie
|
||||||
*@param tag 书源url 缺省为传入的url
|
*@param tag 书源url 缺省为传入的url
|
||||||
*/
|
*/
|
||||||
private fun setCookie(tag: String?) {
|
private fun setCookie(tag: String?) {
|
||||||
val cookie = CookieStore.getCookie(tag ?: url)
|
val domain = NetworkUtils.getSubDomain(tag ?: url)
|
||||||
|
//书源启用保存cookie时 添加内存中的cookie到数据库
|
||||||
|
if (enabledCookieJar) {
|
||||||
|
val key = "${domain}_cookieJar"
|
||||||
|
CacheManager.getFromMemory(key)?.let {
|
||||||
|
CookieStore.replaceCookie(domain, it)
|
||||||
|
CacheManager.deleteMemory(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
val cookie = CookieStore.getCookie(domain)
|
||||||
if (cookie.isNotEmpty()) {
|
if (cookie.isNotEmpty()) {
|
||||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||||
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
|
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
|
||||||
cookieMap.putAll(customCookieMap)
|
cookieMap.putAll(customCookieMap)
|
||||||
val newCookie = CookieStore.mapToCookie(cookieMap)
|
CookieStore.mapToCookie(cookieMap)?.let {
|
||||||
newCookie?.let {
|
|
||||||
headerMap.put("Cookie", it)
|
headerMap.put("Cookie", it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,29 @@
|
|||||||
package io.legado.app.model.localBook
|
package io.legado.app.model.localBook
|
||||||
|
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
|
import android.util.Base64
|
||||||
import androidx.documentfile.provider.DocumentFile
|
import androidx.documentfile.provider.DocumentFile
|
||||||
import com.script.SimpleBindings
|
import com.script.SimpleBindings
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.BaseSource
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookChapter
|
import io.legado.app.data.entities.BookChapter
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.exception.TocEmptyException
|
import io.legado.app.exception.TocEmptyException
|
||||||
import io.legado.app.help.BookHelp
|
import io.legado.app.help.BookHelp
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import java.io.File
|
import java.io.*
|
||||||
import java.io.FileInputStream
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.InputStream
|
|
||||||
import java.util.regex.Pattern
|
import java.util.regex.Pattern
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 书籍文件导入 目录正文解析
|
||||||
|
* 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
|
||||||
|
*/
|
||||||
object LocalBook {
|
object LocalBook {
|
||||||
|
|
||||||
private val nameAuthorPatterns = arrayOf(
|
private val nameAuthorPatterns = arrayOf(
|
||||||
@@ -41,6 +46,20 @@ object LocalBook {
|
|||||||
throw FileNotFoundException("${uri.path} 文件不存在")
|
throw FileNotFoundException("${uri.path} 文件不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getLastModified(book: Book): Result<Long> {
|
||||||
|
return kotlin.runCatching {
|
||||||
|
val uri = Uri.parse(book.bookUrl)
|
||||||
|
if (uri.isContentScheme()) {
|
||||||
|
return@runCatching DocumentFile.fromSingleUri(appCtx, uri)!!.lastModified()
|
||||||
|
}
|
||||||
|
val file = File(uri.path!!)
|
||||||
|
if (file.exists()) {
|
||||||
|
return@runCatching File(uri.path!!).lastModified()
|
||||||
|
}
|
||||||
|
throw FileNotFoundException("${uri.path} 文件不存在")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Throws(Exception::class)
|
@Throws(Exception::class)
|
||||||
fun getChapterList(book: Book): ArrayList<BookChapter> {
|
fun getChapterList(book: Book): ArrayList<BookChapter> {
|
||||||
val chapters = when {
|
val chapters = when {
|
||||||
@@ -79,6 +98,23 @@ object LocalBook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载在线的文件并自动导入到阅读(txt umd epub)
|
||||||
|
* 压缩文件请先提示用户解压
|
||||||
|
*/
|
||||||
|
fun importFileOnLine(
|
||||||
|
str: String,
|
||||||
|
fileName: String,
|
||||||
|
source: BaseSource? = null,
|
||||||
|
): Book {
|
||||||
|
return saveBookFile(str, fileName, source).let {
|
||||||
|
importFile(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入本地文件
|
||||||
|
*/
|
||||||
fun importFile(uri: Uri): Book {
|
fun importFile(uri: Uri): Book {
|
||||||
val bookUrl: String
|
val bookUrl: String
|
||||||
val updateTime: Long
|
val updateTime: Long
|
||||||
@@ -119,7 +155,10 @@ object LocalBook {
|
|||||||
return book
|
return book
|
||||||
}
|
}
|
||||||
|
|
||||||
fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
/**
|
||||||
|
* 从文件分析书籍必要信息(书名 作者等)
|
||||||
|
*/
|
||||||
|
private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
||||||
val tempFileName = fileName.substringBeforeLast(".")
|
val tempFileName = fileName.substringBeforeLast(".")
|
||||||
var name: String
|
var name: String
|
||||||
var author: String
|
var author: String
|
||||||
@@ -171,4 +210,75 @@ object LocalBook {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 下载在线的文件
|
||||||
|
*/
|
||||||
|
fun saveBookFile(
|
||||||
|
str: String,
|
||||||
|
fileName: String,
|
||||||
|
source: BaseSource? = null,
|
||||||
|
): Uri {
|
||||||
|
val bytes = when {
|
||||||
|
str.isAbsUrl() -> AnalyzeUrl(str, source = source).getByteArray()
|
||||||
|
str.isDataUrl() -> Base64.decode(str.substringAfter("base64,"), Base64.DEFAULT)
|
||||||
|
else -> throw NoStackTraceException("在线导入书籍支持http/https/DataURL")
|
||||||
|
}
|
||||||
|
return saveBookFile(bytes, fileName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 分析下载文件类书源的下载链接的文件后缀
|
||||||
|
* https://www.example.com/download/{fileName}.{type} 含有文件名和后缀
|
||||||
|
* https://www.example.com/download/?fileid=1234, {type: "txt"} 规则设置
|
||||||
|
*/
|
||||||
|
fun parseFileSuffix(url: String): String {
|
||||||
|
val analyzeUrl = AnalyzeUrl(url)
|
||||||
|
val urlNoOption = analyzeUrl.url
|
||||||
|
val lastPath = urlNoOption.substringAfterLast("/")
|
||||||
|
val fileType = lastPath.substringAfterLast(".")
|
||||||
|
val type = analyzeUrl.type
|
||||||
|
return type ?: fileType
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun saveBookFile(
|
||||||
|
bytes: ByteArray,
|
||||||
|
fileName: String
|
||||||
|
): Uri {
|
||||||
|
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||||
|
if (defaultBookTreeUri.isNullOrBlank()) throw NoStackTraceException("没有设置书籍保存位置!")
|
||||||
|
val treeUri = Uri.parse(defaultBookTreeUri)
|
||||||
|
return if (treeUri.isContentScheme()) {
|
||||||
|
val treeDoc = DocumentFile.fromTreeUri(appCtx, treeUri)
|
||||||
|
var doc = treeDoc!!.findFile(fileName)
|
||||||
|
if (doc == null) {
|
||||||
|
doc = treeDoc.createFile(FileUtils.getMimeType(fileName), fileName)
|
||||||
|
?: throw SecurityException("Permission Denial")
|
||||||
|
}
|
||||||
|
appCtx.contentResolver.openOutputStream(doc.uri)!!.use { oStream ->
|
||||||
|
oStream.write(bytes)
|
||||||
|
}
|
||||||
|
doc.uri
|
||||||
|
} else {
|
||||||
|
val treeFile = File(treeUri.path!!)
|
||||||
|
val file = treeFile.getFile(fileName)
|
||||||
|
FileOutputStream(file).use { oStream ->
|
||||||
|
oStream.write(bytes)
|
||||||
|
}
|
||||||
|
Uri.fromFile(file)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
//文件类书源 合并在线书籍信息 在线 > 本地
|
||||||
|
fun mergeBook(localBook: Book, onLineBook: Book?): Book {
|
||||||
|
onLineBook ?: return localBook
|
||||||
|
localBook.name = onLineBook.name.ifBlank { localBook.name }
|
||||||
|
localBook.author = onLineBook.author.ifBlank { localBook.author }
|
||||||
|
localBook.coverUrl = onLineBook.coverUrl
|
||||||
|
localBook.intro =
|
||||||
|
if (onLineBook.intro.isNullOrBlank()) localBook.intro else onLineBook.intro
|
||||||
|
localBook.save()
|
||||||
|
return localBook
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
# 本地书籍解析
|
# 书籍文件导入解析
|
||||||
|
|
||||||
* BaseLocalBookParse.kt 本地书籍解析接口
|
* BaseLocalBookParse.kt 本地书籍解析接口
|
||||||
* LocalBook.kt 总入口
|
* LocalBook.kt 导入解析总入口
|
||||||
* TextFile.kt 解析txt
|
* TextFile.kt 解析txt
|
||||||
* EpubFile.kt 解析epub
|
* EpubFile.kt 解析epub
|
||||||
* UmdFile.kt 解析umd
|
* UmdFile.kt 解析umd
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package io.legado.app.model.webBook
|
package io.legado.app.model.webBook
|
||||||
|
|
||||||
|
import android.text.TextUtils
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
@@ -137,14 +139,29 @@ object BookInfo {
|
|||||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||||
DebugLog.e("获取封面出错", e)
|
DebugLog.e("获取封面出错", e)
|
||||||
}
|
}
|
||||||
scope.ensureActive()
|
if (book.type != BookType.file) {
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
scope.ensureActive()
|
||||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||||
if (book.tocUrl == baseUrl) {
|
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||||
book.tocHtml = body
|
if (book.tocUrl == baseUrl) {
|
||||||
|
book.tocHtml = body
|
||||||
|
}
|
||||||
|
Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
|
||||||
|
} else {
|
||||||
|
scope.ensureActive()
|
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取文件下载链接")
|
||||||
|
book.downloadUrls = analyzeRule.getStringList(infoRule.downloadUrls, isUrl = true)
|
||||||
|
if (book.downloadUrls == null) {
|
||||||
|
Debug.log(bookSource.bookSourceUrl, "└")
|
||||||
|
throw NoStackTraceException("下载链接为空")
|
||||||
|
} else {
|
||||||
|
Debug.log(
|
||||||
|
bookSource.bookSourceUrl,
|
||||||
|
"└" + TextUtils.join(",\n", book.downloadUrls!!)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Debug.log(bookSource.bookSourceUrl, "└${book.tocUrl}")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -6,6 +6,7 @@ import com.script.ScriptException
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseService
|
import io.legado.app.base.BaseService
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.IntentAction
|
import io.legado.app.constant.IntentAction
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
@@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
|
|||||||
mBook = WebBook.getBookInfoAwait(this, source, mBook)
|
mBook = WebBook.getBookInfoAwait(this, source, mBook)
|
||||||
}
|
}
|
||||||
//校验目录
|
//校验目录
|
||||||
if (CheckSource.checkCategory) {
|
if (CheckSource.checkCategory &&
|
||||||
|
source.bookSourceType != BookType.file
|
||||||
|
) {
|
||||||
val toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
|
val toc = WebBook.getChapterListAwait(this, source, mBook).getOrThrow()
|
||||||
val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
|
val nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
|
||||||
//校验正文
|
//校验正文
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|||||||
private val ttsFolderPath: String by lazy {
|
private val ttsFolderPath: String by lazy {
|
||||||
cacheDir.absolutePath + File.separator + "httpTTS" + File.separator
|
cacheDir.absolutePath + File.separator + "httpTTS" + File.separator
|
||||||
}
|
}
|
||||||
private var speechRate: Int = AppConfig.speechRatePlay
|
private var speechRate: Int = AppConfig.speechRatePlay + 5
|
||||||
private var downloadTask: Coroutine<*>? = null
|
private var downloadTask: Coroutine<*>? = null
|
||||||
private var playIndexJob: Job? = null
|
private var playIndexJob: Job? = null
|
||||||
private var downloadTaskIsActive = false
|
private var downloadTaskIsActive = false
|
||||||
@@ -301,7 +301,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
|||||||
override fun upSpeechRate(reset: Boolean) {
|
override fun upSpeechRate(reset: Boolean) {
|
||||||
downloadTask?.cancel()
|
downloadTask?.cancel()
|
||||||
exoPlayer.stop()
|
exoPlayer.stop()
|
||||||
speechRate = AppConfig.speechRatePlay
|
speechRate = AppConfig.speechRatePlay + 5
|
||||||
downloadAudio()
|
downloadAudio()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,16 +2,7 @@ package io.legado.app.ui.association
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import io.legado.app.R
|
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.data.appDb
|
|
||||||
import io.legado.app.data.entities.TxtTocRule
|
|
||||||
import io.legado.app.exception.NoStackTraceException
|
|
||||||
import io.legado.app.help.config.ThemeConfig
|
|
||||||
import io.legado.app.utils.GSON
|
|
||||||
import io.legado.app.utils.fromJsonArray
|
|
||||||
import io.legado.app.utils.fromJsonObject
|
|
||||||
import io.legado.app.utils.isJsonArray
|
|
||||||
|
|
||||||
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
|
abstract class BaseAssociationViewModel(application: Application) : BaseViewModel(application) {
|
||||||
|
|
||||||
@@ -37,47 +28,4 @@ abstract class BaseAssociationViewModel(application: Application) : BaseViewMode
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun importTextTocRule(json: String, finally: (title: String, msg: String) -> Unit) {
|
|
||||||
execute {
|
|
||||||
if (json.isJsonArray()) {
|
|
||||||
GSON.fromJsonArray<TxtTocRule>(json).getOrThrow()?.let {
|
|
||||||
appDb.txtTocRuleDao.insert(*it.toTypedArray())
|
|
||||||
} ?: throw NoStackTraceException("格式不对")
|
|
||||||
} else {
|
|
||||||
GSON.fromJsonObject<TxtTocRule>(json).getOrThrow()?.let {
|
|
||||||
appDb.txtTocRuleDao.insert(it)
|
|
||||||
} ?: throw NoStackTraceException("格式不对")
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
finally.invoke(context.getString(R.string.success), "导入Txt规则成功")
|
|
||||||
}.onError {
|
|
||||||
finally.invoke(
|
|
||||||
context.getString(R.string.error),
|
|
||||||
it.localizedMessage ?: context.getString(R.string.unknown_error)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun importTheme(json: String, finally: (title: String, msg: String) -> Unit) {
|
|
||||||
execute {
|
|
||||||
if (json.isJsonArray()) {
|
|
||||||
GSON.fromJsonArray<ThemeConfig.Config>(json).getOrThrow()?.forEach {
|
|
||||||
ThemeConfig.addConfig(it)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
GSON.fromJsonObject<ThemeConfig.Config>(json).getOrThrow()?.let {
|
|
||||||
ThemeConfig.addConfig(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}.onSuccess {
|
|
||||||
finally.invoke(context.getString(R.string.success), "导入主题成功")
|
|
||||||
}.onError {
|
|
||||||
finally.invoke(
|
|
||||||
context.getString(R.string.error),
|
|
||||||
it.localizedMessage ?: context.getString(R.string.unknown_error)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -61,21 +61,7 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
|
|
||||||
fun importSource(text: String) {
|
fun importSource(text: String) {
|
||||||
execute {
|
execute {
|
||||||
val mText = text.trim()
|
importSourceAwait(text.trim())
|
||||||
when {
|
|
||||||
mText.isJsonObject() -> {
|
|
||||||
HttpTTS.fromJson(mText).getOrThrow().let {
|
|
||||||
allSources.add(it)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
mText.isJsonArray() -> HttpTTS.fromJsonArray(mText).getOrThrow().let { items ->
|
|
||||||
allSources.addAll(items)
|
|
||||||
}
|
|
||||||
mText.isAbsUrl() -> {
|
|
||||||
importSourceUrl(mText)
|
|
||||||
}
|
|
||||||
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
|
||||||
}
|
|
||||||
}.onError {
|
}.onError {
|
||||||
it.printOnDebug()
|
it.printOnDebug()
|
||||||
errorLiveData.postValue(it.localizedMessage ?: "")
|
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||||
@@ -84,11 +70,28 @@ class ImportHttpTtsViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun importSourceAwait(text: String) {
|
||||||
|
when {
|
||||||
|
text.isJsonObject() -> {
|
||||||
|
HttpTTS.fromJson(text).getOrThrow().let {
|
||||||
|
allSources.add(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
text.isJsonArray() -> HttpTTS.fromJsonArray(text).getOrThrow().let { items ->
|
||||||
|
allSources.addAll(items)
|
||||||
|
}
|
||||||
|
text.isAbsUrl() -> {
|
||||||
|
importSourceUrl(text)
|
||||||
|
}
|
||||||
|
else -> throw NoStackTraceException(context.getString(R.string.wrong_format))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private suspend fun importSourceUrl(url: String) {
|
private suspend fun importSourceUrl(url: String) {
|
||||||
okHttpClient.newCallResponseBody {
|
okHttpClient.newCallResponseBody {
|
||||||
url(url)
|
url(url)
|
||||||
}.text().let {
|
}.text().let {
|
||||||
allSources.addAll(HttpTTS.fromJsonArray(it).getOrThrow())
|
importSourceAwait(it)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,132 @@
|
|||||||
|
package io.legado.app.ui.association
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.DialogInterface
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.view.View
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.fragment.app.viewModels
|
||||||
|
import androidx.recyclerview.widget.LinearLayoutManager
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseDialogFragment
|
||||||
|
import io.legado.app.base.adapter.ItemViewHolder
|
||||||
|
import io.legado.app.base.adapter.RecyclerAdapter
|
||||||
|
import io.legado.app.databinding.DialogRecyclerViewBinding
|
||||||
|
import io.legado.app.databinding.ItemBookFileImportBinding
|
||||||
|
import io.legado.app.help.config.AppConfig
|
||||||
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.lib.theme.primaryColor
|
||||||
|
import io.legado.app.ui.widget.dialog.WaitDialog
|
||||||
|
import io.legado.app.utils.*
|
||||||
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 导入在线书籍文件弹出窗口
|
||||||
|
*/
|
||||||
|
class ImportOnLineBookFileDialog() : BaseDialogFragment(R.layout.dialog_recycler_view) {
|
||||||
|
|
||||||
|
|
||||||
|
private val binding by viewBinding(DialogRecyclerViewBinding::bind)
|
||||||
|
private val viewModel by viewModels<ImportOnLineBookFileViewModel>()
|
||||||
|
private val adapter by lazy { BookFileAdapter(requireContext()) }
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFragmentCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
|
val bookUrl = arguments?.getString("bookUrl")
|
||||||
|
viewModel.initData(bookUrl)
|
||||||
|
binding.toolBar.setBackgroundColor(primaryColor)
|
||||||
|
binding.toolBar.setTitle(R.string.download_and_import_file)
|
||||||
|
binding.rotateLoading.show()
|
||||||
|
binding.recyclerView.layoutManager = LinearLayoutManager(requireContext())
|
||||||
|
binding.recyclerView.adapter = adapter
|
||||||
|
viewModel.errorLiveData.observe(this) {
|
||||||
|
binding.rotateLoading.hide()
|
||||||
|
binding.tvMsg.apply {
|
||||||
|
text = it
|
||||||
|
visible()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.successLiveData.observe(this) {
|
||||||
|
binding.rotateLoading.hide()
|
||||||
|
if (it > 0) {
|
||||||
|
adapter.setItems(viewModel.allBookFiles)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
viewModel.savedFileUriData.observe(this) {
|
||||||
|
requireContext().openFileUri(it, "*/*")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun importFileAndUpdate(url: String, fileName: String) {
|
||||||
|
val waitDialog = WaitDialog(requireContext())
|
||||||
|
waitDialog.show()
|
||||||
|
viewModel.importOnLineBookFile(url, fileName) {
|
||||||
|
waitDialog.dismiss()
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun downloadFile(url: String, fileName: String) {
|
||||||
|
val waitDialog = WaitDialog(requireContext())
|
||||||
|
waitDialog.show()
|
||||||
|
viewModel.downloadUrl(url, fileName) {
|
||||||
|
waitDialog.dismiss()
|
||||||
|
dismissAllowingStateLoss()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inner class BookFileAdapter(context: Context) :
|
||||||
|
RecyclerAdapter<Triple<String, String, Boolean>
|
||||||
|
, ItemBookFileImportBinding>(context) {
|
||||||
|
|
||||||
|
override fun getViewBinding(parent: ViewGroup): ItemBookFileImportBinding {
|
||||||
|
return ItemBookFileImportBinding.inflate(inflater, parent, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun convert(
|
||||||
|
holder: ItemViewHolder,
|
||||||
|
binding: ItemBookFileImportBinding,
|
||||||
|
item: Triple<String, String, Boolean>,
|
||||||
|
payloads: MutableList<Any>
|
||||||
|
) {
|
||||||
|
binding.apply {
|
||||||
|
cbFileName.text = item.second
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun registerListener(
|
||||||
|
holder: ItemViewHolder,
|
||||||
|
binding: ItemBookFileImportBinding
|
||||||
|
) {
|
||||||
|
binding.apply {
|
||||||
|
cbFileName.setOnClickListener {
|
||||||
|
val selectFile = viewModel.allBookFiles[holder.layoutPosition]
|
||||||
|
if (selectFile.third) {
|
||||||
|
importFileAndUpdate(selectFile.first, selectFile.second)
|
||||||
|
} else {
|
||||||
|
alert(
|
||||||
|
title = getString(R.string.draw),
|
||||||
|
message = getString(R.string.file_not_supported, selectFile.second)
|
||||||
|
) {
|
||||||
|
okButton {
|
||||||
|
importFileAndUpdate(selectFile.first, selectFile.second)
|
||||||
|
}
|
||||||
|
neutralButton(R.string.open_fun) {
|
||||||
|
downloadFile(selectFile.first, selectFile.second)
|
||||||
|
}
|
||||||
|
cancelButton()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package io.legado.app.ui.association
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
import android.net.Uri
|
||||||
|
import androidx.lifecycle.MutableLiveData
|
||||||
|
import io.legado.app.R
|
||||||
|
import io.legado.app.base.BaseViewModel
|
||||||
|
import io.legado.app.constant.AppPattern
|
||||||
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
|
import io.legado.app.data.appDb
|
||||||
|
import io.legado.app.data.entities.Book
|
||||||
|
import io.legado.app.data.entities.BookSource
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||||
|
import io.legado.app.model.localBook.LocalBook
|
||||||
|
import io.legado.app.utils.*
|
||||||
|
|
||||||
|
class ImportOnLineBookFileViewModel(app: Application) : BaseViewModel(app) {
|
||||||
|
|
||||||
|
val allBookFiles = arrayListOf<Triple<String, String, Boolean>>()
|
||||||
|
val errorLiveData = MutableLiveData<String>()
|
||||||
|
val successLiveData = MutableLiveData<Int>()
|
||||||
|
val savedFileUriData = MutableLiveData<Uri>()
|
||||||
|
var bookSource: BookSource? = null
|
||||||
|
|
||||||
|
fun initData(bookUrl: String?) {
|
||||||
|
execute {
|
||||||
|
bookUrl ?: throw NoStackTraceException("书籍详情页链接为空")
|
||||||
|
val book = appDb.searchBookDao.getSearchBook(bookUrl)?.toBook()
|
||||||
|
?: throw NoStackTraceException("book is null")
|
||||||
|
bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
||||||
|
?: throw NoStackTraceException("bookSource is null")
|
||||||
|
val ruleDownloadUrls = bookSource?.getBookInfoRule()?.downloadUrls
|
||||||
|
val content = AnalyzeUrl(bookUrl, source = bookSource).getStrResponse().body
|
||||||
|
val analyzeRule = AnalyzeRule(book, bookSource)
|
||||||
|
analyzeRule.setContent(content).setBaseUrl(bookUrl)
|
||||||
|
val fileName = "${book.name} 作者:${book.author}"
|
||||||
|
analyzeRule.getStringList(ruleDownloadUrls, isUrl = true)?.let {
|
||||||
|
it.forEach { url ->
|
||||||
|
val mFileName = "${fileName}.${LocalBook.parseFileSuffix(url)}"
|
||||||
|
val isSupportedFile = AppPattern.bookFileRegex.matches(mFileName)
|
||||||
|
allBookFiles.add(Triple(url, mFileName, isSupportedFile))
|
||||||
|
}
|
||||||
|
} ?: throw NoStackTraceException("下载链接规则解析为空")
|
||||||
|
}.onSuccess {
|
||||||
|
successLiveData.postValue(allBookFiles.size)
|
||||||
|
}.onError {
|
||||||
|
errorLiveData.postValue(it.localizedMessage ?: "")
|
||||||
|
context.toastOnUi("获取书籍下载链接失败\n${it.localizedMessage}")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
fun downloadUrl(url: String, fileName: String, success: () -> Unit) {
|
||||||
|
execute {
|
||||||
|
LocalBook.saveBookFile(url, fileName, bookSource).let {
|
||||||
|
savedFileUriData.postValue(it)
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
success.invoke()
|
||||||
|
}.onError {
|
||||||
|
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun importOnLineBookFile(url: String, fileName: String, success: () -> Unit) {
|
||||||
|
execute {
|
||||||
|
LocalBook.importFileOnLine(url, fileName, bookSource).let {
|
||||||
|
postEvent(EventBus.BOOK_URL_CHANGED, it.bookUrl)
|
||||||
|
}
|
||||||
|
}.onSuccess {
|
||||||
|
success.invoke()
|
||||||
|
}.onError {
|
||||||
|
context.toastOnUi("下载书籍文件失败\n${it.localizedMessage}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -88,14 +88,14 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
|
|||||||
adapter.notifyDataSetChanged()
|
adapter.notifyDataSetChanged()
|
||||||
upSelectText()
|
upSelectText()
|
||||||
}
|
}
|
||||||
viewModel.errorLiveData.observe(this, {
|
viewModel.errorLiveData.observe(this) {
|
||||||
binding.rotateLoading.hide()
|
binding.rotateLoading.hide()
|
||||||
binding.tvMsg.apply {
|
binding.tvMsg.apply {
|
||||||
text = it
|
text = it
|
||||||
visible()
|
visible()
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
viewModel.successLiveData.observe(this, {
|
viewModel.successLiveData.observe(this) {
|
||||||
binding.rotateLoading.hide()
|
binding.rotateLoading.hide()
|
||||||
if (it > 0) {
|
if (it > 0) {
|
||||||
adapter.setItems(viewModel.allRules)
|
adapter.setItems(viewModel.allRules)
|
||||||
@@ -106,7 +106,7 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
|
|||||||
visible()
|
visible()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
}
|
||||||
val source = arguments?.getString("source")
|
val source = arguments?.getString("source")
|
||||||
if (source.isNullOrEmpty()) {
|
if (source.isNullOrEmpty()) {
|
||||||
dismiss()
|
dismiss()
|
||||||
|
|||||||
@@ -6,12 +6,15 @@ import io.legado.app.base.BaseViewModel
|
|||||||
import io.legado.app.constant.AppPattern
|
import io.legado.app.constant.AppPattern
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.ReplaceRule
|
import io.legado.app.data.entities.ReplaceRule
|
||||||
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.ReplaceAnalyzer
|
import io.legado.app.help.ReplaceAnalyzer
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.http.newCallResponseBody
|
import io.legado.app.help.http.newCallResponseBody
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.help.http.text
|
import io.legado.app.help.http.text
|
||||||
import io.legado.app.utils.isAbsUrl
|
import io.legado.app.utils.isAbsUrl
|
||||||
|
import io.legado.app.utils.isJsonArray
|
||||||
|
import io.legado.app.utils.isJsonObject
|
||||||
import io.legado.app.utils.splitNotBlank
|
import io.legado.app.utils.splitNotBlank
|
||||||
|
|
||||||
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||||
@@ -83,17 +86,7 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
|
|
||||||
fun import(text: String) {
|
fun import(text: String) {
|
||||||
execute {
|
execute {
|
||||||
if (text.isAbsUrl()) {
|
importAwait(text.trim())
|
||||||
okHttpClient.newCallResponseBody {
|
|
||||||
url(text)
|
|
||||||
}.text("utf-8").let {
|
|
||||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(it)
|
|
||||||
allRules.addAll(rules)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(text)
|
|
||||||
allRules.addAll(rules)
|
|
||||||
}
|
|
||||||
}.onError {
|
}.onError {
|
||||||
errorLiveData.postValue(it.localizedMessage ?: "ERROR")
|
errorLiveData.postValue(it.localizedMessage ?: "ERROR")
|
||||||
}.onSuccess {
|
}.onSuccess {
|
||||||
@@ -101,6 +94,29 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private suspend fun importAwait(text: String) {
|
||||||
|
when {
|
||||||
|
text.isAbsUrl() -> importUrl(text)
|
||||||
|
text.isJsonArray() -> {
|
||||||
|
val rules = ReplaceAnalyzer.jsonToReplaceRules(text).getOrThrow()
|
||||||
|
allRules.addAll(rules)
|
||||||
|
}
|
||||||
|
text.isJsonObject() -> {
|
||||||
|
val rule = ReplaceAnalyzer.jsonToReplaceRule(text).getOrThrow()
|
||||||
|
allRules.add(rule)
|
||||||
|
}
|
||||||
|
else -> throw NoStackTraceException("格式不对")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun importUrl(url: String) {
|
||||||
|
okHttpClient.newCallResponseBody {
|
||||||
|
url(url)
|
||||||
|
}.text("utf-8").let {
|
||||||
|
importAwait(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun comparisonSource() {
|
private fun comparisonSource() {
|
||||||
execute {
|
execute {
|
||||||
allRules.forEach {
|
allRules.forEach {
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import androidx.activity.viewModels
|
|||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.VMBaseActivity
|
import io.legado.app.base.VMBaseActivity
|
||||||
import io.legado.app.constant.BookType
|
import io.legado.app.constant.BookType
|
||||||
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.Theme
|
import io.legado.app.constant.Theme
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
@@ -26,6 +27,7 @@ import io.legado.app.lib.theme.bottomBackground
|
|||||||
import io.legado.app.lib.theme.getPrimaryTextColor
|
import io.legado.app.lib.theme.getPrimaryTextColor
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.ui.about.AppLogDialog
|
import io.legado.app.ui.about.AppLogDialog
|
||||||
|
import io.legado.app.ui.association.ImportOnLineBookFileDialog
|
||||||
import io.legado.app.ui.book.audio.AudioPlayActivity
|
import io.legado.app.ui.book.audio.AudioPlayActivity
|
||||||
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
import io.legado.app.ui.book.changecover.ChangeCoverDialog
|
||||||
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
import io.legado.app.ui.book.changesource.ChangeBookSourceDialog
|
||||||
@@ -245,7 +247,7 @@ class BookInfoActivity :
|
|||||||
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
|
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.loading))
|
||||||
}
|
}
|
||||||
chapterList.isNullOrEmpty() -> {
|
chapterList.isNullOrEmpty() -> {
|
||||||
binding.tvToc.text = getString(R.string.toc_s, getString(R.string.error_load_toc))
|
binding.tvToc.text = if (viewModel.isImportBookOnLine) getString(R.string.click_read_button_load) else getString(R.string.toc_s, getString(R.string.error_load_toc))
|
||||||
}
|
}
|
||||||
else -> {
|
else -> {
|
||||||
viewModel.bookData.value?.let {
|
viewModel.bookData.value?.let {
|
||||||
@@ -293,8 +295,14 @@ class BookInfoActivity :
|
|||||||
true
|
true
|
||||||
}
|
}
|
||||||
tvRead.setOnClickListener {
|
tvRead.setOnClickListener {
|
||||||
viewModel.bookData.value?.let {
|
viewModel.bookData.value?.let { book ->
|
||||||
readBook(it)
|
if (viewModel.isImportBookOnLine) {
|
||||||
|
showDialogFragment<ImportOnLineBookFileDialog> {
|
||||||
|
putString("bookUrl", book.bookUrl)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
readBook(book)
|
||||||
|
}
|
||||||
} ?: toastOnUi("Book is null")
|
} ?: toastOnUi("Book is null")
|
||||||
}
|
}
|
||||||
tvShelf.setOnClickListener {
|
tvShelf.setOnClickListener {
|
||||||
@@ -497,4 +505,9 @@ class BookInfoActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun observeLiveBus() {
|
||||||
|
observeEvent<String>(EventBus.BOOK_URL_CHANGED) {
|
||||||
|
viewModel.changeToLocalBook(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -2,11 +2,13 @@ package io.legado.app.ui.book.info
|
|||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
|
import android.net.Uri
|
||||||
import androidx.lifecycle.MutableLiveData
|
import androidx.lifecycle.MutableLiveData
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseViewModel
|
import io.legado.app.base.BaseViewModel
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
|
import io.legado.app.constant.BookType
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.Book
|
import io.legado.app.data.entities.Book
|
||||||
@@ -30,6 +32,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
var inBookshelf = false
|
var inBookshelf = false
|
||||||
var bookSource: BookSource? = null
|
var bookSource: BookSource? = null
|
||||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||||
|
var isImportBookOnLine = false
|
||||||
|
|
||||||
fun initData(intent: Intent) {
|
fun initData(intent: Intent) {
|
||||||
execute {
|
execute {
|
||||||
@@ -73,8 +76,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
upCoverByRule(book)
|
upCoverByRule(book)
|
||||||
bookSource = if (book.isLocalBook()) null else
|
bookSource = if (book.isLocalBook()) null else
|
||||||
appDb.bookSourceDao.getBookSource(book.origin)
|
appDb.bookSourceDao.getBookSource(book.origin)
|
||||||
|
isImportBookOnLine = (bookSource?.bookSourceType ?: BookType.local) == BookType.file
|
||||||
if (book.tocUrl.isEmpty()) {
|
if (book.tocUrl.isEmpty()) {
|
||||||
loadBookInfo(book)
|
loadBookInfo(book)
|
||||||
|
} else if (isImportBookOnLine) {
|
||||||
|
chapterListData.postValue(emptyList())
|
||||||
} else {
|
} else {
|
||||||
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
||||||
if (chapterList.isNotEmpty()) {
|
if (chapterList.isNotEmpty()) {
|
||||||
@@ -113,6 +119,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
|
WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
|
||||||
.onSuccess(IO) {
|
.onSuccess(IO) {
|
||||||
bookData.postValue(book)
|
bookData.postValue(book)
|
||||||
|
if (isImportBookOnLine) {
|
||||||
|
appDb.searchBookDao.update(book.toSearchBook())
|
||||||
|
}
|
||||||
if (inBookshelf) {
|
if (inBookshelf) {
|
||||||
appDb.bookDao.update(book)
|
appDb.bookDao.update(book)
|
||||||
}
|
}
|
||||||
@@ -141,6 +150,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
appDb.bookChapterDao.insert(*it.toTypedArray())
|
appDb.bookChapterDao.insert(*it.toTypedArray())
|
||||||
chapterListData.postValue(it)
|
chapterListData.postValue(it)
|
||||||
}
|
}
|
||||||
|
} else if (isImportBookOnLine) {
|
||||||
|
chapterListData.postValue(emptyList())
|
||||||
} else {
|
} else {
|
||||||
bookSource?.let { bookSource ->
|
bookSource?.let { bookSource ->
|
||||||
WebBook.getChapterList(this, bookSource, book)
|
WebBook.getChapterList(this, bookSource, book)
|
||||||
@@ -282,4 +293,16 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun changeToLocalBook(bookUrl: String) {
|
||||||
|
appDb.bookDao.getBook(bookUrl)?.let { localBook ->
|
||||||
|
isImportBookOnLine = false
|
||||||
|
inBookshelf = true
|
||||||
|
LocalBook.mergeBook(localBook, bookData.value).let {
|
||||||
|
bookData.postValue(it)
|
||||||
|
loadChapter(it)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -229,8 +229,12 @@ abstract class BaseReadBookActivity :
|
|||||||
customView { alertBinding.root }
|
customView { alertBinding.root }
|
||||||
yesButton {
|
yesButton {
|
||||||
alertBinding.run {
|
alertBinding.run {
|
||||||
val start = editStart.text?.toString()?.toInt() ?: 0
|
val start = editStart.text!!.toString().let {
|
||||||
val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum
|
if (it.isEmpty()) 0 else it.toInt()
|
||||||
|
}
|
||||||
|
val end = editEnd.text!!.toString().let {
|
||||||
|
if (it.isEmpty()) book.totalChapterNum else it.toInt()
|
||||||
|
}
|
||||||
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
|
CacheBook.start(this@BaseReadBookActivity, book, start - 1, end - 1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,41 +61,33 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun initBook(book: Book) {
|
private fun initBook(book: Book) {
|
||||||
if (ReadBook.book?.bookUrl != book.bookUrl) {
|
val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
|
||||||
ReadBook.resetData(book)
|
if (isSameBook) ReadBook.upData(book) else ReadBook.resetData(book)
|
||||||
isInitFinish = true
|
isInitFinish = true
|
||||||
if (ReadBook.chapterSize == 0) {
|
if (ReadBook.chapterSize == 0) {
|
||||||
if (book.tocUrl.isEmpty()) {
|
if (book.tocUrl.isEmpty()) {
|
||||||
loadBookInfo(book)
|
loadBookInfo(book)
|
||||||
} else {
|
} else {
|
||||||
loadChapterList(book)
|
loadChapterList(book)
|
||||||
}
|
}
|
||||||
|
} else if (book.isLocalBook()
|
||||||
|
&& LocalBook.getLastModified(book).getOrDefault(0L) > book.latestChapterTime
|
||||||
|
) {
|
||||||
|
loadChapterList(book)
|
||||||
|
} else if (isSameBook) {
|
||||||
|
if (ReadBook.curTextChapter != null) {
|
||||||
|
ReadBook.callBack?.upContent(resetPageOffset = false)
|
||||||
} else {
|
} else {
|
||||||
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
|
||||||
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
|
||||||
}
|
|
||||||
ReadBook.loadContent(resetPageOffset = true)
|
ReadBook.loadContent(resetPageOffset = true)
|
||||||
}
|
}
|
||||||
syncBookProgress(book)
|
|
||||||
} else {
|
} else {
|
||||||
ReadBook.upData(book)
|
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
||||||
isInitFinish = true
|
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
||||||
if (ReadBook.chapterSize == 0) {
|
|
||||||
if (book.tocUrl.isEmpty()) {
|
|
||||||
loadBookInfo(book)
|
|
||||||
} else {
|
|
||||||
loadChapterList(book)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (ReadBook.curTextChapter != null) {
|
|
||||||
ReadBook.callBack?.upContent(resetPageOffset = false)
|
|
||||||
} else {
|
|
||||||
ReadBook.loadContent(resetPageOffset = true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!BaseReadAloudService.isRun) {
|
|
||||||
syncBookProgress(book)
|
|
||||||
}
|
}
|
||||||
|
ReadBook.loadContent(resetPageOffset = isSameBook)
|
||||||
|
}
|
||||||
|
if (!isSameBook || !BaseReadAloudService.isRun) {
|
||||||
|
syncBookProgress(book)
|
||||||
}
|
}
|
||||||
if (!book.isLocalBook() && ReadBook.bookSource == null) {
|
if (!book.isLocalBook() && ReadBook.bookSource == null) {
|
||||||
autoChangeSource(book.name, book.author)
|
autoChangeSource(book.name, book.author)
|
||||||
|
|||||||
@@ -9,10 +9,10 @@ import android.widget.LinearLayout
|
|||||||
import androidx.fragment.app.DialogFragment
|
import androidx.fragment.app.DialogFragment
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.help.config.ReadBookConfig
|
import io.legado.app.help.config.ReadBookConfig
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.bottomBackground
|
import io.legado.app.lib.theme.bottomBackground
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.ReadBook
|
import io.legado.app.model.ReadBook
|
||||||
@@ -67,7 +67,7 @@ class MoreConfigDialog : DialogFragment() {
|
|||||||
(activity as ReadBookActivity).bottomDialog--
|
(activity as ReadBookActivity).bottomDialog--
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReadPreferenceFragment : BasePreferenceFragment(),
|
class ReadPreferenceFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
@SuppressLint("RestrictedApi")
|
@SuppressLint("RestrictedApi")
|
||||||
|
|||||||
@@ -10,12 +10,12 @@ import androidx.fragment.app.DialogFragment
|
|||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.help.IntentHelp
|
import io.legado.app.help.IntentHelp
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
import io.legado.app.lib.dialogs.SelectItem
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.backgroundColor
|
import io.legado.app.lib.theme.backgroundColor
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.ReadAloud
|
import io.legado.app.model.ReadAloud
|
||||||
@@ -54,7 +54,7 @@ class ReadAloudConfigDialog : DialogFragment() {
|
|||||||
.commit()
|
.commit()
|
||||||
}
|
}
|
||||||
|
|
||||||
class ReadAloudPreferenceFragment : BasePreferenceFragment(),
|
class ReadAloudPreferenceFragment : PreferenceFragment(),
|
||||||
SpeakEngineDialog.CallBack,
|
SpeakEngineDialog.CallBack,
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import io.legado.app.databinding.ActivityBookSourceEditBinding
|
|||||||
import io.legado.app.help.config.LocalConfig
|
import io.legado.app.help.config.LocalConfig
|
||||||
import io.legado.app.lib.dialogs.SelectItem
|
import io.legado.app.lib.dialogs.SelectItem
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
import io.legado.app.lib.theme.backgroundColor
|
import io.legado.app.lib.theme.backgroundColor
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
|
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
|
||||||
@@ -47,7 +48,7 @@ class BookSourceEditActivity :
|
|||||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||||
it ?: return@registerForActivityResult
|
it ?: return@registerForActivityResult
|
||||||
viewModel.importSource(it) { source ->
|
viewModel.importSource(it) { source ->
|
||||||
upRecyclerView(source)
|
upSourceView(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||||
@@ -68,7 +69,7 @@ class BookSourceEditActivity :
|
|||||||
softKeyboardTool.attachToWindow(window)
|
softKeyboardTool.attachToWindow(window)
|
||||||
initView()
|
initView()
|
||||||
viewModel.initData(intent) {
|
viewModel.initData(intent) {
|
||||||
upRecyclerView()
|
upSourceView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,9 +110,10 @@ class BookSourceEditActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
R.id.menu_clear_cookie -> viewModel.clearCookie(getSource().bookSourceUrl)
|
||||||
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
||||||
R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
|
R.id.menu_copy_source -> sendToClip(GSON.toJson(getSource()))
|
||||||
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
|
R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
|
||||||
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
||||||
R.id.menu_share_str -> share(GSON.toJson(getSource()))
|
R.id.menu_share_str -> share(GSON.toJson(getSource()))
|
||||||
R.id.menu_share_qr -> shareWithQr(
|
R.id.menu_share_qr -> shareWithQr(
|
||||||
@@ -139,6 +141,7 @@ class BookSourceEditActivity :
|
|||||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
binding.tabLayout.setBackgroundColor(backgroundColor)
|
binding.tabLayout.setBackgroundColor(backgroundColor)
|
||||||
|
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||||
|
|
||||||
@@ -186,12 +189,14 @@ class BookSourceEditActivity :
|
|||||||
binding.recyclerView.scrollToPosition(0)
|
binding.recyclerView.scrollToPosition(0)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upRecyclerView(source: BookSource? = viewModel.bookSource) {
|
private fun upSourceView(source: BookSource? = viewModel.bookSource) {
|
||||||
source?.let {
|
source?.let {
|
||||||
binding.cbIsEnable.isChecked = it.enabled
|
binding.cbIsEnable.isChecked = it.enabled
|
||||||
binding.cbIsEnableFind.isChecked = it.enabledExplore
|
binding.cbIsEnableFind.isChecked = it.enabledExplore
|
||||||
|
binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
|
||||||
binding.spType.setSelection(
|
binding.spType.setSelection(
|
||||||
when (it.bookSourceType) {
|
when (it.bookSourceType) {
|
||||||
|
BookType.file -> 3
|
||||||
BookType.image -> 2
|
BookType.image -> 2
|
||||||
BookType.audio -> 1
|
BookType.audio -> 1
|
||||||
else -> 0
|
else -> 0
|
||||||
@@ -261,6 +266,7 @@ class BookSourceEditActivity :
|
|||||||
add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
|
add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
|
||||||
add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
|
add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
|
||||||
add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
|
add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
|
||||||
|
add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
|
||||||
}
|
}
|
||||||
//目录页
|
//目录页
|
||||||
val tr = source?.getTocRule()
|
val tr = source?.getTocRule()
|
||||||
@@ -295,7 +301,9 @@ class BookSourceEditActivity :
|
|||||||
val source = viewModel.bookSource?.copy() ?: BookSource()
|
val source = viewModel.bookSource?.copy() ?: BookSource()
|
||||||
source.enabled = binding.cbIsEnable.isChecked
|
source.enabled = binding.cbIsEnable.isChecked
|
||||||
source.enabledExplore = binding.cbIsEnableFind.isChecked
|
source.enabledExplore = binding.cbIsEnableFind.isChecked
|
||||||
|
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||||
source.bookSourceType = when (binding.spType.selectedItemPosition) {
|
source.bookSourceType = when (binding.spType.selectedItemPosition) {
|
||||||
|
3 -> BookType.file
|
||||||
2 -> BookType.image
|
2 -> BookType.image
|
||||||
1 -> BookType.audio
|
1 -> BookType.audio
|
||||||
else -> BookType.default
|
else -> BookType.default
|
||||||
@@ -389,6 +397,7 @@ class BookSourceEditActivity :
|
|||||||
"tocUrl" -> bookInfoRule.tocUrl =
|
"tocUrl" -> bookInfoRule.tocUrl =
|
||||||
viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
|
viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
|
||||||
"canReName" -> bookInfoRule.canReName = it.value
|
"canReName" -> bookInfoRule.canReName = it.value
|
||||||
|
"downloadUrls" -> bookInfoRule.downloadUrls = viewModel.ruleComplete(it.value, bookInfoRule.init)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
tocEntities.forEach {
|
tocEntities.forEach {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import io.legado.app.data.appDb
|
|||||||
import io.legado.app.data.entities.BookSource
|
import io.legado.app.data.entities.BookSource
|
||||||
import io.legado.app.exception.NoStackTraceException
|
import io.legado.app.exception.NoStackTraceException
|
||||||
import io.legado.app.help.RuleComplete
|
import io.legado.app.help.RuleComplete
|
||||||
|
import io.legado.app.help.http.CookieStore
|
||||||
import io.legado.app.help.http.newCallStrResponse
|
import io.legado.app.help.http.newCallStrResponse
|
||||||
import io.legado.app.help.http.okHttpClient
|
import io.legado.app.help.http.okHttpClient
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
@@ -95,6 +96,12 @@ class BookSourceEditViewModel(application: Application) : BaseViewModel(applicat
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCookie(url: String) {
|
||||||
|
execute {
|
||||||
|
CookieStore.removeCookie(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
||||||
if (autoComplete) {
|
if (autoComplete) {
|
||||||
return RuleComplete.autoComplete(rule, preRule, type)
|
return RuleComplete.autoComplete(rule, preRule, type)
|
||||||
|
|||||||
@@ -19,8 +19,10 @@ import io.legado.app.utils.gone
|
|||||||
import io.legado.app.utils.longToastOnUi
|
import io.legado.app.utils.longToastOnUi
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
import kotlinx.coroutines.isActive
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.runBlocking
|
import kotlinx.coroutines.async
|
||||||
|
import kotlinx.coroutines.ensureActive
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import java.util.concurrent.ConcurrentHashMap
|
import java.util.concurrent.ConcurrentHashMap
|
||||||
|
|
||||||
class ChapterListAdapter(context: Context, val callback: Callback) :
|
class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||||
@@ -54,17 +56,10 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private val replaceRules
|
|
||||||
get() = callback.book?.let {
|
|
||||||
ContentProcessor.get(it.name, it.origin).getTitleReplaceRules()
|
|
||||||
}
|
|
||||||
private val useReplace
|
|
||||||
get() = AppConfig.tocUiUseReplace && callback.book?.getUseReplaceRule() == true
|
|
||||||
private var upDisplayTileJob: Coroutine<*>? = null
|
private var upDisplayTileJob: Coroutine<*>? = null
|
||||||
|
|
||||||
override fun onCurrentListChanged() {
|
override fun onCurrentListChanged() {
|
||||||
super.onCurrentListChanged()
|
super.onCurrentListChanged()
|
||||||
upDisplayTitle()
|
|
||||||
callback.onListChanged()
|
callback.onListChanged()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -73,38 +68,52 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|||||||
displayTitleMap.clear()
|
displayTitleMap.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun upDisplayTitle() {
|
fun upDisplayTitles(startIndex: Int) {
|
||||||
upDisplayTileJob?.cancel()
|
upDisplayTileJob?.cancel()
|
||||||
upDisplayTileJob = Coroutine.async(callback.scope) {
|
upDisplayTileJob = Coroutine.async(callback.scope) {
|
||||||
val replaceRules = replaceRules
|
val book = callback.book ?: return@async
|
||||||
val useReplace = useReplace
|
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||||
getItems().forEach {
|
val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
|
||||||
if (!isActive) {
|
val items = getItems()
|
||||||
return@async
|
async {
|
||||||
|
for (i in startIndex until items.size) {
|
||||||
|
val item = items[i]
|
||||||
|
if (displayTitleMap[item.title] == null) {
|
||||||
|
ensureActive()
|
||||||
|
val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
|
||||||
|
ensureActive()
|
||||||
|
displayTitleMap[item.title] = displayTitle
|
||||||
|
withContext(Main) {
|
||||||
|
notifyItemChanged(i, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (displayTitleMap[it.title] == null) {
|
}.start()
|
||||||
displayTitleMap[it.title] = it.getDisplayTitle(replaceRules, useReplace)
|
async {
|
||||||
|
for (i in startIndex downTo 0) {
|
||||||
|
val item = items[i]
|
||||||
|
if (displayTitleMap[item.title] == null) {
|
||||||
|
ensureActive()
|
||||||
|
val displayTitle = item.getDisplayTitle(replaceRules, useReplace)
|
||||||
|
ensureActive()
|
||||||
|
displayTitleMap[item.title] = displayTitle
|
||||||
|
withContext(Main) {
|
||||||
|
notifyItemChanged(i, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}.start()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun getDisplayTitle(chapter: BookChapter): String {
|
||||||
|
return displayTitleMap[chapter.title] ?: chapter.title
|
||||||
|
}
|
||||||
|
|
||||||
override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
|
override fun getViewBinding(parent: ViewGroup): ItemChapterListBinding {
|
||||||
return ItemChapterListBinding.inflate(inflater, parent, false)
|
return ItemChapterListBinding.inflate(inflater, parent, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDisplayTile(chapter: BookChapter): String {
|
|
||||||
var displayTitle = displayTitleMap[chapter.title]
|
|
||||||
if (displayTitle != null) {
|
|
||||||
return displayTitle
|
|
||||||
}
|
|
||||||
displayTitle = runBlocking {
|
|
||||||
chapter.getDisplayTitle(replaceRules, useReplace)
|
|
||||||
}
|
|
||||||
displayTitleMap[chapter.title] = displayTitle
|
|
||||||
return displayTitle
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun convert(
|
override fun convert(
|
||||||
holder: ItemViewHolder,
|
holder: ItemViewHolder,
|
||||||
binding: ItemChapterListBinding,
|
binding: ItemChapterListBinding,
|
||||||
@@ -120,7 +129,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|||||||
} else {
|
} else {
|
||||||
tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText))
|
tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||||
}
|
}
|
||||||
tvChapterName.text = getDisplayTile(item)
|
tvChapterName.text = getDisplayTitle(item)
|
||||||
if (item.isVolume) {
|
if (item.isVolume) {
|
||||||
//卷名,如第一卷 突出显示
|
//卷名,如第一卷 突出显示
|
||||||
tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
|
tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
|
||||||
@@ -138,6 +147,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|||||||
}
|
}
|
||||||
upHasCache(binding, isDur, cached)
|
upHasCache(binding, isDur, cached)
|
||||||
} else {
|
} else {
|
||||||
|
tvChapterName.text = getDisplayTitle(item)
|
||||||
upHasCache(binding, isDur, cached)
|
upHasCache(binding, isDur, cached)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -150,8 +160,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
holder.itemView.setOnLongClickListener {
|
holder.itemView.setOnLongClickListener {
|
||||||
getItem(holder.layoutPosition)?.let {
|
getItem(holder.layoutPosition)?.let { item ->
|
||||||
context.longToastOnUi(getDisplayTile(it))
|
context.longToastOnUi(getDisplayTitle(item))
|
||||||
}
|
}
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -97,7 +97,15 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
|||||||
viewModel.bookData.value?.bookUrl?.let { bookUrl ->
|
viewModel.bookData.value?.bookUrl?.let { bookUrl ->
|
||||||
if (chapter.bookUrl == bookUrl) {
|
if (chapter.bookUrl == bookUrl) {
|
||||||
adapter.cacheFileNames.add(chapter.getFileName())
|
adapter.cacheFileNames.add(chapter.getFileName())
|
||||||
adapter.notifyItemChanged(chapter.index, true)
|
if (viewModel.searchKey.isNullOrEmpty()) {
|
||||||
|
adapter.notifyItemChanged(chapter.index, true)
|
||||||
|
} else {
|
||||||
|
adapter.getItems().forEachIndexed { index, bookChapter ->
|
||||||
|
if (bookChapter.index == chapter.index) {
|
||||||
|
adapter.notifyItemChanged(index, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -128,12 +136,13 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
mLayoutManager.scrollToPositionWithOffset(scrollPos, 0)
|
mLayoutManager.scrollToPositionWithOffset(scrollPos, 0)
|
||||||
|
adapter.upDisplayTitles(scrollPos)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun clearDisplayTitle() {
|
override fun clearDisplayTitle() {
|
||||||
adapter.clearDisplayTitle()
|
adapter.clearDisplayTitle()
|
||||||
adapter.upDisplayTitle()
|
adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition())
|
||||||
}
|
}
|
||||||
|
|
||||||
override val scope: CoroutineScope
|
override val scope: CoroutineScope
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>() {
|
|||||||
setOnSearchClickListener { tabLayout.gone() }
|
setOnSearchClickListener { tabLayout.gone() }
|
||||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
override fun onQueryTextSubmit(query: String): Boolean {
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
viewModel.searchKey = query
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onQueryTextChange(newText: String): Boolean {
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
|
viewModel.searchKey = newText
|
||||||
if (tabLayout.selectedTabPosition == 1) {
|
if (tabLayout.selectedTabPosition == 1) {
|
||||||
viewModel.startBookmarkSearch(newText)
|
viewModel.startBookmarkSearch(newText)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ class TocViewModel(application: Application) : BaseViewModel(application) {
|
|||||||
var bookData = MutableLiveData<Book>()
|
var bookData = MutableLiveData<Book>()
|
||||||
var chapterListCallBack: ChapterListCallBack? = null
|
var chapterListCallBack: ChapterListCallBack? = null
|
||||||
var bookMarkCallBack: BookmarkCallBack? = null
|
var bookMarkCallBack: BookmarkCallBack? = null
|
||||||
|
var searchKey: String? = null
|
||||||
|
|
||||||
fun initBook(bookUrl: String) {
|
fun initBook(bookUrl: String) {
|
||||||
this.bookUrl = bookUrl
|
this.bookUrl = bookUrl
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.preference.EditTextPreference
|
|||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.AppLog
|
import io.legado.app.constant.AppLog
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
@@ -25,7 +24,7 @@ import io.legado.app.help.storage.*
|
|||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.lib.permission.Permissions
|
import io.legado.app.lib.permission.Permissions
|
||||||
import io.legado.app.lib.permission.PermissionsCompat
|
import io.legado.app.lib.permission.PermissionsCompat
|
||||||
import io.legado.app.lib.theme.accentColor
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.ui.document.HandleFileContract
|
import io.legado.app.ui.document.HandleFileContract
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
import io.legado.app.ui.widget.dialog.TextDialog
|
||||||
@@ -33,8 +32,9 @@ import io.legado.app.utils.*
|
|||||||
import kotlinx.coroutines.Dispatchers.Main
|
import kotlinx.coroutines.Dispatchers.Main
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
import kotlin.collections.set
|
||||||
|
|
||||||
class BackupConfigFragment : BasePreferenceFragment(),
|
class BackupConfigFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||||
@@ -100,34 +100,18 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
|||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
addPreferencesFromResource(R.xml.pref_config_backup)
|
addPreferencesFromResource(R.xml.pref_config_backup)
|
||||||
findPreference<EditTextPreference>(PreferKey.webDavUrl)?.let {
|
|
||||||
it.setOnBindEditTextListener { editText ->
|
|
||||||
editText.applyTint(requireContext().accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findPreference<EditTextPreference>(PreferKey.webDavAccount)?.let {
|
|
||||||
it.setOnBindEditTextListener { editText ->
|
|
||||||
editText.applyTint(requireContext().accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
findPreference<EditTextPreference>(PreferKey.webDavPassword)?.let {
|
findPreference<EditTextPreference>(PreferKey.webDavPassword)?.let {
|
||||||
it.setOnBindEditTextListener { editText ->
|
it.setOnBindEditTextListener { editText ->
|
||||||
editText.applyTint(requireContext().accentColor)
|
|
||||||
editText.inputType =
|
editText.inputType =
|
||||||
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
InputType.TYPE_TEXT_VARIATION_PASSWORD or InputType.TYPE_CLASS_TEXT
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
findPreference<EditTextPreference>(PreferKey.webDavDir)?.let {
|
|
||||||
it.setOnBindEditTextListener { editText ->
|
|
||||||
editText.applyTint(requireContext().accentColor)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
|
upPreferenceSummary(PreferKey.webDavUrl, getPrefString(PreferKey.webDavUrl))
|
||||||
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
|
upPreferenceSummary(PreferKey.webDavAccount, getPrefString(PreferKey.webDavAccount))
|
||||||
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
|
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
|
||||||
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
|
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
|
||||||
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
|
upPreferenceSummary(PreferKey.backupPath, getPrefString(PreferKey.backupPath))
|
||||||
findPreference<io.legado.app.ui.widget.prefs.Preference>("web_dav_restore")
|
findPreference<io.legado.app.lib.prefs.Preference>("web_dav_restore")
|
||||||
?.onLongClick { restoreDir.launch(); true }
|
?.onLongClick { restoreDir.launch(); true }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +210,9 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
|||||||
return super.onPreferenceTreeClick(preference)
|
return super.onPreferenceTreeClick(preference)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 备份忽略设置
|
||||||
|
*/
|
||||||
private fun backupIgnore() {
|
private fun backupIgnore() {
|
||||||
val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
|
val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
|
||||||
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false
|
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false
|
||||||
|
|||||||
@@ -7,16 +7,16 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
|
import io.legado.app.lib.prefs.SwitchPreference
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.ui.widget.prefs.SwitchPreference
|
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
class CoverConfigFragment : BasePreferenceFragment(),
|
class CoverConfigFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val requestCodeCover = 111
|
private val requestCodeCover = 111
|
||||||
|
|||||||
@@ -6,16 +6,17 @@ import android.content.SharedPreferences
|
|||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.view.View
|
import android.view.View
|
||||||
|
import androidx.core.view.postDelayed
|
||||||
import androidx.fragment.app.activityViewModels
|
import androidx.fragment.app.activityViewModels
|
||||||
import androidx.preference.ListPreference
|
import androidx.preference.ListPreference
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
import io.legado.app.databinding.DialogEditTextBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.CheckSource
|
import io.legado.app.model.CheckSource
|
||||||
import io.legado.app.receiver.SharedReceiverActivity
|
import io.legado.app.receiver.SharedReceiverActivity
|
||||||
@@ -26,7 +27,7 @@ import io.legado.app.utils.*
|
|||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
|
|
||||||
|
|
||||||
class OtherConfigFragment : BasePreferenceFragment(),
|
class OtherConfigFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||||
@@ -131,9 +132,9 @@ class OtherConfigFragment : BasePreferenceFragment(),
|
|||||||
setProcessTextEnable(it.getBoolean(key, true))
|
setProcessTextEnable(it.getBoolean(key, true))
|
||||||
}
|
}
|
||||||
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
|
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
|
||||||
PreferKey.language -> listView.postDelayed({
|
PreferKey.language -> listView.postDelayed(1000) {
|
||||||
appCtx.restart()
|
appCtx.restart()
|
||||||
}, 1000)
|
}
|
||||||
PreferKey.userAgent -> listView.post {
|
PreferKey.userAgent -> listView.post {
|
||||||
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
|
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,6 @@ import android.widget.SeekBar
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.AppContextWrapper
|
import io.legado.app.base.AppContextWrapper
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.AppConst
|
import io.legado.app.constant.AppConst
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
@@ -24,16 +23,17 @@ import io.legado.app.help.config.AppConfig
|
|||||||
import io.legado.app.help.config.ThemeConfig
|
import io.legado.app.help.config.ThemeConfig
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
|
import io.legado.app.lib.prefs.ColorPreference
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.ui.widget.number.NumberPickerDialog
|
import io.legado.app.ui.widget.number.NumberPickerDialog
|
||||||
import io.legado.app.ui.widget.prefs.ColorPreference
|
|
||||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
|
|
||||||
@Suppress("SameParameterValue")
|
@Suppress("SameParameterValue")
|
||||||
class ThemeConfigFragment : BasePreferenceFragment(),
|
class ThemeConfigFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val requestCodeBgLight = 121
|
private val requestCodeBgLight = 121
|
||||||
|
|||||||
@@ -7,15 +7,15 @@ import android.os.Bundle
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.model.BookCover
|
import io.legado.app.model.BookCover
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import java.io.FileOutputStream
|
import java.io.FileOutputStream
|
||||||
|
|
||||||
class WelcomeConfigFragment : BasePreferenceFragment(),
|
class WelcomeConfigFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
private val requestWelcomeImage = 221
|
private val requestWelcomeImage = 221
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import android.view.KeyEvent
|
|||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
import androidx.activity.viewModels
|
import androidx.activity.viewModels
|
||||||
|
import androidx.core.view.postDelayed
|
||||||
import androidx.fragment.app.Fragment
|
import androidx.fragment.app.Fragment
|
||||||
import androidx.fragment.app.FragmentManager
|
import androidx.fragment.app.FragmentManager
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||||
@@ -87,13 +88,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
|||||||
upVersion()
|
upVersion()
|
||||||
//自动更新书籍
|
//自动更新书籍
|
||||||
if (AppConfig.autoRefreshBook) {
|
if (AppConfig.autoRefreshBook) {
|
||||||
binding.viewPagerMain.postDelayed({
|
binding.viewPagerMain.postDelayed(1000) {
|
||||||
viewModel.upAllBookToc()
|
viewModel.upAllBookToc()
|
||||||
}, 1000)
|
}
|
||||||
}
|
}
|
||||||
binding.viewPagerMain.postDelayed({
|
binding.viewPagerMain.postDelayed(3000) {
|
||||||
viewModel.postLoad()
|
viewModel.postLoad()
|
||||||
}, 3000)
|
}
|
||||||
launch {
|
launch {
|
||||||
val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() }
|
val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() }
|
||||||
?: return@launch
|
?: return@launch
|
||||||
|
|||||||
@@ -8,13 +8,16 @@ import android.view.View
|
|||||||
import androidx.preference.Preference
|
import androidx.preference.Preference
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.base.BaseFragment
|
import io.legado.app.base.BaseFragment
|
||||||
import io.legado.app.base.BasePreferenceFragment
|
|
||||||
import io.legado.app.constant.EventBus
|
import io.legado.app.constant.EventBus
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.databinding.FragmentMyConfigBinding
|
import io.legado.app.databinding.FragmentMyConfigBinding
|
||||||
import io.legado.app.help.config.AppConfig
|
import io.legado.app.help.config.AppConfig
|
||||||
import io.legado.app.help.config.ThemeConfig
|
import io.legado.app.help.config.ThemeConfig
|
||||||
import io.legado.app.lib.dialogs.selector
|
import io.legado.app.lib.dialogs.selector
|
||||||
|
import io.legado.app.lib.prefs.NameListPreference
|
||||||
|
import io.legado.app.lib.prefs.PreferenceCategory
|
||||||
|
import io.legado.app.lib.prefs.SwitchPreference
|
||||||
|
import io.legado.app.lib.prefs.fragment.PreferenceFragment
|
||||||
import io.legado.app.lib.theme.primaryColor
|
import io.legado.app.lib.theme.primaryColor
|
||||||
import io.legado.app.service.WebService
|
import io.legado.app.service.WebService
|
||||||
import io.legado.app.ui.about.AboutActivity
|
import io.legado.app.ui.about.AboutActivity
|
||||||
@@ -26,9 +29,6 @@ import io.legado.app.ui.config.ConfigActivity
|
|||||||
import io.legado.app.ui.config.ConfigTag
|
import io.legado.app.ui.config.ConfigTag
|
||||||
import io.legado.app.ui.replace.ReplaceRuleActivity
|
import io.legado.app.ui.replace.ReplaceRuleActivity
|
||||||
import io.legado.app.ui.widget.dialog.TextDialog
|
import io.legado.app.ui.widget.dialog.TextDialog
|
||||||
import io.legado.app.ui.widget.prefs.NameListPreference
|
|
||||||
import io.legado.app.ui.widget.prefs.PreferenceCategory
|
|
||||||
import io.legado.app.ui.widget.prefs.SwitchPreference
|
|
||||||
import io.legado.app.utils.*
|
import io.legado.app.utils.*
|
||||||
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
import io.legado.app.utils.viewbindingdelegate.viewBinding
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
|||||||
setSupportToolbar(binding.titleBar.toolbar)
|
setSupportToolbar(binding.titleBar.toolbar)
|
||||||
val fragmentTag = "prefFragment"
|
val fragmentTag = "prefFragment"
|
||||||
var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
|
var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
|
||||||
if (preferenceFragment == null) preferenceFragment = PreferenceFragment()
|
if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment()
|
||||||
childFragmentManager.beginTransaction()
|
childFragmentManager.beginTransaction()
|
||||||
.replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit()
|
.replace(R.id.pre_fragment, preferenceFragment, fragmentTag).commit()
|
||||||
}
|
}
|
||||||
@@ -61,7 +61,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
|||||||
/**
|
/**
|
||||||
* 配置
|
* 配置
|
||||||
*/
|
*/
|
||||||
class PreferenceFragment : BasePreferenceFragment(),
|
class MyPreferenceFragment : PreferenceFragment(),
|
||||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||||
|
|
||||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import io.legado.app.base.VMBaseActivity
|
|||||||
import io.legado.app.databinding.ActivityRssArtivlesBinding
|
import io.legado.app.databinding.ActivityRssArtivlesBinding
|
||||||
import io.legado.app.databinding.DialogEditTextBinding
|
import io.legado.app.databinding.DialogEditTextBinding
|
||||||
import io.legado.app.lib.dialogs.alert
|
import io.legado.app.lib.dialogs.alert
|
||||||
|
import io.legado.app.lib.theme.accentColor
|
||||||
import io.legado.app.ui.login.SourceLoginActivity
|
import io.legado.app.ui.login.SourceLoginActivity
|
||||||
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
import io.legado.app.ui.rss.source.edit.RssSourceEditActivity
|
||||||
import io.legado.app.utils.StartActivityContract
|
import io.legado.app.utils.StartActivityContract
|
||||||
@@ -45,9 +46,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
|
|||||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||||
binding.viewPager.adapter = adapter
|
binding.viewPager.adapter = adapter
|
||||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||||
viewModel.titleLiveData.observe(this, {
|
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||||
|
viewModel.titleLiveData.observe(this) {
|
||||||
binding.titleBar.title = it
|
binding.titleBar.title = it
|
||||||
})
|
}
|
||||||
viewModel.initData(intent) {
|
viewModel.initData(intent) {
|
||||||
upFragments()
|
upFragments()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class RssSourceEditActivity :
|
|||||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||||
it?.let {
|
it?.let {
|
||||||
viewModel.importSource(it) { source: RssSource ->
|
viewModel.importSource(it) { source: RssSource ->
|
||||||
upRecyclerView(source)
|
upSourceView(source)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -57,7 +57,7 @@ class RssSourceEditActivity :
|
|||||||
softKeyboardTool.attachToWindow(window)
|
softKeyboardTool.attachToWindow(window)
|
||||||
initView()
|
initView()
|
||||||
viewModel.initData(intent) {
|
viewModel.initData(intent) {
|
||||||
upRecyclerView()
|
upSourceView()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -130,10 +130,11 @@ class RssSourceEditActivity :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
R.id.menu_clear_cookie -> viewModel.clearCookie(getRssSource().sourceUrl)
|
||||||
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
R.id.menu_auto_complete -> viewModel.autoComplete = !viewModel.autoComplete
|
||||||
R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
|
R.id.menu_copy_source -> sendToClip(GSON.toJson(getRssSource()))
|
||||||
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
R.id.menu_qr_code_camera -> qrCodeResult.launch()
|
||||||
R.id.menu_paste_source -> viewModel.pasteSource { upRecyclerView(it) }
|
R.id.menu_paste_source -> viewModel.pasteSource { upSourceView(it) }
|
||||||
R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
|
R.id.menu_share_str -> share(GSON.toJson(getRssSource()))
|
||||||
R.id.menu_share_qr -> shareWithQr(
|
R.id.menu_share_qr -> shareWithQr(
|
||||||
GSON.toJson(getRssSource()),
|
GSON.toJson(getRssSource()),
|
||||||
@@ -150,10 +151,11 @@ class RssSourceEditActivity :
|
|||||||
binding.recyclerView.adapter = adapter
|
binding.recyclerView.adapter = adapter
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun upRecyclerView(source: RssSource? = viewModel.rssSource) {
|
private fun upSourceView(source: RssSource? = viewModel.rssSource) {
|
||||||
source?.let {
|
source?.let {
|
||||||
binding.cbIsEnable.isChecked = source.enabled
|
binding.cbIsEnable.isChecked = source.enabled
|
||||||
binding.cbSingleUrl.isChecked = source.singleUrl
|
binding.cbSingleUrl.isChecked = source.singleUrl
|
||||||
|
binding.cbIsEnableCookie.isChecked = source.enabledCookieJar == true
|
||||||
binding.cbEnableJs.isChecked = source.enableJs
|
binding.cbEnableJs.isChecked = source.enableJs
|
||||||
binding.cbEnableBaseUrl.isChecked = source.loadWithBaseUrl
|
binding.cbEnableBaseUrl.isChecked = source.loadWithBaseUrl
|
||||||
}
|
}
|
||||||
@@ -191,6 +193,7 @@ class RssSourceEditActivity :
|
|||||||
val source = viewModel.rssSource
|
val source = viewModel.rssSource
|
||||||
source.enabled = binding.cbIsEnable.isChecked
|
source.enabled = binding.cbIsEnable.isChecked
|
||||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||||
|
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||||
source.enableJs = binding.cbEnableJs.isChecked
|
source.enableJs = binding.cbEnableJs.isChecked
|
||||||
source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked
|
source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked
|
||||||
sourceEntities.forEach {
|
sourceEntities.forEach {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import io.legado.app.base.BaseViewModel
|
|||||||
import io.legado.app.data.appDb
|
import io.legado.app.data.appDb
|
||||||
import io.legado.app.data.entities.RssSource
|
import io.legado.app.data.entities.RssSource
|
||||||
import io.legado.app.help.RuleComplete
|
import io.legado.app.help.RuleComplete
|
||||||
|
import io.legado.app.help.http.CookieStore
|
||||||
import io.legado.app.utils.getClipText
|
import io.legado.app.utils.getClipText
|
||||||
import io.legado.app.utils.msg
|
import io.legado.app.utils.msg
|
||||||
import io.legado.app.utils.printOnDebug
|
import io.legado.app.utils.printOnDebug
|
||||||
@@ -77,6 +78,12 @@ class RssSourceEditViewModel(application: Application) : BaseViewModel(applicati
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun clearCookie(url: String) {
|
||||||
|
execute {
|
||||||
|
CookieStore.removeCookie(url)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
fun ruleComplete(rule: String?, preRule: String? = null, type: Int = 1): String? {
|
||||||
if (autoComplete) {
|
if (autoComplete) {
|
||||||
return RuleComplete.autoComplete(rule, preRule, type)
|
return RuleComplete.autoComplete(rule, preRule, type)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.welcome
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.graphics.drawable.BitmapDrawable
|
import android.graphics.drawable.BitmapDrawable
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import androidx.core.view.postDelayed
|
||||||
import io.legado.app.base.BaseActivity
|
import io.legado.app.base.BaseActivity
|
||||||
import io.legado.app.constant.PreferKey
|
import io.legado.app.constant.PreferKey
|
||||||
import io.legado.app.constant.Theme
|
import io.legado.app.constant.Theme
|
||||||
@@ -26,7 +27,7 @@ open class WelcomeActivity : BaseActivity<ActivityWelcomeBinding>() {
|
|||||||
if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) {
|
if (intent.flags and Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT != 0) {
|
||||||
finish()
|
finish()
|
||||||
} else {
|
} else {
|
||||||
binding.root.postDelayed({ startMainActivity() }, 600)
|
binding.root.postDelayed(600) { startMainActivity() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import android.util.AttributeSet
|
|||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.animation.LinearInterpolator
|
import android.view.animation.LinearInterpolator
|
||||||
import android.widget.Checkable
|
import android.widget.Checkable
|
||||||
|
import androidx.core.view.postDelayed
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.lib.theme.ThemeStore
|
import io.legado.app.lib.theme.ThemeStore
|
||||||
import io.legado.app.utils.dpToPx
|
import io.legado.app.utils.dpToPx
|
||||||
@@ -244,7 +245,7 @@ class SmoothCheckBox @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
// invalidate
|
// invalidate
|
||||||
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {
|
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {
|
||||||
postDelayed({ this.postInvalidate() }, 10)
|
postDelayed(10) { this.postInvalidate() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,10 +299,10 @@ class SmoothCheckBox @JvmOverloads constructor(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun drawTickDelayed() {
|
private fun drawTickDelayed() {
|
||||||
postDelayed({
|
postDelayed(mAnimDuration.toLong()) {
|
||||||
mTickDrawing = true
|
mTickDrawing = true
|
||||||
postInvalidate()
|
postInvalidate()
|
||||||
}, mAnimDuration.toLong())
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
package io.legado.app.ui.widget.prefs
|
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.util.AttributeSet
|
|
||||||
import android.widget.TextView
|
|
||||||
import androidx.preference.PreferenceViewHolder
|
|
||||||
import io.legado.app.R
|
|
||||||
|
|
||||||
class EditTextPreference(context: Context, attrs: AttributeSet) :
|
|
||||||
androidx.preference.EditTextPreference(context, attrs) {
|
|
||||||
|
|
||||||
init {
|
|
||||||
// isPersistent = true
|
|
||||||
layoutResource = R.layout.view_preference
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
|
||||||
Preference.bindView<TextView>(context, holder, icon, title, summary, null, null)
|
|
||||||
super.onBindViewHolder(holder)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -15,6 +15,7 @@ import android.widget.FrameLayout.LayoutParams
|
|||||||
import androidx.appcompat.widget.AppCompatTextView
|
import androidx.appcompat.widget.AppCompatTextView
|
||||||
import io.legado.app.R
|
import io.legado.app.R
|
||||||
import io.legado.app.lib.theme.accentColor
|
import io.legado.app.lib.theme.accentColor
|
||||||
|
import io.legado.app.utils.ColorUtils
|
||||||
import io.legado.app.utils.getCompatColor
|
import io.legado.app.utils.getCompatColor
|
||||||
import io.legado.app.utils.invisible
|
import io.legado.app.utils.invisible
|
||||||
import io.legado.app.utils.visible
|
import io.legado.app.utils.visible
|
||||||
@@ -87,8 +88,6 @@ class BadgeView @JvmOverloads constructor(
|
|||||||
setLayoutParams(layoutParams)
|
setLayoutParams(layoutParams)
|
||||||
}
|
}
|
||||||
|
|
||||||
// set default font
|
|
||||||
setTextColor(Color.WHITE)
|
|
||||||
//setTypeface(Typeface.DEFAULT_BOLD);
|
//setTypeface(Typeface.DEFAULT_BOLD);
|
||||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
|
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
|
||||||
setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
|
setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
|
||||||
@@ -106,6 +105,10 @@ class BadgeView @JvmOverloads constructor(
|
|||||||
minHeight = dip2Px(16f)
|
minHeight = dip2Px(16f)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun setBackgroundColor(color: Int) {
|
||||||
|
setBackground(radius, color)
|
||||||
|
}
|
||||||
|
|
||||||
fun setBackground(dipRadius: Float, badgeColor: Int) {
|
fun setBackground(dipRadius: Float, badgeColor: Int) {
|
||||||
val radius = dip2Px(dipRadius).toFloat()
|
val radius = dip2Px(dipRadius).toFloat()
|
||||||
val radiusArray =
|
val radiusArray =
|
||||||
@@ -118,10 +121,13 @@ class BadgeView @JvmOverloads constructor(
|
|||||||
val bgDrawable = ShapeDrawable(roundRect)
|
val bgDrawable = ShapeDrawable(roundRect)
|
||||||
bgDrawable.paint.color = badgeColor
|
bgDrawable.paint.color = badgeColor
|
||||||
background = bgDrawable
|
background = bgDrawable
|
||||||
}
|
setTextColor(
|
||||||
|
if (ColorUtils.isColorLight(badgeColor)) {
|
||||||
fun setBackground(badgeColor: Int) {
|
Color.BLACK
|
||||||
setBackground(radius, badgeColor)
|
} else {
|
||||||
|
Color.WHITE
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -142,9 +148,9 @@ class BadgeView @JvmOverloads constructor(
|
|||||||
|
|
||||||
fun setHighlight(highlight: Boolean) {
|
fun setHighlight(highlight: Boolean) {
|
||||||
if (highlight) {
|
if (highlight) {
|
||||||
setBackground(context.accentColor)
|
setBackgroundColor(context.accentColor)
|
||||||
} else {
|
} else {
|
||||||
setBackground(context.getCompatColor(R.color.darker_gray))
|
setBackgroundColor(context.getCompatColor(R.color.darker_gray))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,20 +1,22 @@
|
|||||||
package io.legado.app.utils
|
package io.legado.app.utils
|
||||||
|
|
||||||
import android.graphics.Color
|
import android.graphics.Color
|
||||||
|
|
||||||
import androidx.annotation.ColorInt
|
import androidx.annotation.ColorInt
|
||||||
import androidx.annotation.FloatRange
|
import androidx.annotation.FloatRange
|
||||||
import java.util.*
|
import androidx.core.graphics.ColorUtils
|
||||||
import kotlin.math.*
|
import kotlin.math.*
|
||||||
|
|
||||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||||
object ColorUtils {
|
object ColorUtils {
|
||||||
|
|
||||||
|
fun isColorLight(@ColorInt color: Int): Boolean {
|
||||||
|
return ColorUtils.calculateLuminance(color) >= 0.5
|
||||||
|
}
|
||||||
|
|
||||||
fun intToString(intColor: Int): String {
|
fun intToString(intColor: Int): String {
|
||||||
return String.format("#%06X", 0xFFFFFF and intColor)
|
return String.format("#%06X", 0xFFFFFF and intColor)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
fun stripAlpha(@ColorInt color: Int): Int {
|
fun stripAlpha(@ColorInt color: Int): Int {
|
||||||
return -0x1000000 or color
|
return -0x1000000 or color
|
||||||
}
|
}
|
||||||
@@ -39,12 +41,6 @@ object ColorUtils {
|
|||||||
return shiftColor(color, 1.1f)
|
return shiftColor(color, 1.1f)
|
||||||
}
|
}
|
||||||
|
|
||||||
fun isColorLight(@ColorInt color: Int): Boolean {
|
|
||||||
val darkness =
|
|
||||||
1 - (0.299 * Color.red(color) + 0.587 * Color.green(color) + 0.114 * Color.blue(color)) / 255
|
|
||||||
return darkness < 0.4
|
|
||||||
}
|
|
||||||
|
|
||||||
@ColorInt
|
@ColorInt
|
||||||
fun invertColor(@ColorInt color: Int): Int {
|
fun invertColor(@ColorInt color: Int): Int {
|
||||||
val r = 255 - Color.red(color)
|
val r = 255 - Color.red(color)
|
||||||
@@ -81,83 +77,6 @@ object ColorUtils {
|
|||||||
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
|
return Color.argb(a.toInt(), r.toInt(), g.toInt(), b.toInt())
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 按条件的到随机颜色
|
|
||||||
*
|
|
||||||
* @param alpha 透明
|
|
||||||
* @param lower 下边界
|
|
||||||
* @param upper 上边界
|
|
||||||
* @return 颜色值
|
|
||||||
*/
|
|
||||||
fun getRandomColor(alpha: Int, lower: Int, upper: Int): Int {
|
|
||||||
return RandomColor(alpha, lower, upper).color
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @return 获取随机色
|
|
||||||
*/
|
|
||||||
fun getRandomColor(): Int {
|
|
||||||
return RandomColor(255, 80, 200).color
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 随机颜色
|
|
||||||
*/
|
|
||||||
class RandomColor(alpha: Int, lower: Int, upper: Int) {
|
|
||||||
private var alpha: Int = 0
|
|
||||||
private var lower: Int = 0
|
|
||||||
private var upper: Int = 0
|
|
||||||
|
|
||||||
//随机数是前闭 后开
|
|
||||||
val color: Int
|
|
||||||
get() {
|
|
||||||
val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
|
||||||
val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
|
||||||
val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
|
||||||
|
|
||||||
return Color.argb(getAlpha(), red, green, blue)
|
|
||||||
}
|
|
||||||
|
|
||||||
init {
|
|
||||||
require(upper > lower) { "must be lower < upper" }
|
|
||||||
setAlpha(alpha)
|
|
||||||
setLower(lower)
|
|
||||||
setUpper(upper)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getAlpha(): Int {
|
|
||||||
return alpha
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setAlpha(alpha: Int) {
|
|
||||||
var alpha1 = alpha
|
|
||||||
if (alpha1 > 255) alpha1 = 255
|
|
||||||
if (alpha1 < 0) alpha1 = 0
|
|
||||||
this.alpha = alpha1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getLower(): Int {
|
|
||||||
return lower
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setLower(lower: Int) {
|
|
||||||
var lower1 = lower
|
|
||||||
if (lower1 < 0) lower1 = 0
|
|
||||||
this.lower = lower1
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getUpper(): Int {
|
|
||||||
return upper
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun setUpper(upper: Int) {
|
|
||||||
var upper1 = upper
|
|
||||||
if (upper1 > 255) upper1 = 255
|
|
||||||
this.upper = upper1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fun argb(R: Int, G: Int, B: Int): Int {
|
fun argb(R: Int, G: Int, B: Int): Int {
|
||||||
return argb(Byte.MAX_VALUE.toInt(), R, G, B)
|
return argb(Byte.MAX_VALUE.toInt(), R, G, B)
|
||||||
}
|
}
|
||||||
@@ -177,72 +96,19 @@ object ColorUtils {
|
|||||||
+ (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))
|
+ (colorByteArr[2].toInt() and 0xFF shl 8) + (colorByteArr[3].toInt() and 0xFF))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun rgb2lab(R: Int, G: Int, B: Int): IntArray {
|
|
||||||
val x: Float
|
|
||||||
val y: Float
|
|
||||||
val z: Float
|
|
||||||
val fx: Float
|
|
||||||
val fy: Float
|
|
||||||
val fz: Float
|
|
||||||
val xr: Float
|
|
||||||
val yr: Float
|
|
||||||
val zr: Float
|
|
||||||
val eps = 216f / 24389f
|
|
||||||
val k = 24389f / 27f
|
|
||||||
val xr1 = 0.964221f // reference white D50
|
|
||||||
val yr1 = 1.0f
|
|
||||||
val zr1 = 0.825211f
|
|
||||||
|
|
||||||
// RGB to XYZ
|
|
||||||
var r: Float = R / 255f //R 0..1
|
|
||||||
var g: Float = G / 255f //G 0..1
|
|
||||||
var b: Float = B / 255f //B 0..1
|
|
||||||
|
|
||||||
// assuming sRGB (D65)
|
|
||||||
r = if (r <= 0.04045) r / 12 else ((r + 0.055) / 1.055).pow(2.4).toFloat()
|
|
||||||
g = if (g <= 0.04045) g / 12 else ((g + 0.055) / 1.055).pow(2.4).toFloat()
|
|
||||||
b = if (b <= 0.04045) b / 12 else ((b + 0.055) / 1.055).pow(2.4).toFloat()
|
|
||||||
x = 0.436052025f * r + 0.385081593f * g + 0.143087414f * b
|
|
||||||
y = 0.222491598f * r + 0.71688606f * g + 0.060621486f * b
|
|
||||||
z = 0.013929122f * r + 0.097097002f * g + 0.71418547f * b
|
|
||||||
|
|
||||||
// XYZ to Lab
|
|
||||||
xr = x / xr1
|
|
||||||
yr = y / yr1
|
|
||||||
zr = z / zr1
|
|
||||||
fx = if (xr > eps) xr.toDouble().pow(1 / 3.0)
|
|
||||||
.toFloat() else ((k * xr + 16.0) / 116.0).toFloat()
|
|
||||||
fy = if (yr > eps) yr.toDouble().pow(1 / 3.0)
|
|
||||||
.toFloat() else ((k * yr + 16.0) / 116.0).toFloat()
|
|
||||||
fz = if (zr > eps) zr.toDouble().pow(1 / 3.0)
|
|
||||||
.toFloat() else ((k * zr + 16.0) / 116).toFloat()
|
|
||||||
val ls: Float = 116 * fy - 16
|
|
||||||
val `as`: Float = 500 * (fx - fy)
|
|
||||||
val bs: Float = 200 * (fy - fz)
|
|
||||||
val lab = IntArray(3)
|
|
||||||
lab[0] = (2.55 * ls + .5).toInt()
|
|
||||||
lab[1] = (`as` + .5).toInt()
|
|
||||||
lab[2] = (bs + .5).toInt()
|
|
||||||
return lab
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Computes the difference between two RGB colors by converting them to the L*a*b scale and
|
* Computes the difference between two RGB colors by converting them to the L*a*b scale and
|
||||||
* comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
|
* comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
|
||||||
*/
|
*/
|
||||||
fun getColorDifference(a: Int, b: Int): Double {
|
fun getColorDifference(a: Int, b: Int): Double {
|
||||||
val r1: Int = Color.red(a)
|
val lab1 = DoubleArray(3)
|
||||||
val g1: Int = Color.green(a)
|
val lab2 = DoubleArray(3)
|
||||||
val b1: Int = Color.blue(a)
|
ColorUtils.colorToLAB(a, lab1)
|
||||||
val r2: Int = Color.red(b)
|
ColorUtils.colorToLAB(b, lab2)
|
||||||
val g2: Int = Color.green(b)
|
|
||||||
val b2: Int = Color.blue(b)
|
|
||||||
val lab1 = rgb2lab(r1, g1, b1)
|
|
||||||
val lab2 = rgb2lab(r2, g2, b2)
|
|
||||||
return sqrt(
|
return sqrt(
|
||||||
(lab2[0] - lab1[0].toDouble())
|
(lab2[0] - lab1[0])
|
||||||
.pow(2.0) + (lab2[1] - lab1[1].toDouble())
|
.pow(2.0) + (lab2[1] - lab1[1])
|
||||||
.pow(2.0) + (lab2[2] - lab1[2].toDouble())
|
.pow(2.0) + (lab2[2] - lab1[2])
|
||||||
.pow(2.0)
|
.pow(2.0)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -331,6 +331,7 @@ fun Context.openFileUri(uri: Uri, type: String? = null) {
|
|||||||
|
|
||||||
@Suppress("DEPRECATION")
|
@Suppress("DEPRECATION")
|
||||||
val Context.isWifiConnect: Boolean
|
val Context.isWifiConnect: Boolean
|
||||||
|
@SuppressLint("MissingPermission")
|
||||||
get() {
|
get() {
|
||||||
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
||||||
return info?.isConnected == true
|
return info?.isConnected == true
|
||||||
@@ -338,7 +339,7 @@ val Context.isWifiConnect: Boolean
|
|||||||
|
|
||||||
val Context.isPad: Boolean
|
val Context.isPad: Boolean
|
||||||
get() {
|
get() {
|
||||||
return resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
return (resources.configuration.screenLayout and Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_LARGE
|
||||||
}
|
}
|
||||||
|
|
||||||
val Context.channel: String
|
val Context.channel: String
|
||||||
|
|||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package io.legado.app.utils
|
||||||
|
|
||||||
|
import android.graphics.Color
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
@Suppress("unused")
|
||||||
|
class RandomColor(alpha: Int, lower: Int, upper: Int) {
|
||||||
|
|
||||||
|
constructor() : this(255, 80, 200)
|
||||||
|
|
||||||
|
private var alpha: Int = 0
|
||||||
|
private var lower: Int = 0
|
||||||
|
private var upper: Int = 0
|
||||||
|
|
||||||
|
init {
|
||||||
|
require(upper > lower) { "must be lower < upper" }
|
||||||
|
setAlpha(alpha)
|
||||||
|
setLower(lower)
|
||||||
|
setUpper(upper)
|
||||||
|
}
|
||||||
|
|
||||||
|
//随机数是前闭 后开
|
||||||
|
fun build(): Int {
|
||||||
|
val red = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||||
|
val green = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||||
|
val blue = getLower() + Random().nextInt(getUpper() - getLower() + 1)
|
||||||
|
return Color.argb(getAlpha(), red, green, blue)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getAlpha(): Int {
|
||||||
|
return alpha
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setAlpha(alpha: Int) {
|
||||||
|
var alpha1 = alpha
|
||||||
|
if (alpha1 > 255) alpha1 = 255
|
||||||
|
if (alpha1 < 0) alpha1 = 0
|
||||||
|
this.alpha = alpha1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getLower(): Int {
|
||||||
|
return lower
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setLower(lower: Int) {
|
||||||
|
var lower1 = lower
|
||||||
|
if (lower1 < 0) lower1 = 0
|
||||||
|
this.lower = lower1
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getUpper(): Int {
|
||||||
|
return upper
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun setUpper(upper: Int) {
|
||||||
|
var upper1 = upper
|
||||||
|
if (upper1 > 255) upper1 = 255
|
||||||
|
this.upper = upper1
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -1,11 +1,13 @@
|
|||||||
package io.legado.app.utils
|
package io.legado.app.utils
|
||||||
|
|
||||||
|
import androidx.core.os.postDelayed
|
||||||
import io.legado.app.exception.RegexTimeoutException
|
import io.legado.app.exception.RegexTimeoutException
|
||||||
import io.legado.app.help.CrashHandler
|
import io.legado.app.help.CrashHandler
|
||||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
import splitties.init.appCtx
|
import splitties.init.appCtx
|
||||||
import kotlin.concurrent.thread
|
import kotlin.concurrent.thread
|
||||||
import kotlin.coroutines.resume
|
import kotlin.coroutines.resume
|
||||||
|
import kotlin.coroutines.resumeWithException
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 带有超时检测的正则替换
|
* 带有超时检测的正则替换
|
||||||
@@ -19,23 +21,23 @@ suspend fun CharSequence.replace(regex: Regex, replacement: String, timeout: Lon
|
|||||||
val result = regex.replace(charSequence, replacement)
|
val result = regex.replace(charSequence, replacement)
|
||||||
block.resume(result)
|
block.resume(result)
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
block.cancel(e)
|
block.resumeWithException(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
mainHandler.postDelayed({
|
mainHandler.postDelayed(timeout) {
|
||||||
if (thread.isAlive) {
|
if (thread.isAlive) {
|
||||||
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
|
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
|
||||||
val exception = RegexTimeoutException(timeoutMsg)
|
val exception = RegexTimeoutException(timeoutMsg)
|
||||||
block.cancel(exception)
|
block.cancel(exception)
|
||||||
appCtx.longToastOnUi(timeoutMsg)
|
appCtx.longToastOnUi(timeoutMsg)
|
||||||
CrashHandler.saveCrashInfo2File(exception)
|
CrashHandler.saveCrashInfo2File(exception)
|
||||||
mainHandler.postDelayed({
|
mainHandler.postDelayed(3000) {
|
||||||
if (thread.isAlive) {
|
if (thread.isAlive) {
|
||||||
appCtx.restart()
|
appCtx.restart()
|
||||||
}
|
}
|
||||||
}, 3000)
|
}
|
||||||
}
|
}
|
||||||
}, timeout)
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
|
|||||||
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
||||||
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
||||||
"/saveBook" -> BookController.saveBook(postData)
|
"/saveBook" -> BookController.saveBook(postData)
|
||||||
|
"/saveBookProgress" -> BookController.saveBookProgress(postData)
|
||||||
"/addLocalBook" -> BookController.addLocalBook(session.parameters)
|
"/addLocalBook" -> BookController.addLocalBook(session.parameters)
|
||||||
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
||||||
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
||||||
|
|||||||
@@ -33,6 +33,13 @@
|
|||||||
android:checked="true"
|
android:checked="true"
|
||||||
android:text="@string/discovery" />
|
android:text="@string/discovery" />
|
||||||
|
|
||||||
|
<io.legado.app.lib.theme.view.ThemeCheckBox
|
||||||
|
android:id="@+id/cb_is_enable_cookie"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/auto_save_cookie" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="wrap_content"
|
android:layout_height="wrap_content"
|
||||||
|
|||||||
@@ -45,6 +45,13 @@
|
|||||||
android:text="@string/single_url"
|
android:text="@string/single_url"
|
||||||
tools:ignore="TouchTargetSizeCheck" />
|
tools:ignore="TouchTargetSizeCheck" />
|
||||||
|
|
||||||
|
<io.legado.app.lib.theme.view.ThemeCheckBox
|
||||||
|
android:id="@+id/cb_is_enable_cookie"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:checked="true"
|
||||||
|
android:text="@string/auto_save_cookie" />
|
||||||
|
|
||||||
</LinearLayout>
|
</LinearLayout>
|
||||||
|
|
||||||
<LinearLayout
|
<LinearLayout
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||||
|
xmlns:tools="http://schemas.android.com/tools"
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:padding="8dp">
|
||||||
|
|
||||||
|
<TextView
|
||||||
|
android:id="@+id/cb_file_name"
|
||||||
|
android:layout_width="wrap_content"
|
||||||
|
android:layout_height="wrap_content"
|
||||||
|
android:textColor="@color/primaryText"
|
||||||
|
android:textSize="14sp"
|
||||||
|
app:layout_constrainedWidth="true"
|
||||||
|
app:layout_constraintBottom_toBottomOf="parent"
|
||||||
|
app:layout_constraintHorizontal_bias="0"
|
||||||
|
app:layout_constraintHorizontal_chainStyle="packed"
|
||||||
|
app:layout_constraintLeft_toLeftOf="parent"
|
||||||
|
app:layout_constraintTop_toTopOf="parent"
|
||||||
|
tools:ignore="TouchTargetSizeCheck" />
|
||||||
|
|
||||||
|
</androidx.constraintlayout.widget.ConstraintLayout>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user