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)
|
||||
* [Jsdelivr](https://cdn.jsdelivr.net/gh/gedoor/release@release/)
|
||||
* [\#Beta](https://kunfei.lanzoui.com/b0f810h4b)
|
||||
* [IzzyOnDroid F-Droid Repository](https://apt.izzysoft.de/fdroid/index/apk/io.legado.app.release)
|
||||
|
||||
|
||||
#### IOS-苹果
|
||||
|
||||
@@ -1,30 +1,37 @@
|
||||
# 阅读API
|
||||
|
||||
## 对于Web的配置
|
||||
您需要先在设置中启用"Web 服务"。
|
||||
|
||||
您需要先在设置中启用"Web 服务"。
|
||||
|
||||
## 使用
|
||||
|
||||
### Web
|
||||
|
||||
以下说明假设您的操作在本机进行,且开放端口为1234。
|
||||
如果您要从远程计算机访问[阅读](),请将`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
|
||||
Method = POST
|
||||
```
|
||||
|
||||
请求BODY内容为`JSON`字符串,
|
||||
格式参考[这个文件](https://github.com/gedoor/legado/blob/master/app/src/main/java/io/legado/app/data/entities/BookSource.kt)
|
||||
|
||||
#### 插入多个书源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/saveRssSources
|
||||
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订阅源
|
||||
|
||||
请求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/deleteRssSources
|
||||
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
|
||||
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
|
||||
Method = GET
|
||||
```
|
||||
|
||||
获取APP内的所有书籍。
|
||||
获取APP内的所有书籍。
|
||||
|
||||
#### 获取书籍章节列表
|
||||
|
||||
```
|
||||
URL = http://127.0.0.1:1234/getChapterList?url=xxx
|
||||
Method = GET
|
||||
```
|
||||
|
||||
获取指定图书的章节列表。
|
||||
获取指定图书的章节列表。
|
||||
|
||||
#### 获取书籍内容
|
||||
|
||||
```
|
||||
URL = http://127.0.0.1:1234/getBookContent?url=xxx&index=1
|
||||
Method = GET
|
||||
```
|
||||
|
||||
获取指定图书的第`index`章节的文本内容。
|
||||
|
||||
#### 获取封面
|
||||
|
||||
```
|
||||
URL = http://127.0.0.1:1234/cover?path=xxxxx
|
||||
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
|
||||
|
||||
* 需声明`io.legado.READ_WRITE`权限
|
||||
* `providerHost`为`包名.readerProvider`, 如`io.legado.app.release.readerProvider`,不同包的地址不同,防止冲突安装失败
|
||||
* 以下出现的`providerHost`请自行替换
|
||||
|
||||
#### 插入单个书源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/rssSource/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订阅源
|
||||
|
||||
创建`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/rssSources/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订阅源
|
||||
|
||||
获取指定URL对应的书源信息。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
```
|
||||
URL = content://providerHost/bookSource/query?url=xxx
|
||||
URL = content://providerHost/rssSource/query?url=xxx
|
||||
Method = query
|
||||
```
|
||||
|
||||
获取指定URL对应的书源信息。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
#### 获取所有书源or订阅源
|
||||
|
||||
获取APP内的所有订阅源。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
```
|
||||
URL = content://providerHost/bookSources/query
|
||||
URL = content://providerHost/rssSources/query
|
||||
Method = query
|
||||
```
|
||||
|
||||
获取APP内的所有书源。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
#### 删除多个书源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/rssSources/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
|
||||
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
|
||||
Method = query
|
||||
```
|
||||
|
||||
获取APP内的所有书籍。
|
||||
#### 获取书籍章节列表
|
||||
|
||||
获取指定图书的章节列表。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
#### 获取书籍章节列表
|
||||
```
|
||||
URL = content://providerHost/book/chapter/query?url=xxx
|
||||
Method = query
|
||||
```
|
||||
|
||||
获取指定图书的章节列表。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
#### 获取书籍内容
|
||||
|
||||
获取指定图书的第`index`章节的文本内容。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
```
|
||||
URL = content://providerHost/book/content/query?url=xxx&index=1
|
||||
Method = query
|
||||
```
|
||||
获取指定图书的第`index`章节的文本内容。
|
||||
用`Cursor.getString(0)`取出返回结果。
|
||||
|
||||
#### 获取封面
|
||||
|
||||
```
|
||||
URL = content://providerHost/book/cover/query?path=xxxx
|
||||
Method = query
|
||||
|
||||
+6
-4
@@ -3,7 +3,7 @@ apply plugin: 'kotlin-android'
|
||||
apply plugin: 'kotlin-parcelize'
|
||||
apply plugin: 'kotlin-kapt'
|
||||
apply plugin: 'de.timfreiheit.resourceplaceholders'
|
||||
//apply plugin: 'com.google.gms.google-services'
|
||||
apply plugin: 'com.google.gms.google-services'
|
||||
apply from: 'download.gradle'
|
||||
|
||||
static def releaseTime() {
|
||||
@@ -179,8 +179,10 @@ dependencies {
|
||||
androidTestImplementation 'androidx.compose.ui:ui-test-junit4:1.1.1'
|
||||
|
||||
//firebase
|
||||
//implementation platform('com.google.firebase:firebase-bom:29.1.0')
|
||||
//implementation 'com.google.firebase:firebase-analytics-ktx'
|
||||
implementation platform('com.google.firebase:firebase-bom:30.0.1')
|
||||
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
|
||||
implementation("androidx.media:media:1.6.0")
|
||||
@@ -219,7 +221,7 @@ dependencies {
|
||||
implementation(fileTree(dir: 'cronetlib', include: ['*.jar', '*.aar']))
|
||||
|
||||
//Glide
|
||||
def glideVersion = "4.13.1"
|
||||
def glideVersion = "4.13.2"
|
||||
implementation("com.github.bumptech.glide:glide:$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"?>
|
||||
<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.ACCESS_MEDIA_LOCATION" />
|
||||
|
||||
@@ -10,7 +10,11 @@
|
||||
@Json: json规则,直接写时以$.开头可省略@Json
|
||||
: regex规则,不可省略,只可以用在书籍列表和目录列表
|
||||
```
|
||||
* 书源类型: 文件
|
||||
> 对于类似知轩藏书提供文件整合下载的网站,可以'在书源详情的下载URL规则获取文件链接,支持多个链接,阅读会自动下载并导入
|
||||
|
||||
* CookieJar
|
||||
> 启用后会自动保存每次返回头中的Set-Cookie中的值,适用于验证码图片一类需要session的网站
|
||||
* 登录UI
|
||||
> 不使用内置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**
|
||||
|
||||
* 更新cronet: 101.0.4951.61
|
||||
|
||||
@@ -25,7 +25,7 @@
|
||||
<div>
|
||||
<div>源类型 :</div>
|
||||
<textarea rows="1" id="bookSourceType" class="base" title="bookSourceType"
|
||||
placeholder="<必填>0:文本 1:音频 2:图片"></textarea>
|
||||
placeholder="<必填>0:文本 1:音频 2:图片 3:文件(只提供下载的网站)"></textarea>
|
||||
</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;
|
||||
}</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 {
|
||||
SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
|
||||
SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
|
||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover
|
||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover,
|
||||
SaveBookProgress
|
||||
}
|
||||
|
||||
private val postBodyKey = "json"
|
||||
@@ -86,6 +87,9 @@ class ReaderProvider : ContentProvider() {
|
||||
RequestCode.SaveBook -> values?.let {
|
||||
BookController.saveBook(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveBookProgress -> values?.let {
|
||||
BookController.saveBookProgress(values.getAsString(postBodyKey))
|
||||
}
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||
)
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
package io.legado.app.api.controller
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
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.help.BookHelp
|
||||
import io.legado.app.help.CacheManager
|
||||
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.storage.AppWebDav
|
||||
import io.legado.app.model.BookCover
|
||||
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.UmdFile
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.ui.book.read.page.provider.ImageProvider
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
|
||||
object BookController {
|
||||
|
||||
@@ -177,7 +170,6 @@ object BookController {
|
||||
var content: String? = BookHelp.getContent(book, chapter)
|
||||
if (content != null) {
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
content = runBlocking {
|
||||
contentProcessor.getContent(book, chapter, content!!, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
@@ -190,7 +182,6 @@ object BookController {
|
||||
content = runBlocking {
|
||||
WebBook.getContentAwait(this, bookSource, book, chapter).let {
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
contentProcessor.getContent(book, chapter, it, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
}
|
||||
@@ -222,20 +213,26 @@ object BookController {
|
||||
/**
|
||||
* 保存进度
|
||||
*/
|
||||
private fun saveBookReadIndex(book: Book, index: Int) {
|
||||
book.durChapterIndex = index
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
|
||||
book.durChapterTitle = it.title
|
||||
}
|
||||
appDb.bookDao.update(book)
|
||||
AppWebDav.uploadBookProgress(book)
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = book
|
||||
ReadBook.durChapterIndex = index
|
||||
ReadBook.clearTextChapter()
|
||||
ReadBook.loadContent(true)
|
||||
}
|
||||
fun saveBookProgress(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
GSON.fromJsonObject<BookProgress>(postData)
|
||||
.onFailure { it.printOnDebug() }
|
||||
.getOrNull()?.let { bookProgress ->
|
||||
appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
|
||||
book.durChapterIndex = bookProgress.durChapterIndex
|
||||
book.durChapterPos = bookProgress.durChapterPos
|
||||
book.durChapterTitle = bookProgress.durChapterTitle
|
||||
book.durChapterTime = bookProgress.durChapterTime
|
||||
appDb.bookDao.update(book)
|
||||
AppWebDav.uploadBookProgress(bookProgress)
|
||||
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()
|
||||
?: return returnData.setErrorMsg("fileData 不能为空")
|
||||
kotlin.runCatching {
|
||||
val defaultBookTreeUri = AppConfig.defaultBookTreeUri
|
||||
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)
|
||||
LocalBook.importFileOnLine(fileData, fileName)
|
||||
}.onFailure {
|
||||
return when (it) {
|
||||
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")
|
||||
|
||||
@@ -5,11 +5,12 @@ import androidx.annotation.IntDef
|
||||
object BookType {
|
||||
const val default = 0 // 0 文本
|
||||
const val audio = 1 // 1 音频
|
||||
const val image = 2 //图片
|
||||
const val image = 2 // 2 图片
|
||||
const val file = 3 // 3 只提供下载服务的网站
|
||||
const val local = "loc_book"
|
||||
|
||||
@Target(AnnotationTarget.VALUE_PARAMETER)
|
||||
@Retention(AnnotationRetention.SOURCE)
|
||||
@IntDef(default, audio, image)
|
||||
@IntDef(default, audio, image, file)
|
||||
annotation class Type
|
||||
}
|
||||
@@ -28,4 +28,5 @@ object EventBus {
|
||||
const val TIP_COLOR = "tipColor"
|
||||
const val SOURCE_CHANGED = "sourceChanged"
|
||||
const val SEARCH_RESULT = "searchResult"
|
||||
const val BOOK_URL_CHANGED = "bookUrlChanged"
|
||||
}
|
||||
@@ -20,7 +20,7 @@ val appDb by lazy {
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 48,
|
||||
version = 49,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
@@ -32,7 +32,8 @@ val appDb by lazy {
|
||||
AutoMigration(from = 44, to = 45),
|
||||
AutoMigration(from = 45, to = 46),
|
||||
AutoMigration(from = 46, to = 47),
|
||||
AutoMigration(from = 47, to = 48)
|
||||
AutoMigration(from = 47, to = 48),
|
||||
AutoMigration(from = 48, to = 49)
|
||||
]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
@@ -21,6 +21,7 @@ interface BaseSource : JsExtensions {
|
||||
var loginUrl: String? // 登录地址
|
||||
var loginUi: String? // 登录UI
|
||||
var header: String? // 请求头
|
||||
var enabledCookieJar: Boolean? //启用cookieJar
|
||||
|
||||
fun getTag(): String
|
||||
|
||||
|
||||
@@ -154,6 +154,10 @@ data class Book(
|
||||
@IgnoredOnParcel
|
||||
override var tocHtml: String? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
var downloadUrls: List<String>? = null
|
||||
|
||||
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
|
||||
|
||||
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
||||
|
||||
@@ -7,6 +7,7 @@ import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
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.RuleDataInterface
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
@@ -93,7 +95,7 @@ data class BookChapter(
|
||||
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
|
||||
}
|
||||
}
|
||||
if (useReplace && replaceRules != null) {
|
||||
if (useReplace && replaceRules != null) kotlin.run {
|
||||
replaceRules.forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
try {
|
||||
@@ -112,7 +114,10 @@ data class BookChapter(
|
||||
} catch (e: RegexTimeoutException) {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
} catch (e: CancellationException) {
|
||||
return@run
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
import java.io.InputStream
|
||||
|
||||
@Suppress("unused")
|
||||
@Parcelize
|
||||
@TypeConverters(BookSource.Converters::class)
|
||||
@Entity(
|
||||
@@ -27,7 +28,7 @@ data class BookSource(
|
||||
var bookSourceName: String = "",
|
||||
// 分组
|
||||
var bookSourceGroup: String? = null,
|
||||
// 类型,0 文本,1 音频, 2 图片
|
||||
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
|
||||
@BookType.Type
|
||||
var bookSourceType: Int = 0,
|
||||
// 详情页url正则
|
||||
@@ -38,6 +39,9 @@ data class BookSource(
|
||||
var enabled: Boolean = true,
|
||||
// 启用发现
|
||||
var enabledExplore: Boolean = true,
|
||||
// 启用okhttp CookieJAr 自动保存每次请求的cookie
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
// 并发率
|
||||
override var concurrentRate: String? = null,
|
||||
// 请求头
|
||||
@@ -158,7 +162,7 @@ data class BookSource(
|
||||
|
||||
fun removeGroup(groups: String): BookSource {
|
||||
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
|
||||
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
|
||||
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
|
||||
bookSourceGroup = TextUtils.join(",", it)
|
||||
}
|
||||
return this
|
||||
@@ -190,6 +194,7 @@ data class BookSource(
|
||||
&& equal(bookSourceComment, source.bookSourceComment)
|
||||
&& enabled == source.enabled
|
||||
&& enabledExplore == source.enabledExplore
|
||||
&& enabledCookieJar == source.enabledCookieJar
|
||||
&& equal(header, source.header)
|
||||
&& loginUrl == source.loginUrl
|
||||
&& equal(exploreUrl, source.exploreUrl)
|
||||
|
||||
@@ -24,6 +24,8 @@ data class HttpTTS(
|
||||
override var loginUrl: String? = null,
|
||||
override var loginUi: String? = null,
|
||||
override var header: String? = null,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
var loginCheckJs: String? = null,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
var lastUpdateTime: Long = System.currentTimeMillis()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
@@ -19,6 +20,8 @@ data class RssSource(
|
||||
var sourceGroup: String? = null,
|
||||
var sourceComment: String? = null,
|
||||
var enabled: Boolean = true,
|
||||
@ColumnInfo(defaultValue = "0")
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
override var concurrentRate: String? = null, //并发率
|
||||
override var header: String? = null, // 请求头
|
||||
override var loginUrl: String? = null, // 登录地址
|
||||
@@ -150,6 +153,7 @@ data class RssSource(
|
||||
style = doc.readString("$.style"),
|
||||
enableJs = doc.readBool("$.enableJs") ?: true,
|
||||
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
||||
enabledCookieJar = doc.readBool("$.enabledCookieJar") ?: false,
|
||||
customOrder = doc.readInt("$.customOrder") ?: 0
|
||||
)
|
||||
}
|
||||
|
||||
@@ -16,5 +16,6 @@ data class BookInfoRule(
|
||||
var coverUrl: String? = null,
|
||||
var tocUrl: String? = null,
|
||||
var wordCount: String? = null,
|
||||
var canReName: String? = null
|
||||
var canReName: String? = null,
|
||||
var downloadUrls: String? = null
|
||||
) : Parcelable
|
||||
@@ -12,6 +12,7 @@ import io.legado.app.help.config.ReadBookConfig
|
||||
import io.legado.app.utils.msg
|
||||
import io.legado.app.utils.replace
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CancellationException
|
||||
import splitties.init.appCtx
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
@@ -135,7 +136,7 @@ class ContentProcessor private constructor(
|
||||
var mContent = content
|
||||
getContentReplaceRules().forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
kotlin.runCatching {
|
||||
try {
|
||||
mContent = if (item.isRegex) {
|
||||
mContent.replace(
|
||||
item.pattern.toRegex(),
|
||||
@@ -145,18 +146,15 @@ class ContentProcessor private constructor(
|
||||
} else {
|
||||
mContent.replace(item.pattern, item.replacement)
|
||||
}
|
||||
}.onFailure {
|
||||
when (it) {
|
||||
is RegexTimeoutException -> {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
return item.name + it.msg
|
||||
}
|
||||
else -> {
|
||||
AppLog.put("${item.name}替换出错\n${it.localizedMessage}", it)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
} catch (e: RegexTimeoutException) {
|
||||
item.isEnabled = false
|
||||
appDb.replaceRuleDao.update(item)
|
||||
return item.name + e.msg
|
||||
} catch (e: CancellationException) {
|
||||
return mContent
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("${item.name}替换出错\n替换内容\n${mContent}", e)
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,12 +197,10 @@ interface JsExtensions {
|
||||
*js实现读取cookie
|
||||
*/
|
||||
fun getCookie(tag: String, key: String? = null): String {
|
||||
val cookie = CookieStore.getCookie(tag)
|
||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||
return if (key != null) {
|
||||
cookieMap[key] ?: ""
|
||||
CookieStore.getKey(tag, key)
|
||||
} else {
|
||||
cookie
|
||||
CookieStore.getCookie(tag)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -470,10 +468,8 @@ interface JsExtensions {
|
||||
* @return zip指定文件的数据
|
||||
*/
|
||||
fun getZipByteArrayContent(url: String, path: String): ByteArray? {
|
||||
val bytes = if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
runBlocking {
|
||||
return@runBlocking okHttpClient.newCallResponseBody { url(url) }.bytes()
|
||||
}
|
||||
val bytes = if (url.isAbsUrl()) {
|
||||
AnalyzeUrl(url, source = getSource()).getByteArray()
|
||||
} else {
|
||||
StringUtils.hexStringToByte(url)
|
||||
}
|
||||
@@ -517,7 +513,7 @@ interface JsExtensions {
|
||||
str.isAbsUrl() -> runBlocking {
|
||||
var x = CacheManager.getByteArray(key)
|
||||
if (x == null) {
|
||||
x = okHttpClient.newCallResponseBody { url(str) }.bytes()
|
||||
x = AnalyzeUrl(str, source = getSource()).getByteArray()
|
||||
x.let {
|
||||
CacheManager.put(key, it)
|
||||
}
|
||||
|
||||
@@ -1,43 +1,47 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.utils.*
|
||||
|
||||
object ReplaceAnalyzer {
|
||||
|
||||
fun jsonToReplaceRules(json: String): List<ReplaceRule> {
|
||||
val replaceRules = mutableListOf<ReplaceRule>()
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToReplaceRule(jsonItem.jsonString())?.let {
|
||||
if (it.isValid()) {
|
||||
replaceRules.add(it)
|
||||
fun jsonToReplaceRules(json: String): Result<MutableList<ReplaceRule>> {
|
||||
return kotlin.runCatching {
|
||||
val replaceRules = mutableListOf<ReplaceRule>()
|
||||
val items: List<Map<String, Any>> = jsonPath.parse(json).read("$")
|
||||
for (item in items) {
|
||||
val jsonItem = jsonPath.parse(item)
|
||||
jsonToReplaceRule(jsonItem.jsonString()).getOrThrow().let {
|
||||
if (it.isValid()) {
|
||||
replaceRules.add(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
replaceRules
|
||||
}
|
||||
return replaceRules
|
||||
}
|
||||
|
||||
private fun jsonToReplaceRule(json: String): ReplaceRule? {
|
||||
val replaceRule: ReplaceRule? = GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
||||
runCatching {
|
||||
fun jsonToReplaceRule(json: String): Result<ReplaceRule> {
|
||||
return runCatching {
|
||||
val replaceRule: ReplaceRule? =
|
||||
GSON.fromJsonObject<ReplaceRule>(json.trim()).getOrNull()
|
||||
if (replaceRule == null || replaceRule.pattern.isBlank()) {
|
||||
val jsonItem = jsonPath.parse(json.trim())
|
||||
val rule = ReplaceRule()
|
||||
rule.id = jsonItem.readLong("$.id") ?: System.currentTimeMillis()
|
||||
rule.pattern = jsonItem.readString("$.regex") ?: ""
|
||||
if (rule.pattern.isEmpty()) return null
|
||||
if (rule.pattern.isEmpty()) throw NoStackTraceException("格式不对")
|
||||
rule.name = jsonItem.readString("$.replaceSummary") ?: ""
|
||||
rule.replacement = jsonItem.readString("$.replacement") ?: ""
|
||||
rule.isRegex = jsonItem.readBool("$.isRegex") == true
|
||||
rule.scope = jsonItem.readString("$.useTo")
|
||||
rule.isEnabled = jsonItem.readBool("$.enable") == true
|
||||
rule.order = jsonItem.readInt("$.serialNumber") ?: 0
|
||||
return rule
|
||||
return@runCatching rule
|
||||
}
|
||||
return@runCatching replaceRule
|
||||
}
|
||||
return replaceRule
|
||||
}
|
||||
|
||||
}
|
||||
@@ -150,6 +150,7 @@ object SourceAnalyzer {
|
||||
source.customOrder = sourceAny.customOrder
|
||||
source.enabled = sourceAny.enabled
|
||||
source.enabledExplore = sourceAny.enabledExplore
|
||||
source.enabledCookieJar = sourceAny.enabledCookieJar
|
||||
source.concurrentRate = sourceAny.concurrentRate
|
||||
source.header = sourceAny.header
|
||||
source.loginUrl = when (sourceAny.loginUrl) {
|
||||
@@ -219,6 +220,7 @@ object SourceAnalyzer {
|
||||
var customOrder: Int = 0, // 手动排序编号
|
||||
var enabled: Boolean = true, // 是否启用
|
||||
var enabledExplore: Boolean = true, // 启用发现
|
||||
var enabledCookieJar: Boolean = false, // 启用CookieJar
|
||||
var concurrentRate: String? = null, // 并发率
|
||||
var header: String? = null, // 请求头
|
||||
var loginUrl: Any? = null, // 登录规则
|
||||
|
||||
@@ -32,7 +32,7 @@ object SourceVerificationHelp {
|
||||
startBrowser(source, url, title, true)
|
||||
}
|
||||
|
||||
var waitUserInput: Boolean = false
|
||||
var waitUserInput = false
|
||||
while(CacheManager.get(key) == null) {
|
||||
if (!waitUserInput) {
|
||||
AppLog.putDebug("等待返回验证结果...")
|
||||
|
||||
@@ -18,6 +18,7 @@ import org.apache.commons.text.StringEscapeUtils
|
||||
import splitties.init.appCtx
|
||||
import java.lang.ref.WeakReference
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.resumeWithException
|
||||
|
||||
/**
|
||||
* 后台webView
|
||||
@@ -50,14 +51,14 @@ class BackstageWebView(
|
||||
|
||||
override fun onError(error: Throwable) {
|
||||
if (!block.isCompleted)
|
||||
block.cancel(error)
|
||||
block.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
runOnUI {
|
||||
try {
|
||||
load()
|
||||
} catch (error: Throwable) {
|
||||
block.cancel(error)
|
||||
block.resumeWithException(error)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,8 +5,8 @@ package io.legado.app.help.http
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.data.appDb
|
||||
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.http.api.CookieManager
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
|
||||
object CookieStore : CookieManager {
|
||||
@@ -44,7 +44,7 @@ object CookieStore : CookieManager {
|
||||
CacheManager.getFromMemory("${domain}_cookie")?.let { return it }
|
||||
val cookieBean = appDb.cookieDao.get(domain)
|
||||
val cookie = cookieBean?.cookie ?: ""
|
||||
CacheManager.putMemory(url, cookie ?: "")
|
||||
CacheManager.putMemory(url, cookie)
|
||||
return cookie
|
||||
}
|
||||
|
||||
@@ -56,8 +56,9 @@ object CookieStore : CookieManager {
|
||||
|
||||
override fun removeCookie(url: String) {
|
||||
val domain = NetworkUtils.getSubDomain(url)
|
||||
CacheManager.deleteMemory("${domain}_cookie")
|
||||
appDb.cookieDao.delete(domain)
|
||||
CacheManager.deleteMemory("${domain}_cookie")
|
||||
android.webkit.CookieManager.getInstance().removeAllCookies(null)
|
||||
}
|
||||
|
||||
override fun cookieToMap(cookie: String): MutableMap<String, String> {
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
package io.legado.app.help.http
|
||||
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.cronet.CronetInterceptor
|
||||
import io.legado.app.help.http.cronet.CronetLoader
|
||||
import io.legado.app.utils.NetworkUtils
|
||||
import okhttp3.*
|
||||
import java.net.InetSocketAddress
|
||||
import java.net.Proxy
|
||||
@@ -23,7 +25,10 @@ val cookieJar by lazy {
|
||||
|
||||
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
|
||||
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 {
|
||||
val bookProgress = BookProgress(book)
|
||||
val json = GSON.toJson(bookProgress)
|
||||
val url = getProgressUrl(book)
|
||||
val url = getProgressUrl(book.name, book.author)
|
||||
WebDav(url, authorization).upload(json.toByteArray(), "application/json")
|
||||
}.onError {
|
||||
AppLog.put("上传进度失败\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
|
||||
private fun getProgressUrl(book: Book): String {
|
||||
return bookProgressUrl + book.name + "_" + book.author + ".json"
|
||||
fun uploadBookProgress(bookProgress: BookProgress) {
|
||||
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? {
|
||||
authorization?.let {
|
||||
val url = getProgressUrl(book)
|
||||
val url = getProgressUrl(book.name, book.author)
|
||||
kotlin.runCatching {
|
||||
WebDav(url, it).download().let { byteArray ->
|
||||
val json = String(byteArray)
|
||||
|
||||
@@ -98,9 +98,12 @@ object ImportOldData {
|
||||
}
|
||||
|
||||
private fun importOldReplaceRule(json: String): Int {
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(json)
|
||||
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
||||
return rules.size
|
||||
val rules = ReplaceAnalyzer.jsonToReplaceRules(json).getOrNull()
|
||||
rules?.let {
|
||||
appDb.replaceRuleDao.insert(*rules.toTypedArray())
|
||||
return rules.size
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
private fun fromOldBooks(json: String): List<Book> {
|
||||
|
||||
@@ -22,40 +22,36 @@ import kotlinx.coroutines.withContext
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileOutputStream
|
||||
|
||||
|
||||
object Restore {
|
||||
|
||||
suspend fun restore(context: Context, path: String) {
|
||||
withContext(IO) {
|
||||
kotlin.runCatching {
|
||||
if (path.isContentScheme()) {
|
||||
DocumentFile.fromTreeUri(context, Uri.parse(path))?.listFiles()?.forEach { doc ->
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
if (doc.name == fileName) {
|
||||
DocumentUtils.readText(context, doc.uri).let {
|
||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName")
|
||||
.writeText(it)
|
||||
if (Backup.backupFileNames.contains(doc.name)) {
|
||||
context.contentResolver.openInputStream(doc.uri)?.use { inputStream ->
|
||||
val file = File("${Backup.backupPath}${File.separator}${doc.name}")
|
||||
FileOutputStream(file).use { outputStream ->
|
||||
inputStream.copyTo(outputStream)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
val file = File(path)
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
file.getFile(fileName).let {
|
||||
if (it.exists()) {
|
||||
it.copyTo(
|
||||
FileUtils.createFileIfNotExist("${Backup.backupPath}${File.separator}$fileName"),
|
||||
true
|
||||
)
|
||||
}
|
||||
}
|
||||
val dir = File(path)
|
||||
for (fileName in Backup.backupFileNames) {
|
||||
val file = dir.getFile(fileName)
|
||||
if (file.exists()) {
|
||||
val target = File("${Backup.backupPath}${File.separator}$fileName")
|
||||
file.copyTo(target, true)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
AppLog.put("恢复复制文件出错\n${it.localizedMessage}", it)
|
||||
}
|
||||
restoreDatabase()
|
||||
restoreConfig()
|
||||
@@ -129,7 +125,7 @@ object Restore {
|
||||
ThemeConfig.upConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复主题出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
if (!BackupConfig.ignoreReadConfig) {
|
||||
//恢复阅读界面配置
|
||||
@@ -142,7 +138,7 @@ object Restore {
|
||||
ReadBookConfig.initConfigs()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
try {
|
||||
val file =
|
||||
@@ -153,7 +149,7 @@ object Restore {
|
||||
ReadBookConfig.initShareConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printOnDebug()
|
||||
AppLog.put("恢复阅读界面出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
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.ContextWrapper
|
||||
@@ -118,7 +118,7 @@ class ColorPreference(context: Context, attrs: AttributeSet) : Preference(contex
|
||||
}
|
||||
|
||||
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,
|
||||
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.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.EditTextPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
||||
@@ -23,6 +25,13 @@ class EditTextPreferenceDialog : EditTextPreferenceDialogFragmentCompat() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
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
|
||||
}
|
||||
|
||||
+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.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.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.ListPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
||||
@@ -23,6 +25,13 @@ class ListPreferenceDialog : ListPreferenceDialogFragmentCompat() {
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
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
|
||||
}
|
||||
|
||||
+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.os.Bundle
|
||||
import androidx.appcompat.app.AlertDialog
|
||||
import androidx.preference.MultiSelectListPreferenceDialogFragmentCompat
|
||||
import androidx.preference.PreferenceDialogFragmentCompat
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.lib.theme.filletBackground
|
||||
|
||||
class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentCompat() {
|
||||
@@ -25,6 +27,13 @@ class MultiSelectListPreferenceDialog : MultiSelectListPreferenceDialogFragmentC
|
||||
override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
|
||||
val dialog = super.onCreateDialog(savedInstanceState)
|
||||
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
|
||||
}
|
||||
|
||||
+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.util.AttributeSet
|
||||
@@ -25,14 +25,8 @@ class NameListPreference(context: Context, attrs: AttributeSet) : ListPreference
|
||||
|
||||
override fun onBindViewHolder(holder: PreferenceViewHolder) {
|
||||
val v = Preference.bindView<TextView>(
|
||||
context,
|
||||
holder,
|
||||
icon,
|
||||
title,
|
||||
summary,
|
||||
widgetLayoutResource,
|
||||
R.id.text_view,
|
||||
isBottomBackground = isBottomBackground
|
||||
context, holder, icon, title, summary, widgetLayoutResource,
|
||||
R.id.text_view, isBottomBackground = isBottomBackground
|
||||
)
|
||||
if (v is TextView) {
|
||||
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.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.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.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 androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.*
|
||||
import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.ListPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
|
||||
import io.legado.app.lib.prefs.EditTextPreferenceDialog
|
||||
import io.legado.app.lib.prefs.ListPreferenceDialog
|
||||
import io.legado.app.lib.prefs.MultiSelectListPreferenceDialog
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
abstract class PreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
|
||||
|
||||
@@ -34,7 +34,7 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
return
|
||||
}
|
||||
|
||||
val f: DialogFragment = when (preference) {
|
||||
val dialogFragment: DialogFragment = when (preference) {
|
||||
is EditTextPreference -> {
|
||||
EditTextPreferenceDialog.newInstance(preference.getKey())
|
||||
}
|
||||
@@ -54,10 +54,9 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
}
|
||||
}
|
||||
@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 loginUi: String? = null,
|
||||
override var header: String? = null,
|
||||
override var enabledCookieJar: Boolean? = false,
|
||||
) : BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
|
||||
@@ -131,9 +131,9 @@ object CacheBook {
|
||||
|
||||
class CacheBookModel(var bookSource: BookSource, var book: Book) {
|
||||
|
||||
private val waitDownloadSet = hashSetOf<Int>()
|
||||
private val onDownloadSet = hashSetOf<Int>()
|
||||
private val successDownloadSet = hashSetOf<Int>()
|
||||
private val waitDownloadSet = linkedSetOf<Int>()
|
||||
private val onDownloadSet = linkedSetOf<Int>()
|
||||
private val successDownloadSet = linkedSetOf<Int>()
|
||||
private val errorDownloadMap = hashMapOf<Int, Int>()
|
||||
|
||||
val waitCount get() = waitDownloadSet.size
|
||||
@@ -174,7 +174,7 @@ object CacheBook {
|
||||
}
|
||||
onDownloadSet.remove(index)
|
||||
//重试3次
|
||||
if (errorDownloadMap[index] ?: 0 < 3) {
|
||||
if ((errorDownloadMap[index] ?: 0) < 3) {
|
||||
waitDownloadSet.add(index)
|
||||
} else {
|
||||
AppLog.put(
|
||||
|
||||
@@ -2,6 +2,7 @@ package io.legado.app.model
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.*
|
||||
import io.legado.app.help.coroutine.CompositeCoroutine
|
||||
import io.legado.app.model.rss.Rss
|
||||
@@ -238,7 +239,11 @@ object Debug {
|
||||
.onSuccess {
|
||||
log(debugSource, "︽详情页解析完成")
|
||||
log(debugSource, showTime = false)
|
||||
tocDebug(scope, bookSource, book)
|
||||
if (book.type != BookType.file) {
|
||||
tocDebug(scope, bookSource, book)
|
||||
} else {
|
||||
log(debugSource, "≡文件类书源跳过解析目录", state = 1000)
|
||||
}
|
||||
}
|
||||
.onError {
|
||||
log(debugSource, it.msg, state = -1)
|
||||
|
||||
@@ -69,6 +69,7 @@ class AnalyzeUrl(
|
||||
private var retry: Int = 0
|
||||
private var useWebView: Boolean = false
|
||||
private var webJs: String? = null
|
||||
private val enabledCookieJar = source?.enabledCookieJar ?: false
|
||||
|
||||
init {
|
||||
if (!mUrl.isDataUrl()) {
|
||||
@@ -519,17 +520,26 @@ class AnalyzeUrl(
|
||||
}
|
||||
|
||||
/**
|
||||
*设置cookie urlOption的优先级大于书源保存的cookie
|
||||
*设置cookie 优先级
|
||||
* urlOption临时cookie > 数据库cookie = okhttp CookieJar保存在内存中的cookie
|
||||
*@param tag 书源url 缺省为传入的url
|
||||
*/
|
||||
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()) {
|
||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||
val customCookieMap = CookieStore.cookieToMap(headerMap["Cookie"] ?: "")
|
||||
cookieMap.putAll(customCookieMap)
|
||||
val newCookie = CookieStore.mapToCookie(cookieMap)
|
||||
newCookie?.let {
|
||||
CookieStore.mapToCookie(cookieMap)?.let {
|
||||
headerMap.put("Cookie", it)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
package io.legado.app.model.localBook
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import com.script.SimpleBindings
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.exception.TocEmptyException
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.utils.*
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
import java.io.FileInputStream
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.InputStream
|
||||
import java.io.*
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* 书籍文件导入 目录正文解析
|
||||
* 支持在线文件(txt epub umd 压缩文件需要用户解压) 本地文件
|
||||
*/
|
||||
object LocalBook {
|
||||
|
||||
private val nameAuthorPatterns = arrayOf(
|
||||
@@ -41,6 +46,20 @@ object LocalBook {
|
||||
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)
|
||||
fun getChapterList(book: Book): ArrayList<BookChapter> {
|
||||
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 {
|
||||
val bookUrl: String
|
||||
val updateTime: Long
|
||||
@@ -119,7 +155,10 @@ object LocalBook {
|
||||
return book
|
||||
}
|
||||
|
||||
fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
||||
/**
|
||||
* 从文件分析书籍必要信息(书名 作者等)
|
||||
*/
|
||||
private fun analyzeNameAuthor(fileName: String): Pair<String, String> {
|
||||
val tempFileName = fileName.substringBeforeLast(".")
|
||||
var name: 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 本地书籍解析接口
|
||||
* LocalBook.kt 总入口
|
||||
* LocalBook.kt 导入解析总入口
|
||||
* TextFile.kt 解析txt
|
||||
* EpubFile.kt 解析epub
|
||||
* UmdFile.kt 解析umd
|
||||
@@ -1,6 +1,8 @@
|
||||
package io.legado.app.model.webBook
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
@@ -137,14 +139,29 @@ object BookInfo {
|
||||
Debug.log(bookSource.bookSourceUrl, "└${e.localizedMessage}")
|
||||
DebugLog.e("获取封面出错", e)
|
||||
}
|
||||
scope.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||
if (book.tocUrl == baseUrl) {
|
||||
book.tocHtml = body
|
||||
if (book.type != BookType.file) {
|
||||
scope.ensureActive()
|
||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录链接")
|
||||
book.tocUrl = analyzeRule.getString(infoRule.tocUrl, isUrl = true)
|
||||
if (book.tocUrl.isEmpty()) book.tocUrl = baseUrl
|
||||
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.base.BaseService
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.IntentAction
|
||||
import io.legado.app.data.appDb
|
||||
@@ -208,7 +209,9 @@ class CheckSourceService : BaseService() {
|
||||
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 nextChapterUrl = toc.getOrNull(1)?.url ?: toc.first().url
|
||||
//校验正文
|
||||
|
||||
@@ -38,7 +38,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
private val ttsFolderPath: String by lazy {
|
||||
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 playIndexJob: Job? = null
|
||||
private var downloadTaskIsActive = false
|
||||
@@ -301,7 +301,7 @@ class HttpReadAloudService : BaseReadAloudService(),
|
||||
override fun upSpeechRate(reset: Boolean) {
|
||||
downloadTask?.cancel()
|
||||
exoPlayer.stop()
|
||||
speechRate = AppConfig.speechRatePlay
|
||||
speechRate = AppConfig.speechRatePlay + 5
|
||||
downloadAudio()
|
||||
}
|
||||
|
||||
|
||||
@@ -2,16 +2,7 @@ package io.legado.app.ui.association
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import io.legado.app.R
|
||||
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) {
|
||||
|
||||
@@ -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) {
|
||||
execute {
|
||||
val mText = 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))
|
||||
}
|
||||
importSourceAwait(text.trim())
|
||||
}.onError {
|
||||
it.printOnDebug()
|
||||
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) {
|
||||
okHttpClient.newCallResponseBody {
|
||||
url(url)
|
||||
}.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()
|
||||
upSelectText()
|
||||
}
|
||||
viewModel.errorLiveData.observe(this, {
|
||||
viewModel.errorLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
binding.tvMsg.apply {
|
||||
text = it
|
||||
visible()
|
||||
}
|
||||
})
|
||||
viewModel.successLiveData.observe(this, {
|
||||
}
|
||||
viewModel.successLiveData.observe(this) {
|
||||
binding.rotateLoading.hide()
|
||||
if (it > 0) {
|
||||
adapter.setItems(viewModel.allRules)
|
||||
@@ -106,7 +106,7 @@ class ImportReplaceRuleDialog() : BaseDialogFragment(R.layout.dialog_recycler_vi
|
||||
visible()
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
val source = arguments?.getString("source")
|
||||
if (source.isNullOrEmpty()) {
|
||||
dismiss()
|
||||
|
||||
@@ -6,12 +6,15 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
import io.legado.app.help.ReplaceAnalyzer
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.http.newCallResponseBody
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.help.http.text
|
||||
import io.legado.app.utils.isAbsUrl
|
||||
import io.legado.app.utils.isJsonArray
|
||||
import io.legado.app.utils.isJsonObject
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
|
||||
class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
@@ -83,17 +86,7 @@ class ImportReplaceRuleViewModel(app: Application) : BaseViewModel(app) {
|
||||
|
||||
fun import(text: String) {
|
||||
execute {
|
||||
if (text.isAbsUrl()) {
|
||||
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)
|
||||
}
|
||||
importAwait(text.trim())
|
||||
}.onError {
|
||||
errorLiveData.postValue(it.localizedMessage ?: "ERROR")
|
||||
}.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() {
|
||||
execute {
|
||||
allRules.forEach {
|
||||
|
||||
@@ -13,6 +13,7 @@ import androidx.activity.viewModels
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.VMBaseActivity
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.data.appDb
|
||||
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.model.BookCover
|
||||
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.changecover.ChangeCoverDialog
|
||||
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))
|
||||
}
|
||||
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 -> {
|
||||
viewModel.bookData.value?.let {
|
||||
@@ -293,8 +295,14 @@ class BookInfoActivity :
|
||||
true
|
||||
}
|
||||
tvRead.setOnClickListener {
|
||||
viewModel.bookData.value?.let {
|
||||
readBook(it)
|
||||
viewModel.bookData.value?.let { book ->
|
||||
if (viewModel.isImportBookOnLine) {
|
||||
showDialogFragment<ImportOnLineBookFileDialog> {
|
||||
putString("bookUrl", book.bookUrl)
|
||||
}
|
||||
} else {
|
||||
readBook(book)
|
||||
}
|
||||
} ?: toastOnUi("Book is null")
|
||||
}
|
||||
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.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.lifecycle.MutableLiveData
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
@@ -30,6 +32,7 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
var inBookshelf = false
|
||||
var bookSource: BookSource? = null
|
||||
private var changeSourceCoroutine: Coroutine<*>? = null
|
||||
var isImportBookOnLine = false
|
||||
|
||||
fun initData(intent: Intent) {
|
||||
execute {
|
||||
@@ -73,8 +76,11 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
upCoverByRule(book)
|
||||
bookSource = if (book.isLocalBook()) null else
|
||||
appDb.bookSourceDao.getBookSource(book.origin)
|
||||
isImportBookOnLine = (bookSource?.bookSourceType ?: BookType.local) == BookType.file
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else if (isImportBookOnLine) {
|
||||
chapterListData.postValue(emptyList())
|
||||
} else {
|
||||
val chapterList = appDb.bookChapterDao.getChapterList(book.bookUrl)
|
||||
if (chapterList.isNotEmpty()) {
|
||||
@@ -113,6 +119,9 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
WebBook.getBookInfo(this, bookSource, book, canReName = canReName)
|
||||
.onSuccess(IO) {
|
||||
bookData.postValue(book)
|
||||
if (isImportBookOnLine) {
|
||||
appDb.searchBookDao.update(book.toSearchBook())
|
||||
}
|
||||
if (inBookshelf) {
|
||||
appDb.bookDao.update(book)
|
||||
}
|
||||
@@ -141,6 +150,8 @@ class BookInfoViewModel(application: Application) : BaseViewModel(application) {
|
||||
appDb.bookChapterDao.insert(*it.toTypedArray())
|
||||
chapterListData.postValue(it)
|
||||
}
|
||||
} else if (isImportBookOnLine) {
|
||||
chapterListData.postValue(emptyList())
|
||||
} else {
|
||||
bookSource?.let { bookSource ->
|
||||
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 }
|
||||
yesButton {
|
||||
alertBinding.run {
|
||||
val start = editStart.text?.toString()?.toInt() ?: 0
|
||||
val end = editEnd.text?.toString()?.toInt() ?: book.totalChapterNum
|
||||
val start = editStart.text!!.toString().let {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -61,41 +61,33 @@ class ReadBookViewModel(application: Application) : BaseViewModel(application) {
|
||||
}
|
||||
|
||||
private fun initBook(book: Book) {
|
||||
if (ReadBook.book?.bookUrl != book.bookUrl) {
|
||||
ReadBook.resetData(book)
|
||||
isInitFinish = true
|
||||
if (ReadBook.chapterSize == 0) {
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else {
|
||||
loadChapterList(book)
|
||||
}
|
||||
val isSameBook = ReadBook.book?.bookUrl == book.bookUrl
|
||||
if (isSameBook) ReadBook.upData(book) else ReadBook.resetData(book)
|
||||
isInitFinish = true
|
||||
if (ReadBook.chapterSize == 0) {
|
||||
if (book.tocUrl.isEmpty()) {
|
||||
loadBookInfo(book)
|
||||
} else {
|
||||
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 {
|
||||
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
||||
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
||||
}
|
||||
ReadBook.loadContent(resetPageOffset = true)
|
||||
}
|
||||
syncBookProgress(book)
|
||||
} else {
|
||||
ReadBook.upData(book)
|
||||
isInitFinish = true
|
||||
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)
|
||||
if (ReadBook.durChapterIndex > ReadBook.chapterSize - 1) {
|
||||
ReadBook.durChapterIndex = ReadBook.chapterSize - 1
|
||||
}
|
||||
ReadBook.loadContent(resetPageOffset = isSameBook)
|
||||
}
|
||||
if (!isSameBook || !BaseReadAloudService.isRun) {
|
||||
syncBookProgress(book)
|
||||
}
|
||||
if (!book.isLocalBook() && ReadBook.bookSource == null) {
|
||||
autoChangeSource(book.name, book.author)
|
||||
|
||||
@@ -9,10 +9,10 @@ import android.widget.LinearLayout
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
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.primaryColor
|
||||
import io.legado.app.model.ReadBook
|
||||
@@ -67,7 +67,7 @@ class MoreConfigDialog : DialogFragment() {
|
||||
(activity as ReadBookActivity).bottomDialog--
|
||||
}
|
||||
|
||||
class ReadPreferenceFragment : BasePreferenceFragment(),
|
||||
class ReadPreferenceFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
|
||||
@@ -10,12 +10,12 @@ import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.IntentHelp
|
||||
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.primaryColor
|
||||
import io.legado.app.model.ReadAloud
|
||||
@@ -54,7 +54,7 @@ class ReadAloudConfigDialog : DialogFragment() {
|
||||
.commit()
|
||||
}
|
||||
|
||||
class ReadAloudPreferenceFragment : BasePreferenceFragment(),
|
||||
class ReadAloudPreferenceFragment : PreferenceFragment(),
|
||||
SpeakEngineDialog.CallBack,
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import io.legado.app.databinding.ActivityBookSourceEditBinding
|
||||
import io.legado.app.help.config.LocalConfig
|
||||
import io.legado.app.lib.dialogs.SelectItem
|
||||
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.primaryColor
|
||||
import io.legado.app.ui.book.source.debug.BookSourceDebugActivity
|
||||
@@ -47,7 +48,7 @@ class BookSourceEditActivity :
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it ?: return@registerForActivityResult
|
||||
viewModel.importSource(it) { source ->
|
||||
upRecyclerView(source)
|
||||
upSourceView(source)
|
||||
}
|
||||
}
|
||||
private val selectDoc = registerForActivityResult(HandleFileContract()) {
|
||||
@@ -68,7 +69,7 @@ class BookSourceEditActivity :
|
||||
softKeyboardTool.attachToWindow(window)
|
||||
initView()
|
||||
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_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_share_str -> share(GSON.toJson(getSource()))
|
||||
R.id.menu_share_qr -> shareWithQr(
|
||||
@@ -139,6 +141,7 @@ class BookSourceEditActivity :
|
||||
binding.recyclerView.layoutManager = LinearLayoutManager(this)
|
||||
binding.recyclerView.adapter = adapter
|
||||
binding.tabLayout.setBackgroundColor(backgroundColor)
|
||||
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
|
||||
override fun onTabReselected(tab: TabLayout.Tab?) {
|
||||
|
||||
@@ -186,12 +189,14 @@ class BookSourceEditActivity :
|
||||
binding.recyclerView.scrollToPosition(0)
|
||||
}
|
||||
|
||||
private fun upRecyclerView(source: BookSource? = viewModel.bookSource) {
|
||||
private fun upSourceView(source: BookSource? = viewModel.bookSource) {
|
||||
source?.let {
|
||||
binding.cbIsEnable.isChecked = it.enabled
|
||||
binding.cbIsEnableFind.isChecked = it.enabledExplore
|
||||
binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
|
||||
binding.spType.setSelection(
|
||||
when (it.bookSourceType) {
|
||||
BookType.file -> 3
|
||||
BookType.image -> 2
|
||||
BookType.audio -> 1
|
||||
else -> 0
|
||||
@@ -261,6 +266,7 @@ class BookSourceEditActivity :
|
||||
add(EditEntity("coverUrl", ir?.coverUrl, R.string.rule_cover_url))
|
||||
add(EditEntity("tocUrl", ir?.tocUrl, R.string.rule_toc_url))
|
||||
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()
|
||||
@@ -295,7 +301,9 @@ class BookSourceEditActivity :
|
||||
val source = viewModel.bookSource?.copy() ?: BookSource()
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.enabledExplore = binding.cbIsEnableFind.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
source.bookSourceType = when (binding.spType.selectedItemPosition) {
|
||||
3 -> BookType.file
|
||||
2 -> BookType.image
|
||||
1 -> BookType.audio
|
||||
else -> BookType.default
|
||||
@@ -389,6 +397,7 @@ class BookSourceEditActivity :
|
||||
"tocUrl" -> bookInfoRule.tocUrl =
|
||||
viewModel.ruleComplete(it.value, bookInfoRule.init, 2)
|
||||
"canReName" -> bookInfoRule.canReName = it.value
|
||||
"downloadUrls" -> bookInfoRule.downloadUrls = viewModel.ruleComplete(it.value, bookInfoRule.init)
|
||||
}
|
||||
}
|
||||
tocEntities.forEach {
|
||||
|
||||
@@ -7,6 +7,7 @@ import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.exception.NoStackTraceException
|
||||
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.okHttpClient
|
||||
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? {
|
||||
if (autoComplete) {
|
||||
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.visible
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.isActive
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.Dispatchers.Main
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.ensureActive
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
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
|
||||
|
||||
override fun onCurrentListChanged() {
|
||||
super.onCurrentListChanged()
|
||||
upDisplayTitle()
|
||||
callback.onListChanged()
|
||||
}
|
||||
|
||||
@@ -73,38 +68,52 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
displayTitleMap.clear()
|
||||
}
|
||||
|
||||
fun upDisplayTitle() {
|
||||
fun upDisplayTitles(startIndex: Int) {
|
||||
upDisplayTileJob?.cancel()
|
||||
upDisplayTileJob = Coroutine.async(callback.scope) {
|
||||
val replaceRules = replaceRules
|
||||
val useReplace = useReplace
|
||||
getItems().forEach {
|
||||
if (!isActive) {
|
||||
return@async
|
||||
val book = callback.book ?: return@async
|
||||
val replaceRules = ContentProcessor.get(book.name, book.origin).getTitleReplaceRules()
|
||||
val useReplace = AppConfig.tocUiUseReplace && book.getUseReplaceRule()
|
||||
val items = getItems()
|
||||
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) {
|
||||
displayTitleMap[it.title] = it.getDisplayTitle(replaceRules, useReplace)
|
||||
}.start()
|
||||
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 {
|
||||
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(
|
||||
holder: ItemViewHolder,
|
||||
binding: ItemChapterListBinding,
|
||||
@@ -120,7 +129,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
} else {
|
||||
tvChapterName.setTextColor(context.getCompatColor(R.color.primaryText))
|
||||
}
|
||||
tvChapterName.text = getDisplayTile(item)
|
||||
tvChapterName.text = getDisplayTitle(item)
|
||||
if (item.isVolume) {
|
||||
//卷名,如第一卷 突出显示
|
||||
tvChapterItem.setBackgroundColor(context.getCompatColor(R.color.btn_bg_press))
|
||||
@@ -138,6 +147,7 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
}
|
||||
upHasCache(binding, isDur, cached)
|
||||
} else {
|
||||
tvChapterName.text = getDisplayTitle(item)
|
||||
upHasCache(binding, isDur, cached)
|
||||
}
|
||||
}
|
||||
@@ -150,8 +160,8 @@ class ChapterListAdapter(context: Context, val callback: Callback) :
|
||||
}
|
||||
}
|
||||
holder.itemView.setOnLongClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
context.longToastOnUi(getDisplayTile(it))
|
||||
getItem(holder.layoutPosition)?.let { item ->
|
||||
context.longToastOnUi(getDisplayTitle(item))
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -97,7 +97,15 @@ class ChapterListFragment : VMBaseFragment<TocViewModel>(R.layout.fragment_chapt
|
||||
viewModel.bookData.value?.bookUrl?.let { bookUrl ->
|
||||
if (chapter.bookUrl == bookUrl) {
|
||||
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)
|
||||
adapter.upDisplayTitles(scrollPos)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearDisplayTitle() {
|
||||
adapter.clearDisplayTitle()
|
||||
adapter.upDisplayTitle()
|
||||
adapter.upDisplayTitles(mLayoutManager.findFirstVisibleItemPosition())
|
||||
}
|
||||
|
||||
override val scope: CoroutineScope
|
||||
|
||||
@@ -59,10 +59,12 @@ class TocActivity : VMBaseActivity<ActivityChapterListBinding, TocViewModel>() {
|
||||
setOnSearchClickListener { tabLayout.gone() }
|
||||
setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||
override fun onQueryTextSubmit(query: String): Boolean {
|
||||
viewModel.searchKey = query
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onQueryTextChange(newText: String): Boolean {
|
||||
viewModel.searchKey = newText
|
||||
if (tabLayout.selectedTabPosition == 1) {
|
||||
viewModel.startBookmarkSearch(newText)
|
||||
} else {
|
||||
|
||||
@@ -12,6 +12,7 @@ class TocViewModel(application: Application) : BaseViewModel(application) {
|
||||
var bookData = MutableLiveData<Book>()
|
||||
var chapterListCallBack: ChapterListCallBack? = null
|
||||
var bookMarkCallBack: BookmarkCallBack? = null
|
||||
var searchKey: String? = null
|
||||
|
||||
fun initBook(bookUrl: String) {
|
||||
this.bookUrl = bookUrl
|
||||
|
||||
@@ -15,7 +15,6 @@ import androidx.preference.EditTextPreference
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.PreferKey
|
||||
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.permission.Permissions
|
||||
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.ui.document.HandleFileContract
|
||||
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.launch
|
||||
import splitties.init.appCtx
|
||||
import kotlin.collections.set
|
||||
|
||||
class BackupConfigFragment : BasePreferenceFragment(),
|
||||
class BackupConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||
@@ -100,34 +100,18 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
||||
|
||||
override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
|
||||
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 {
|
||||
it.setOnBindEditTextListener { editText ->
|
||||
editText.applyTint(requireContext().accentColor)
|
||||
editText.inputType =
|
||||
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.webDavAccount, getPrefString(PreferKey.webDavAccount))
|
||||
upPreferenceSummary(PreferKey.webDavPassword, getPrefString(PreferKey.webDavPassword))
|
||||
upPreferenceSummary(PreferKey.webDavDir, AppConfig.webDavDir)
|
||||
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 }
|
||||
}
|
||||
|
||||
@@ -226,7 +210,9 @@ class BackupConfigFragment : BasePreferenceFragment(),
|
||||
return super.onPreferenceTreeClick(preference)
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 备份忽略设置
|
||||
*/
|
||||
private fun backupIgnore() {
|
||||
val checkedItems = BooleanArray(BackupConfig.ignoreKeys.size) {
|
||||
BackupConfig.ignoreConfig[BackupConfig.ignoreKeys[it]] ?: false
|
||||
|
||||
@@ -7,16 +7,16 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.PreferKey
|
||||
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.model.BookCover
|
||||
import io.legado.app.ui.widget.prefs.SwitchPreference
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class CoverConfigFragment : BasePreferenceFragment(),
|
||||
class CoverConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestCodeCover = 111
|
||||
|
||||
@@ -6,16 +6,17 @@ import android.content.SharedPreferences
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.fragment.app.activityViewModels
|
||||
import androidx.preference.ListPreference
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.databinding.DialogEditTextBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
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.model.CheckSource
|
||||
import io.legado.app.receiver.SharedReceiverActivity
|
||||
@@ -26,7 +27,7 @@ import io.legado.app.utils.*
|
||||
import splitties.init.appCtx
|
||||
|
||||
|
||||
class OtherConfigFragment : BasePreferenceFragment(),
|
||||
class OtherConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val viewModel by activityViewModels<ConfigViewModel>()
|
||||
@@ -131,9 +132,9 @@ class OtherConfigFragment : BasePreferenceFragment(),
|
||||
setProcessTextEnable(it.getBoolean(key, true))
|
||||
}
|
||||
PreferKey.showDiscovery, PreferKey.showRss -> postEvent(EventBus.NOTIFY_MAIN, true)
|
||||
PreferKey.language -> listView.postDelayed({
|
||||
PreferKey.language -> listView.postDelayed(1000) {
|
||||
appCtx.restart()
|
||||
}, 1000)
|
||||
}
|
||||
PreferKey.userAgent -> listView.post {
|
||||
upPreferenceSummary(PreferKey.userAgent, AppConfig.userAgent)
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import android.widget.SeekBar
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.EventBus
|
||||
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.lib.dialogs.alert
|
||||
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.ui.widget.number.NumberPickerDialog
|
||||
import io.legado.app.ui.widget.prefs.ColorPreference
|
||||
import io.legado.app.ui.widget.seekbar.SeekBarChangeListener
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
|
||||
@Suppress("SameParameterValue")
|
||||
class ThemeConfigFragment : BasePreferenceFragment(),
|
||||
class ThemeConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestCodeBgLight = 121
|
||||
|
||||
@@ -7,15 +7,15 @@ import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.PreferKey
|
||||
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.model.BookCover
|
||||
import io.legado.app.utils.*
|
||||
import java.io.FileOutputStream
|
||||
|
||||
class WelcomeConfigFragment : BasePreferenceFragment(),
|
||||
class WelcomeConfigFragment : PreferenceFragment(),
|
||||
SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
|
||||
private val requestWelcomeImage = 221
|
||||
|
||||
@@ -8,6 +8,7 @@ import android.view.KeyEvent
|
||||
import android.view.MenuItem
|
||||
import android.view.ViewGroup
|
||||
import androidx.activity.viewModels
|
||||
import androidx.core.view.postDelayed
|
||||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter
|
||||
@@ -87,13 +88,13 @@ class MainActivity : VMBaseActivity<ActivityMainBinding, MainViewModel>(),
|
||||
upVersion()
|
||||
//自动更新书籍
|
||||
if (AppConfig.autoRefreshBook) {
|
||||
binding.viewPagerMain.postDelayed({
|
||||
binding.viewPagerMain.postDelayed(1000) {
|
||||
viewModel.upAllBookToc()
|
||||
}, 1000)
|
||||
}
|
||||
}
|
||||
binding.viewPagerMain.postDelayed({
|
||||
binding.viewPagerMain.postDelayed(3000) {
|
||||
viewModel.postLoad()
|
||||
}, 3000)
|
||||
}
|
||||
launch {
|
||||
val lastBackupFile = withContext(IO) { AppWebDav.lastBackUp().getOrNull() }
|
||||
?: return@launch
|
||||
|
||||
@@ -8,13 +8,16 @@ import android.view.View
|
||||
import androidx.preference.Preference
|
||||
import io.legado.app.R
|
||||
import io.legado.app.base.BaseFragment
|
||||
import io.legado.app.base.BasePreferenceFragment
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.databinding.FragmentMyConfigBinding
|
||||
import io.legado.app.help.config.AppConfig
|
||||
import io.legado.app.help.config.ThemeConfig
|
||||
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.service.WebService
|
||||
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.replace.ReplaceRuleActivity
|
||||
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.viewbindingdelegate.viewBinding
|
||||
|
||||
@@ -40,7 +40,7 @@ class MyFragment : BaseFragment(R.layout.fragment_my_config) {
|
||||
setSupportToolbar(binding.titleBar.toolbar)
|
||||
val fragmentTag = "prefFragment"
|
||||
var preferenceFragment = childFragmentManager.findFragmentByTag(fragmentTag)
|
||||
if (preferenceFragment == null) preferenceFragment = PreferenceFragment()
|
||||
if (preferenceFragment == null) preferenceFragment = MyPreferenceFragment()
|
||||
childFragmentManager.beginTransaction()
|
||||
.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 {
|
||||
|
||||
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.DialogEditTextBinding
|
||||
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.rss.source.edit.RssSourceEditActivity
|
||||
import io.legado.app.utils.StartActivityContract
|
||||
@@ -45,9 +46,10 @@ class RssSortActivity : VMBaseActivity<ActivityRssArtivlesBinding, RssSortViewMo
|
||||
override fun onActivityCreated(savedInstanceState: Bundle?) {
|
||||
binding.viewPager.adapter = adapter
|
||||
binding.tabLayout.setupWithViewPager(binding.viewPager)
|
||||
viewModel.titleLiveData.observe(this, {
|
||||
binding.tabLayout.setSelectedTabIndicatorColor(accentColor)
|
||||
viewModel.titleLiveData.observe(this) {
|
||||
binding.titleBar.title = it
|
||||
})
|
||||
}
|
||||
viewModel.initData(intent) {
|
||||
upFragments()
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ class RssSourceEditActivity :
|
||||
private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
|
||||
it?.let {
|
||||
viewModel.importSource(it) { source: RssSource ->
|
||||
upRecyclerView(source)
|
||||
upSourceView(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -57,7 +57,7 @@ class RssSourceEditActivity :
|
||||
softKeyboardTool.attachToWindow(window)
|
||||
initView()
|
||||
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_copy_source -> sendToClip(GSON.toJson(getRssSource()))
|
||||
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_qr -> shareWithQr(
|
||||
GSON.toJson(getRssSource()),
|
||||
@@ -150,10 +151,11 @@ class RssSourceEditActivity :
|
||||
binding.recyclerView.adapter = adapter
|
||||
}
|
||||
|
||||
private fun upRecyclerView(source: RssSource? = viewModel.rssSource) {
|
||||
private fun upSourceView(source: RssSource? = viewModel.rssSource) {
|
||||
source?.let {
|
||||
binding.cbIsEnable.isChecked = source.enabled
|
||||
binding.cbSingleUrl.isChecked = source.singleUrl
|
||||
binding.cbIsEnableCookie.isChecked = source.enabledCookieJar == true
|
||||
binding.cbEnableJs.isChecked = source.enableJs
|
||||
binding.cbEnableBaseUrl.isChecked = source.loadWithBaseUrl
|
||||
}
|
||||
@@ -191,6 +193,7 @@ class RssSourceEditActivity :
|
||||
val source = viewModel.rssSource
|
||||
source.enabled = binding.cbIsEnable.isChecked
|
||||
source.singleUrl = binding.cbSingleUrl.isChecked
|
||||
source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
|
||||
source.enableJs = binding.cbEnableJs.isChecked
|
||||
source.loadWithBaseUrl = binding.cbEnableBaseUrl.isChecked
|
||||
sourceEntities.forEach {
|
||||
|
||||
@@ -6,6 +6,7 @@ import io.legado.app.base.BaseViewModel
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.help.RuleComplete
|
||||
import io.legado.app.help.http.CookieStore
|
||||
import io.legado.app.utils.getClipText
|
||||
import io.legado.app.utils.msg
|
||||
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? {
|
||||
if (autoComplete) {
|
||||
return RuleComplete.autoComplete(rule, preRule, type)
|
||||
|
||||
@@ -3,6 +3,7 @@ package io.legado.app.ui.welcome
|
||||
import android.content.Intent
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Bundle
|
||||
import androidx.core.view.postDelayed
|
||||
import io.legado.app.base.BaseActivity
|
||||
import io.legado.app.constant.PreferKey
|
||||
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) {
|
||||
finish()
|
||||
} 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.animation.LinearInterpolator
|
||||
import android.widget.Checkable
|
||||
import androidx.core.view.postDelayed
|
||||
import io.legado.app.R
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.utils.dpToPx
|
||||
@@ -244,7 +245,7 @@ class SmoothCheckBox @JvmOverloads constructor(
|
||||
}
|
||||
// invalidate
|
||||
if (mDrewDistance < mLeftLineDistance + mRightLineDistance) {
|
||||
postDelayed({ this.postInvalidate() }, 10)
|
||||
postDelayed(10) { this.postInvalidate() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -298,10 +299,10 @@ class SmoothCheckBox @JvmOverloads constructor(
|
||||
}
|
||||
|
||||
private fun drawTickDelayed() {
|
||||
postDelayed({
|
||||
postDelayed(mAnimDuration.toLong()) {
|
||||
mTickDrawing = true
|
||||
postInvalidate()
|
||||
}, mAnimDuration.toLong())
|
||||
}
|
||||
}
|
||||
|
||||
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 io.legado.app.R
|
||||
import io.legado.app.lib.theme.accentColor
|
||||
import io.legado.app.utils.ColorUtils
|
||||
import io.legado.app.utils.getCompatColor
|
||||
import io.legado.app.utils.invisible
|
||||
import io.legado.app.utils.visible
|
||||
@@ -87,8 +88,6 @@ class BadgeView @JvmOverloads constructor(
|
||||
setLayoutParams(layoutParams)
|
||||
}
|
||||
|
||||
// set default font
|
||||
setTextColor(Color.WHITE)
|
||||
//setTypeface(Typeface.DEFAULT_BOLD);
|
||||
setTextSize(TypedValue.COMPLEX_UNIT_SP, 11f)
|
||||
setPadding(dip2Px(5f), dip2Px(1f), dip2Px(5f), dip2Px(1f))
|
||||
@@ -106,6 +105,10 @@ class BadgeView @JvmOverloads constructor(
|
||||
minHeight = dip2Px(16f)
|
||||
}
|
||||
|
||||
override fun setBackgroundColor(color: Int) {
|
||||
setBackground(radius, color)
|
||||
}
|
||||
|
||||
fun setBackground(dipRadius: Float, badgeColor: Int) {
|
||||
val radius = dip2Px(dipRadius).toFloat()
|
||||
val radiusArray =
|
||||
@@ -118,10 +121,13 @@ class BadgeView @JvmOverloads constructor(
|
||||
val bgDrawable = ShapeDrawable(roundRect)
|
||||
bgDrawable.paint.color = badgeColor
|
||||
background = bgDrawable
|
||||
}
|
||||
|
||||
fun setBackground(badgeColor: Int) {
|
||||
setBackground(radius, badgeColor)
|
||||
setTextColor(
|
||||
if (ColorUtils.isColorLight(badgeColor)) {
|
||||
Color.BLACK
|
||||
} else {
|
||||
Color.WHITE
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -142,9 +148,9 @@ class BadgeView @JvmOverloads constructor(
|
||||
|
||||
fun setHighlight(highlight: Boolean) {
|
||||
if (highlight) {
|
||||
setBackground(context.accentColor)
|
||||
setBackgroundColor(context.accentColor)
|
||||
} else {
|
||||
setBackground(context.getCompatColor(R.color.darker_gray))
|
||||
setBackgroundColor(context.getCompatColor(R.color.darker_gray))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +1,22 @@
|
||||
package io.legado.app.utils
|
||||
|
||||
import android.graphics.Color
|
||||
|
||||
import androidx.annotation.ColorInt
|
||||
import androidx.annotation.FloatRange
|
||||
import java.util.*
|
||||
import androidx.core.graphics.ColorUtils
|
||||
import kotlin.math.*
|
||||
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
object ColorUtils {
|
||||
|
||||
fun isColorLight(@ColorInt color: Int): Boolean {
|
||||
return ColorUtils.calculateLuminance(color) >= 0.5
|
||||
}
|
||||
|
||||
fun intToString(intColor: Int): String {
|
||||
return String.format("#%06X", 0xFFFFFF and intColor)
|
||||
}
|
||||
|
||||
|
||||
fun stripAlpha(@ColorInt color: Int): Int {
|
||||
return -0x1000000 or color
|
||||
}
|
||||
@@ -39,12 +41,6 @@ object ColorUtils {
|
||||
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
|
||||
fun invertColor(@ColorInt color: Int): Int {
|
||||
val r = 255 - Color.red(color)
|
||||
@@ -81,83 +77,6 @@ object ColorUtils {
|
||||
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 {
|
||||
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))
|
||||
}
|
||||
|
||||
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
|
||||
* comparing them using the CIE76 algorithm { http://en.wikipedia.org/wiki/Color_difference#CIE76}
|
||||
*/
|
||||
fun getColorDifference(a: Int, b: Int): Double {
|
||||
val r1: Int = Color.red(a)
|
||||
val g1: Int = Color.green(a)
|
||||
val b1: Int = Color.blue(a)
|
||||
val r2: Int = Color.red(b)
|
||||
val g2: Int = Color.green(b)
|
||||
val b2: Int = Color.blue(b)
|
||||
val lab1 = rgb2lab(r1, g1, b1)
|
||||
val lab2 = rgb2lab(r2, g2, b2)
|
||||
val lab1 = DoubleArray(3)
|
||||
val lab2 = DoubleArray(3)
|
||||
ColorUtils.colorToLAB(a, lab1)
|
||||
ColorUtils.colorToLAB(b, lab2)
|
||||
return sqrt(
|
||||
(lab2[0] - lab1[0].toDouble())
|
||||
.pow(2.0) + (lab2[1] - lab1[1].toDouble())
|
||||
.pow(2.0) + (lab2[2] - lab1[2].toDouble())
|
||||
(lab2[0] - lab1[0])
|
||||
.pow(2.0) + (lab2[1] - lab1[1])
|
||||
.pow(2.0) + (lab2[2] - lab1[2])
|
||||
.pow(2.0)
|
||||
)
|
||||
}
|
||||
|
||||
@@ -331,6 +331,7 @@ fun Context.openFileUri(uri: Uri, type: String? = null) {
|
||||
|
||||
@Suppress("DEPRECATION")
|
||||
val Context.isWifiConnect: Boolean
|
||||
@SuppressLint("MissingPermission")
|
||||
get() {
|
||||
val info = connectivityManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI)
|
||||
return info?.isConnected == true
|
||||
@@ -338,7 +339,7 @@ val Context.isWifiConnect: Boolean
|
||||
|
||||
val Context.isPad: Boolean
|
||||
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
|
||||
|
||||
@@ -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
|
||||
|
||||
import androidx.core.os.postDelayed
|
||||
import io.legado.app.exception.RegexTimeoutException
|
||||
import io.legado.app.help.CrashHandler
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import splitties.init.appCtx
|
||||
import kotlin.concurrent.thread
|
||||
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)
|
||||
block.resume(result)
|
||||
} catch (e: Exception) {
|
||||
block.cancel(e)
|
||||
block.resumeWithException(e)
|
||||
}
|
||||
}
|
||||
mainHandler.postDelayed({
|
||||
mainHandler.postDelayed(timeout) {
|
||||
if (thread.isAlive) {
|
||||
val timeoutMsg = "替换超时,3秒后还未结束将重启应用\n替换规则$regex\n替换内容:${this}"
|
||||
val exception = RegexTimeoutException(timeoutMsg)
|
||||
block.cancel(exception)
|
||||
appCtx.longToastOnUi(timeoutMsg)
|
||||
CrashHandler.saveCrashInfo2File(exception)
|
||||
mainHandler.postDelayed({
|
||||
mainHandler.postDelayed(3000) {
|
||||
if (thread.isAlive) {
|
||||
appCtx.restart()
|
||||
}
|
||||
}, 3000)
|
||||
}
|
||||
}
|
||||
}, timeout)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,7 @@ class HttpServer(port: Int) : NanoHTTPD(port) {
|
||||
"/saveBookSources" -> BookSourceController.saveSources(postData)
|
||||
"/deleteBookSources" -> BookSourceController.deleteSources(postData)
|
||||
"/saveBook" -> BookController.saveBook(postData)
|
||||
"/saveBookProgress" -> BookController.saveBookProgress(postData)
|
||||
"/addLocalBook" -> BookController.addLocalBook(session.parameters)
|
||||
"/saveReadConfig" -> BookController.saveWebReadConfig(postData)
|
||||
"/saveRssSource" -> RssSourceController.saveSource(postData)
|
||||
|
||||
@@ -33,6 +33,13 @@
|
||||
android:checked="true"
|
||||
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
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
|
||||
@@ -45,6 +45,13 @@
|
||||
android:text="@string/single_url"
|
||||
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
|
||||
|
||||
@@ -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