commit
4e8093ebd0
@ -0,0 +1,4 @@ |
|||||||
|
# 1.0.0 (2020-02-09) |
||||||
|
|
||||||
|
|
||||||
|
|
@ -1,2 +1,9 @@ |
|||||||
# legado |
# legado |
||||||
|
|
||||||
|
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) |
||||||
|
|
||||||
## 阅读3.0 |
## 阅读3.0 |
||||||
|
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg |
||||||
|
|
||||||
|
## 免责声明 |
||||||
|
https://gedoor.github.io/MyBookshelf/disclaimer.html |
||||||
|
@ -1,72 +1,62 @@ |
|||||||
[ |
[ |
||||||
{ |
{ |
||||||
"bgStr": "羊皮纸2.jpg", |
"bgStr": "#EBD9BB", |
||||||
"bgType": 1, |
"bgStrNight": "#1E2021", |
||||||
|
"textColor": "#63543C", |
||||||
|
"textColorNight": "#DCDFE1", |
||||||
|
"bgType": 0, |
||||||
|
"bgTypeNight": 0, |
||||||
"darkStatusIcon": true, |
"darkStatusIcon": true, |
||||||
"textColor": "#5E432E", |
|
||||||
"textSize": 24, |
"textSize": 24, |
||||||
"letterSpacing": 0, |
"letterSpacing": 0, |
||||||
"lineSpacingExtra": 10, |
"lineSpacingExtra": 10 |
||||||
"lineSpacingMultiplier": 1.2, |
|
||||||
"paddingLeft": 16, |
|
||||||
"paddingRight": 16, |
|
||||||
"paddingTop": 0, |
|
||||||
"paddingBottom": 0 |
|
||||||
}, |
}, |
||||||
{ |
{ |
||||||
"bgStr": "#C6BAA1", |
"bgStr": "#DDC090", |
||||||
|
"bgStrNight": "#3C3F43", |
||||||
|
"textColor": "#3E3422", |
||||||
|
"textColorNight": "#DCDFE1", |
||||||
"bgType": 0, |
"bgType": 0, |
||||||
|
"bgTypeNight": 0, |
||||||
"darkStatusIcon": true, |
"darkStatusIcon": true, |
||||||
"textColor": "#5E432E", |
|
||||||
"textSize": 24, |
"textSize": 24, |
||||||
"letterSpacing": 0, |
"letterSpacing": 0, |
||||||
"lineSpacingExtra": 10, |
"lineSpacingExtra": 10 |
||||||
"lineSpacingMultiplier": 1.2, |
|
||||||
"paddingLeft": 16, |
|
||||||
"paddingRight": 16, |
|
||||||
"paddingTop": 0, |
|
||||||
"paddingBottom": 0 |
|
||||||
}, |
}, |
||||||
{ |
{ |
||||||
"bgStr": "#015A86", |
"bgStr": "#C2D8AA", |
||||||
|
"bgStrNight": "#3C3F43", |
||||||
|
"textColor": "#596C44", |
||||||
|
"textColorNight": "#88C16F", |
||||||
"bgType": 0, |
"bgType": 0, |
||||||
|
"bgTypeNight": 0, |
||||||
"darkStatusIcon": false, |
"darkStatusIcon": false, |
||||||
"textColor": "#FFFFFF", |
|
||||||
"textSize": 24, |
"textSize": 24, |
||||||
"letterSpacing": 0, |
"letterSpacing": 0, |
||||||
"lineSpacingExtra": 10, |
"lineSpacingExtra": 10 |
||||||
"lineSpacingMultiplier": 1.2, |
|
||||||
"paddingLeft": 16, |
|
||||||
"paddingRight": 16, |
|
||||||
"paddingTop": 0, |
|
||||||
"paddingBottom": 0 |
|
||||||
}, |
}, |
||||||
{ |
{ |
||||||
"bgStr": "宁静夜色", |
"bgStr": "#DBB8E2", |
||||||
"bgType": 1, |
"bgStrNight": "#3C3F43", |
||||||
|
"textColor": "#68516C", |
||||||
|
"textColorNight": "#F6AEAE", |
||||||
|
"bgType": 0, |
||||||
|
"bgTypeNight": 0, |
||||||
"darkStatusIcon": false, |
"darkStatusIcon": false, |
||||||
"textColor": "#adadad", |
|
||||||
"textSize": 24, |
"textSize": 24, |
||||||
"letterSpacing": 0, |
"letterSpacing": 0, |
||||||
"lineSpacingExtra": 10, |
"lineSpacingExtra": 10 |
||||||
"lineSpacingMultiplier": 1.2, |
|
||||||
"paddingLeft": 16, |
|
||||||
"paddingRight": 16, |
|
||||||
"paddingTop": 0, |
|
||||||
"paddingBottom": 0 |
|
||||||
}, |
}, |
||||||
{ |
{ |
||||||
"bgStr": "#000000", |
"bgStr": "#ABCEE0", |
||||||
|
"bgStrNight": "#3C3F43", |
||||||
|
"textColor": "#3D4C54", |
||||||
|
"textColorNight": "#90BFF5", |
||||||
"bgType": 0, |
"bgType": 0, |
||||||
|
"bgTypeNight": 0, |
||||||
"darkStatusIcon": false, |
"darkStatusIcon": false, |
||||||
"textColor": "#adadad", |
|
||||||
"textSize": 24, |
"textSize": 24, |
||||||
"letterSpacing": 0, |
"letterSpacing": 0, |
||||||
"lineSpacingExtra": 10, |
"lineSpacingExtra": 10 |
||||||
"lineSpacingMultiplier": 1.2, |
|
||||||
"paddingLeft": 16, |
|
||||||
"paddingRight": 16, |
|
||||||
"paddingTop": 0, |
|
||||||
"paddingBottom": 0 |
|
||||||
} |
} |
||||||
] |
] |
@ -1,38 +0,0 @@ |
|||||||
[ |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则1", |
|
||||||
"rule": "^(.{0,8})(第)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([章节卷集部篇回场])(.{0,30})$", |
|
||||||
"serialNumber": 0 |
|
||||||
}, |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则2", |
|
||||||
"rule": "^([0-9]{1,5})([\\,\\.,-])(.{1,20})$", |
|
||||||
"serialNumber": 1 |
|
||||||
}, |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则3", |
|
||||||
"rule": "^(\\s{0,4})([\\(【《]?(卷)?)([0-9零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10})([\\.:: \f\t])(.{0,30})$", |
|
||||||
"serialNumber": 2 |
|
||||||
}, |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则4", |
|
||||||
"rule": "^(\\s{0,4})([\\((【《])(.{0,30})([\\))】》])(\\s{0,2})$", |
|
||||||
"serialNumber": 3 |
|
||||||
}, |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则5", |
|
||||||
"rule": "^(\\s{0,4})(正文)(.{0,20})$", |
|
||||||
"serialNumber": 4 |
|
||||||
}, |
|
||||||
{ |
|
||||||
"enable": true, |
|
||||||
"name": "默认正则6", |
|
||||||
"rule": "^(.{0,4})(Chapter|chapter)(\\s{0,4})([0-9]{1,4})(.{0,30})$", |
|
||||||
"serialNumber": 5 |
|
||||||
} |
|
||||||
] |
|
@ -0,0 +1,62 @@ |
|||||||
|
[ |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "数字 分隔符 标题名称", |
||||||
|
"rule": "^[ \\t]{0,4}\\d{1,5}[\\,\\., 、\\-].{1,30}$", |
||||||
|
"serialNumber": 0 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "目录", |
||||||
|
"rule": "^[ \\t]{0,4}(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||||
|
"serialNumber": 1 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": false, |
||||||
|
"name": "目录(不匹配行前空白)", |
||||||
|
"rule": "^(?<=\\s)(?:(?:内容|文章)?简介|前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||||
|
"serialNumber": 2 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": false, |
||||||
|
"name": "目录(去简介)", |
||||||
|
"rule": "^(?<=\\s)(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|篇(?!张))).{0,30}$", |
||||||
|
"serialNumber": 3 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": false, |
||||||
|
"name": "目录(古典小说备用)", |
||||||
|
"rule": "^[ \\t]{0,4}(?:前言|序章|楔子|正文(?!完)|[Cc]hapter|[Ss]ection|终章|后记|尾声|番外|第?\\s{0,4}[\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+?\\s{0,4}(?:章|节(?!课)|卷|集(?![合和])|部(?!分)|场(?![和合比电是])|篇(?!张))).{0,30}$", |
||||||
|
"serialNumber": 4 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "Chapter/Section/Part 序号 标题", |
||||||
|
"rule": "^[ \\t]{0,4}(?:[Cc]hapter|[Ss]ection|[Pp]art)\\s{0,4}\\d{1,4}.{0,30}$", |
||||||
|
"serialNumber": 5 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "正文 标题/序号", |
||||||
|
"rule": "^[ \\t]{0,4}正文\\s{1,4}.{0,20}$", |
||||||
|
"serialNumber": 6 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "特殊符号 序号 标题", |
||||||
|
"rule": "^[ \\t]{0,4}[〈〖〔【][第卷][\\d零一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]{1,10}[章节][\\.:: \f\t].{0,30}$", |
||||||
|
"serialNumber": 7 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable": true, |
||||||
|
"name": "特殊符号 标题", |
||||||
|
"rule": "^[ \\t]{0,4}[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", |
||||||
|
"serialNumber": 8 |
||||||
|
}, |
||||||
|
{ |
||||||
|
"enable":false, |
||||||
|
"name": "特殊符号 标题(不匹配空白字符)", |
||||||
|
"rule": "(?<=\\s)[〈〖〔【☆★].{1,30}[】〕〗〉]?\\s{0,4}$", |
||||||
|
"serialNumber": 9 |
||||||
|
} |
||||||
|
] |
@ -0,0 +1,12 @@ |
|||||||
|
## 文件结构介绍 |
||||||
|
|
||||||
|
* base 基类 |
||||||
|
* constant 常量 |
||||||
|
* data 数据 |
||||||
|
* help 帮助 |
||||||
|
* lib 库 |
||||||
|
* model 解析 |
||||||
|
* receiver 广播侦听 |
||||||
|
* service 服务 |
||||||
|
* ui 界面 |
||||||
|
* web web服务 |
@ -0,0 +1,24 @@ |
|||||||
|
package io.legado.app.base |
||||||
|
|
||||||
|
import android.os.Bundle |
||||||
|
import androidx.fragment.app.DialogFragment |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.Dispatchers |
||||||
|
import kotlinx.coroutines.Job |
||||||
|
import kotlin.coroutines.CoroutineContext |
||||||
|
|
||||||
|
abstract class BaseDialogFragment : DialogFragment(), CoroutineScope { |
||||||
|
override val coroutineContext: CoroutineContext |
||||||
|
get() = job + Dispatchers.Main |
||||||
|
private lateinit var job: Job |
||||||
|
|
||||||
|
override fun onCreate(savedInstanceState: Bundle?) { |
||||||
|
super.onCreate(savedInstanceState) |
||||||
|
job = Job() |
||||||
|
} |
||||||
|
|
||||||
|
override fun onDestroy() { |
||||||
|
super.onDestroy() |
||||||
|
job.cancel() |
||||||
|
} |
||||||
|
} |
@ -1 +1,17 @@ |
|||||||
## 存储数据用 |
## 存储数据用 |
||||||
|
* dao 数据操作 |
||||||
|
* entities 数据模型 |
||||||
|
* \Book 书籍信息 |
||||||
|
* \BookChapter 目录信息 |
||||||
|
* \BookGroup 书籍分组 |
||||||
|
* \Bookmark 书签 |
||||||
|
* \BookSource 书源 |
||||||
|
* \Cookie http cookie |
||||||
|
* \ReplaceRule 替换规则 |
||||||
|
* \RssArticle rss条目 |
||||||
|
* \RssReadRecord rss阅读记录 |
||||||
|
* \RssSource rss源 |
||||||
|
* \RssStar rss收藏 |
||||||
|
* \SearchBook 搜索结果 |
||||||
|
* \SearchKeyword 搜索关键字 |
||||||
|
* \TxtTocRule txt文件目录规则 |
||||||
|
@ -0,0 +1,28 @@ |
|||||||
|
package io.legado.app.data.dao |
||||||
|
|
||||||
|
import androidx.lifecycle.LiveData |
||||||
|
import androidx.room.* |
||||||
|
import io.legado.app.data.entities.TxtTocRule |
||||||
|
|
||||||
|
@Dao |
||||||
|
interface TxtTocRuleDao { |
||||||
|
|
||||||
|
@Query("select * from txtTocRules order by serialNumber") |
||||||
|
fun observeAll(): LiveData<List<TxtTocRule>> |
||||||
|
|
||||||
|
@get:Query("select * from txtTocRules order by serialNumber") |
||||||
|
val all: List<TxtTocRule> |
||||||
|
|
||||||
|
@get:Query("select * from txtTocRules where enable = 1 order by serialNumber") |
||||||
|
val enabled: List<TxtTocRule> |
||||||
|
|
||||||
|
@Insert(onConflict = OnConflictStrategy.REPLACE) |
||||||
|
fun insert(vararg rule: TxtTocRule) |
||||||
|
|
||||||
|
@Update(onConflict = OnConflictStrategy.REPLACE) |
||||||
|
fun update(vararg rule: TxtTocRule) |
||||||
|
|
||||||
|
@Delete |
||||||
|
fun delete(vararg rule: TxtTocRule) |
||||||
|
|
||||||
|
} |
@ -0,0 +1,7 @@ |
|||||||
|
package io.legado.app.data.entities |
||||||
|
|
||||||
|
import androidx.room.Entity |
||||||
|
import androidx.room.PrimaryKey |
||||||
|
|
||||||
|
@Entity(tableName = "rssReadRecords") |
||||||
|
data class RssReadRecord(@PrimaryKey val record: String, val read: Boolean = true) |
@ -0,0 +1,14 @@ |
|||||||
|
package io.legado.app.data.entities |
||||||
|
|
||||||
|
import androidx.room.Entity |
||||||
|
import androidx.room.PrimaryKey |
||||||
|
|
||||||
|
|
||||||
|
@Entity(tableName = "txtTocRules") |
||||||
|
data class TxtTocRule( |
||||||
|
@PrimaryKey |
||||||
|
var name: String = "", |
||||||
|
var rule: String = "", |
||||||
|
var serialNumber: Int = -1, |
||||||
|
var enable: Boolean = true |
||||||
|
) |
@ -0,0 +1,89 @@ |
|||||||
|
package io.legado.app.help |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.constant.PreferKey |
||||||
|
import io.legado.app.utils.* |
||||||
|
|
||||||
|
object AppConfig { |
||||||
|
|
||||||
|
fun isNightTheme(context: Context): Boolean { |
||||||
|
return when (context.getPrefString(PreferKey.themeMode, "0")) { |
||||||
|
"1" -> false |
||||||
|
"2" -> true |
||||||
|
else -> context.sysIsDarkMode() |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var isNightTheme: Boolean |
||||||
|
get() = isNightTheme(App.INSTANCE) |
||||||
|
set(value) { |
||||||
|
if (value) { |
||||||
|
App.INSTANCE.putPrefString(PreferKey.themeMode, "2") |
||||||
|
} else { |
||||||
|
App.INSTANCE.putPrefString(PreferKey.themeMode, "1") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var isTransparentStatusBar: Boolean |
||||||
|
get() = App.INSTANCE.getPrefBoolean("transparentStatusBar") |
||||||
|
set(value) { |
||||||
|
App.INSTANCE.putPrefBoolean("transparentStatusBar", value) |
||||||
|
} |
||||||
|
|
||||||
|
var backupPath: String? |
||||||
|
get() = App.INSTANCE.getPrefString(PreferKey.backupPath) |
||||||
|
set(value) { |
||||||
|
if (value.isNullOrEmpty()) { |
||||||
|
App.INSTANCE.removePref(PreferKey.backupPath) |
||||||
|
} else { |
||||||
|
App.INSTANCE.putPrefString(PreferKey.backupPath, value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var isShowRSS: Boolean |
||||||
|
get() = App.INSTANCE.getPrefBoolean(PreferKey.showRss, true) |
||||||
|
set(value) { |
||||||
|
App.INSTANCE.putPrefBoolean(PreferKey.showRss, value) |
||||||
|
} |
||||||
|
|
||||||
|
val autoRefreshBook: Boolean |
||||||
|
get() = App.INSTANCE.getPrefBoolean(App.INSTANCE.getString(R.string.pk_auto_refresh)) |
||||||
|
|
||||||
|
var threadCount: Int |
||||||
|
get() = App.INSTANCE.getPrefInt(PreferKey.threadCount, 16) |
||||||
|
set(value) { |
||||||
|
App.INSTANCE.putPrefInt(PreferKey.threadCount, value) |
||||||
|
} |
||||||
|
|
||||||
|
var importBookPath: String? |
||||||
|
get() = App.INSTANCE.getPrefString("importBookPath") |
||||||
|
set(value) { |
||||||
|
if (value == null) { |
||||||
|
App.INSTANCE.removePref("importBookPath") |
||||||
|
} else { |
||||||
|
App.INSTANCE.putPrefString("importBookPath", value) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
var ttsSpeechRate: Int |
||||||
|
get() = App.INSTANCE.getPrefInt(PreferKey.ttsSpeechRate, 5) |
||||||
|
set(value) { |
||||||
|
App.INSTANCE.putPrefInt(PreferKey.ttsSpeechRate, value) |
||||||
|
} |
||||||
|
|
||||||
|
val ttsSpeechPer: String |
||||||
|
get() = App.INSTANCE.getPrefString(PreferKey.ttsSpeechPer) ?: "0" |
||||||
|
|
||||||
|
val isEInkMode: Boolean |
||||||
|
get() = App.INSTANCE.getPrefBoolean("isEInkMode") |
||||||
|
|
||||||
|
val clickAllNext: Boolean get() = App.INSTANCE.getPrefBoolean(PreferKey.clickAllNext, false) |
||||||
|
|
||||||
|
var chineseConverterType: Int |
||||||
|
get() = App.INSTANCE.getPrefInt(PreferKey.chineseConverterType) |
||||||
|
set(value) { |
||||||
|
App.INSTANCE.putPrefInt(PreferKey.chineseConverterType, value) |
||||||
|
} |
||||||
|
} |
@ -1,58 +0,0 @@ |
|||||||
package io.legado.app.help |
|
||||||
|
|
||||||
import io.legado.app.App |
|
||||||
import java.io.File |
|
||||||
import java.io.IOException |
|
||||||
|
|
||||||
object FileHelp { |
|
||||||
|
|
||||||
|
|
||||||
//获取文件夹 |
|
||||||
fun getFolder(filePath: String): File { |
|
||||||
val file = File(filePath) |
|
||||||
//如果文件夹不存在,就创建它 |
|
||||||
if (!file.exists()) { |
|
||||||
file.mkdirs() |
|
||||||
} |
|
||||||
return file |
|
||||||
} |
|
||||||
|
|
||||||
//获取文件 |
|
||||||
@Synchronized |
|
||||||
fun getFile(filePath: String): File { |
|
||||||
val file = File(filePath) |
|
||||||
try { |
|
||||||
if (!file.exists()) { |
|
||||||
//创建父类文件夹 |
|
||||||
getFolder(file.parent) |
|
||||||
//创建文件 |
|
||||||
file.createNewFile() |
|
||||||
} |
|
||||||
} catch (e: IOException) { |
|
||||||
e.printStackTrace() |
|
||||||
} |
|
||||||
return file |
|
||||||
} |
|
||||||
|
|
||||||
fun getCachePath(): String { |
|
||||||
return App.INSTANCE.externalCacheDir?.absolutePath |
|
||||||
?: App.INSTANCE.cacheDir.absolutePath |
|
||||||
} |
|
||||||
|
|
||||||
//递归删除文件夹下的数据 |
|
||||||
@Synchronized |
|
||||||
fun deleteFile(filePath: String) { |
|
||||||
val file = File(filePath) |
|
||||||
if (!file.exists()) return |
|
||||||
|
|
||||||
if (file.isDirectory) { |
|
||||||
val files = file.listFiles() |
|
||||||
files?.forEach { subFile -> |
|
||||||
val path = subFile.path |
|
||||||
deleteFile(path) |
|
||||||
} |
|
||||||
} |
|
||||||
//删除文件 |
|
||||||
file.delete() |
|
||||||
} |
|
||||||
} |
|
@ -0,0 +1,34 @@ |
|||||||
|
package io.legado.app.help |
||||||
|
|
||||||
|
import androidx.recyclerview.widget.ListUpdateCallback |
||||||
|
import androidx.recyclerview.widget.RecyclerView |
||||||
|
import io.legado.app.base.adapter.ItemViewHolder |
||||||
|
|
||||||
|
class FirstTopListUpCallback : ListUpdateCallback { |
||||||
|
var firstInsert = -1 |
||||||
|
lateinit var adapter: RecyclerView.Adapter<ItemViewHolder> |
||||||
|
|
||||||
|
override fun onChanged(position: Int, count: Int, payload: Any?) { |
||||||
|
adapter.notifyItemRangeChanged(position, count, payload) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onMoved(fromPosition: Int, toPosition: Int) { |
||||||
|
if (toPosition == 0) { |
||||||
|
firstInsert = 0 |
||||||
|
} |
||||||
|
adapter.notifyItemMoved(fromPosition, toPosition) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onInserted(position: Int, count: Int) { |
||||||
|
if (firstInsert == -1 || firstInsert > position) { |
||||||
|
firstInsert = position |
||||||
|
} |
||||||
|
adapter.notifyItemRangeInserted(position, count) |
||||||
|
} |
||||||
|
|
||||||
|
override fun onRemoved(position: Int, count: Int) { |
||||||
|
adapter.notifyItemRangeRemoved(position, count) |
||||||
|
} |
||||||
|
|
||||||
|
|
||||||
|
} |
@ -0,0 +1,65 @@ |
|||||||
|
package io.legado.app.help |
||||||
|
|
||||||
|
import android.content.ComponentName |
||||||
|
import android.content.pm.PackageManager |
||||||
|
import android.os.Build |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.ui.welcome.Launcher1 |
||||||
|
import io.legado.app.ui.welcome.Launcher2 |
||||||
|
import io.legado.app.ui.welcome.Launcher3 |
||||||
|
import io.legado.app.ui.welcome.WelcomeActivity |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
|
||||||
|
/** |
||||||
|
* Created by GKF on 2018/2/27. |
||||||
|
* 更换图标 |
||||||
|
*/ |
||||||
|
object LauncherIconHelp { |
||||||
|
private val packageManager: PackageManager = App.INSTANCE.packageManager |
||||||
|
private val componentNames = arrayListOf( |
||||||
|
ComponentName(App.INSTANCE, Launcher1::class.java.name), |
||||||
|
ComponentName(App.INSTANCE, Launcher2::class.java.name), |
||||||
|
ComponentName(App.INSTANCE, Launcher3::class.java.name) |
||||||
|
) |
||||||
|
|
||||||
|
fun changeIcon(icon: String?) { |
||||||
|
if (icon.isNullOrEmpty()) return |
||||||
|
if (Build.VERSION.SDK_INT < 26) { |
||||||
|
App.INSTANCE.toast(R.string.chage_icon_error) |
||||||
|
return |
||||||
|
} |
||||||
|
var hasEnabled = false |
||||||
|
componentNames.forEach { |
||||||
|
if (icon.equals(it.className.substringAfterLast("."), true)) { |
||||||
|
hasEnabled = true |
||||||
|
//启用 |
||||||
|
packageManager.setComponentEnabledSetting( |
||||||
|
it, |
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, |
||||||
|
PackageManager.DONT_KILL_APP |
||||||
|
) |
||||||
|
} else { |
||||||
|
//禁用 |
||||||
|
packageManager.setComponentEnabledSetting( |
||||||
|
it, |
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, |
||||||
|
PackageManager.DONT_KILL_APP |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
if (hasEnabled) { |
||||||
|
packageManager.setComponentEnabledSetting( |
||||||
|
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), |
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_DISABLED, |
||||||
|
PackageManager.DONT_KILL_APP |
||||||
|
) |
||||||
|
} else { |
||||||
|
packageManager.setComponentEnabledSetting( |
||||||
|
ComponentName(App.INSTANCE, WelcomeActivity::class.java.name), |
||||||
|
PackageManager.COMPONENT_ENABLED_STATE_ENABLED, |
||||||
|
PackageManager.DONT_KILL_APP |
||||||
|
) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
@ -1,102 +0,0 @@ |
|||||||
package io.legado.app.help.http |
|
||||||
|
|
||||||
import kotlinx.coroutines.CompletableDeferred |
|
||||||
import kotlinx.coroutines.Deferred |
|
||||||
import retrofit2.* |
|
||||||
import java.lang.reflect.ParameterizedType |
|
||||||
import java.lang.reflect.Type |
|
||||||
|
|
||||||
class CoroutinesCallAdapterFactory private constructor() : CallAdapter.Factory() { |
|
||||||
companion object { |
|
||||||
fun create(): CoroutinesCallAdapterFactory { |
|
||||||
return CoroutinesCallAdapterFactory() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun get( |
|
||||||
returnType: Type, |
|
||||||
annotations: Array<out Annotation>, |
|
||||||
retrofit: Retrofit |
|
||||||
): CallAdapter<*, *>? { |
|
||||||
if (Deferred::class.java != getRawType(returnType)) { |
|
||||||
return null |
|
||||||
} |
|
||||||
check(returnType is ParameterizedType) { "Deferred return type must be parameterized as Deferred<Foo> or Deferred<out Foo>" } |
|
||||||
val responseType = getParameterUpperBound(0, returnType) |
|
||||||
|
|
||||||
val rawDeferredType = getRawType(responseType) |
|
||||||
return if (rawDeferredType == Response::class.java) { |
|
||||||
check(responseType is ParameterizedType) { "Response must be parameterized as Response<Foo> or Response<out Foo>" } |
|
||||||
ResponseCallAdapter<Any>( |
|
||||||
getParameterUpperBound( |
|
||||||
0, |
|
||||||
responseType |
|
||||||
) |
|
||||||
) |
|
||||||
} else { |
|
||||||
BodyCallAdapter<Any>(responseType) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private class BodyCallAdapter<T>( |
|
||||||
private val responseType: Type |
|
||||||
) : CallAdapter<T, Deferred<T>> { |
|
||||||
|
|
||||||
override fun responseType() = responseType |
|
||||||
|
|
||||||
override fun adapt(call: Call<T>): Deferred<T> { |
|
||||||
val deferred = CompletableDeferred<T>() |
|
||||||
|
|
||||||
deferred.invokeOnCompletion { |
|
||||||
if (deferred.isCancelled) { |
|
||||||
call.cancel() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
call.enqueue(object : Callback<T> { |
|
||||||
override fun onFailure(call: Call<T>, t: Throwable) { |
|
||||||
deferred.completeExceptionally(t) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) { |
|
||||||
if (response.isSuccessful) { |
|
||||||
deferred.complete(response.body()!!) |
|
||||||
} else { |
|
||||||
deferred.completeExceptionally(HttpException(response)) |
|
||||||
} |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
return deferred |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private class ResponseCallAdapter<T>( |
|
||||||
private val responseType: Type |
|
||||||
) : CallAdapter<T, Deferred<Response<T>>> { |
|
||||||
|
|
||||||
override fun responseType() = responseType |
|
||||||
|
|
||||||
override fun adapt(call: Call<T>): Deferred<Response<T>> { |
|
||||||
val deferred = CompletableDeferred<Response<T>>() |
|
||||||
|
|
||||||
deferred.invokeOnCompletion { |
|
||||||
if (deferred.isCancelled) { |
|
||||||
call.cancel() |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
call.enqueue(object : Callback<T> { |
|
||||||
override fun onFailure(call: Call<T>, t: Throwable) { |
|
||||||
deferred.completeExceptionally(t) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onResponse(call: Call<T>, response: Response<T>) { |
|
||||||
deferred.complete(response) |
|
||||||
} |
|
||||||
}) |
|
||||||
|
|
||||||
return deferred |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
@ -1,5 +1,7 @@ |
|||||||
package io.legado.app.help.permission |
package io.legado.app.help.permission |
||||||
|
|
||||||
interface OnPermissionsDeniedCallback { |
interface OnPermissionsDeniedCallback { |
||||||
|
|
||||||
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) |
fun onPermissionsDenied(requestCode: Int, deniedPermissions: Array<String>) |
||||||
|
|
||||||
} |
} |
||||||
|
@ -0,0 +1,149 @@ |
|||||||
|
package io.legado.app.help.storage |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.net.Uri |
||||||
|
import androidx.documentfile.provider.DocumentFile |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.data.entities.BookSource |
||||||
|
import io.legado.app.data.entities.ReplaceRule |
||||||
|
import io.legado.app.help.coroutine.Coroutine |
||||||
|
import io.legado.app.utils.DocumentUtils |
||||||
|
import io.legado.app.utils.FileUtils |
||||||
|
import kotlinx.coroutines.Dispatchers |
||||||
|
import kotlinx.coroutines.GlobalScope |
||||||
|
import kotlinx.coroutines.launch |
||||||
|
import kotlinx.coroutines.withContext |
||||||
|
import org.jetbrains.anko.toast |
||||||
|
import java.io.File |
||||||
|
|
||||||
|
object ImportOldData { |
||||||
|
val yueDuPath by lazy { |
||||||
|
FileUtils.getSdCardPath() + File.separator + "YueDu" |
||||||
|
} |
||||||
|
|
||||||
|
fun import(context: Context) { |
||||||
|
GlobalScope.launch(Dispatchers.IO) { |
||||||
|
try {// 导入书架 |
||||||
|
val shelfFile = |
||||||
|
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookShelf.json") |
||||||
|
val json = shelfFile.readText() |
||||||
|
val importCount = importOldBookshelf(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("成功导入书籍${importCount}") |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("导入书籍失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
try {// Book source |
||||||
|
val sourceFile = |
||||||
|
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookSource.json") |
||||||
|
val json = sourceFile.readText() |
||||||
|
val importCount = importOldSource(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("成功导入书源${importCount}") |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("导入源失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
try {// Replace rules |
||||||
|
val ruleFile = |
||||||
|
FileUtils.createFileIfNotExist(yueDuPath + File.separator + "myBookReplaceRule.json") |
||||||
|
val json = ruleFile.readText() |
||||||
|
val importCount = importOldReplaceRule(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("成功导入替换规则${importCount}") |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
context.toast("导入替换规则失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun importUri(uri: Uri) { |
||||||
|
Coroutine.async { |
||||||
|
DocumentFile.fromTreeUri(App.INSTANCE, uri)?.listFiles()?.forEach { |
||||||
|
when (it.name) { |
||||||
|
"myBookShelf.json" -> |
||||||
|
try { |
||||||
|
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> |
||||||
|
val importCount = importOldBookshelf(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("成功导入书籍${importCount}") |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e: java.lang.Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("导入书籍失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
"myBookSource.json" -> |
||||||
|
try { |
||||||
|
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> |
||||||
|
val importCount = importOldSource(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("成功导入书源${importCount}") |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("导入源失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
"myBookReplaceRule.json" -> |
||||||
|
try { |
||||||
|
DocumentUtils.readText(App.INSTANCE, it.uri)?.let { json -> |
||||||
|
val importCount = importOldReplaceRule(json) |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("成功导入替换规则${importCount}") |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
withContext(Dispatchers.Main) { |
||||||
|
App.INSTANCE.toast("导入替换规则失败\n${e.localizedMessage}") |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
fun importOldBookshelf(json: String): Int { |
||||||
|
val books = OldBook.toNewBook(json) |
||||||
|
App.db.bookDao().insert(*books.toTypedArray()) |
||||||
|
return books.size |
||||||
|
} |
||||||
|
|
||||||
|
fun importOldSource(json: String): Int { |
||||||
|
val bookSources = mutableListOf<BookSource>() |
||||||
|
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") |
||||||
|
for (item in items) { |
||||||
|
val jsonItem = Restore.jsonPath.parse(item) |
||||||
|
OldRule.jsonToBookSource(jsonItem.jsonString())?.let { |
||||||
|
bookSources.add(it) |
||||||
|
} |
||||||
|
} |
||||||
|
App.db.bookSourceDao().insert(*bookSources.toTypedArray()) |
||||||
|
return bookSources.size |
||||||
|
} |
||||||
|
|
||||||
|
fun importOldReplaceRule(json: String): Int { |
||||||
|
val replaceRules = mutableListOf<ReplaceRule>() |
||||||
|
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") |
||||||
|
for (item in items) { |
||||||
|
val jsonItem = Restore.jsonPath.parse(item) |
||||||
|
OldRule.jsonToReplaceRule(jsonItem.jsonString())?.let { |
||||||
|
replaceRules.add(it) |
||||||
|
} |
||||||
|
} |
||||||
|
App.db.replaceRuleDao().insert(*replaceRules.toTypedArray()) |
||||||
|
return replaceRules.size |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,55 @@ |
|||||||
|
package io.legado.app.help.storage |
||||||
|
|
||||||
|
import android.util.Log |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.constant.AppConst |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.utils.readBool |
||||||
|
import io.legado.app.utils.readInt |
||||||
|
import io.legado.app.utils.readLong |
||||||
|
import io.legado.app.utils.readString |
||||||
|
|
||||||
|
object OldBook { |
||||||
|
|
||||||
|
fun toNewBook(json: String): List<Book> { |
||||||
|
val books = mutableListOf<Book>() |
||||||
|
val items: List<Map<String, Any>> = Restore.jsonPath.parse(json).read("$") |
||||||
|
val existingBooks = App.db.bookDao().allBookUrls.toSet() |
||||||
|
for (item in items) { |
||||||
|
val jsonItem = Restore.jsonPath.parse(item) |
||||||
|
val book = Book() |
||||||
|
book.bookUrl = jsonItem.readString("$.noteUrl") ?: "" |
||||||
|
if (book.bookUrl.isBlank()) continue |
||||||
|
book.name = jsonItem.readString("$.bookInfoBean.name") ?: "" |
||||||
|
if (book.bookUrl in existingBooks) { |
||||||
|
Log.d(AppConst.APP_TAG, "Found existing book: ${book.name}") |
||||||
|
continue |
||||||
|
} |
||||||
|
book.origin = jsonItem.readString("$.tag") ?: "" |
||||||
|
book.originName = jsonItem.readString("$.bookInfoBean.origin") ?: "" |
||||||
|
book.author = jsonItem.readString("$.bookInfoBean.author") ?: "" |
||||||
|
book.type = |
||||||
|
if (jsonItem.readString("$.bookInfoBean.bookSourceType") == "AUDIO") 1 else 0 |
||||||
|
book.tocUrl = jsonItem.readString("$.bookInfoBean.chapterUrl") ?: book.bookUrl |
||||||
|
book.coverUrl = jsonItem.readString("$.bookInfoBean.coverUrl") |
||||||
|
book.customCoverUrl = jsonItem.readString("$.customCoverPath") |
||||||
|
book.lastCheckTime = jsonItem.readLong("$.bookInfoBean.finalRefreshData") ?: 0 |
||||||
|
book.canUpdate = jsonItem.readBool("$.allowUpdate") == true |
||||||
|
book.totalChapterNum = jsonItem.readInt("$.chapterListSize") ?: 0 |
||||||
|
book.durChapterIndex = jsonItem.readInt("$.durChapter") ?: 0 |
||||||
|
book.durChapterTitle = jsonItem.readString("$.durChapterName") |
||||||
|
book.durChapterPos = jsonItem.readInt("$.durChapterPage") ?: 0 |
||||||
|
book.durChapterTime = jsonItem.readLong("$.finalDate") ?: 0 |
||||||
|
book.group = jsonItem.readInt("$.group") ?: 0 |
||||||
|
book.intro = jsonItem.readString("$.bookInfoBean.introduce") |
||||||
|
book.latestChapterTitle = jsonItem.readString("$.lastChapterName") |
||||||
|
book.lastCheckCount = jsonItem.readInt("$.newChapters") ?: 0 |
||||||
|
book.order = jsonItem.readInt("$.serialNumber") ?: 0 |
||||||
|
book.useReplaceRule = jsonItem.readBool("$.useReplaceRule") == true |
||||||
|
book.variable = jsonItem.readString("$.variable") |
||||||
|
books.add(book) |
||||||
|
} |
||||||
|
return books |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1 +1,4 @@ |
|||||||
## 放置一些copy过来的库 |
## 放置一些copy过来的库 |
||||||
|
* dialogs 弹出框 |
||||||
|
* theme 主题 |
||||||
|
* webDav 网络存储 |
@ -1,27 +0,0 @@ |
|||||||
package io.legado.app.lib.theme.view |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.graphics.Color |
|
||||||
import android.util.AttributeSet |
|
||||||
import androidx.appcompat.widget.AppCompatTextView |
|
||||||
import io.legado.app.R |
|
||||||
import io.legado.app.lib.theme.ColorUtils |
|
||||||
import io.legado.app.lib.theme.Selector |
|
||||||
import io.legado.app.lib.theme.ThemeStore |
|
||||||
|
|
||||||
class ATEAccentBgTextView(context: Context, attrs: AttributeSet) : |
|
||||||
AppCompatTextView(context, attrs) { |
|
||||||
|
|
||||||
init { |
|
||||||
val typedArray = context.obtainStyledAttributes(attrs, R.styleable.ATEAccentBgTextView) |
|
||||||
val radios = |
|
||||||
typedArray.getDimensionPixelOffset(R.styleable.ATEAccentBgTextView_abt_radius, 0) |
|
||||||
typedArray.recycle() |
|
||||||
background = Selector.shapeBuild() |
|
||||||
.setCornerRadius(radios) |
|
||||||
.setDefaultBgColor(ThemeStore.accentColor(context)) |
|
||||||
.setPressedBgColor(ColorUtils.darkenColor(ThemeStore.accentColor(context))) |
|
||||||
.create() |
|
||||||
setTextColor(Color.WHITE) |
|
||||||
} |
|
||||||
} |
|
@ -1,31 +0,0 @@ |
|||||||
package io.legado.app.lib.theme.view |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.util.AttributeSet |
|
||||||
import androidx.appcompat.widget.AppCompatTextView |
|
||||||
import io.legado.app.R |
|
||||||
import io.legado.app.lib.theme.Selector |
|
||||||
import io.legado.app.lib.theme.ThemeStore |
|
||||||
import io.legado.app.utils.dp |
|
||||||
import io.legado.app.utils.getCompatColor |
|
||||||
|
|
||||||
class ATEStrokeTextView(context: Context, attrs: AttributeSet) : AppCompatTextView(context, attrs) { |
|
||||||
|
|
||||||
init { |
|
||||||
background = Selector.shapeBuild() |
|
||||||
.setCornerRadius(1.dp) |
|
||||||
.setStrokeWidth(1.dp) |
|
||||||
.setDisabledStrokeColor(context.getCompatColor(R.color.md_grey_500)) |
|
||||||
.setDefaultStrokeColor(ThemeStore.textColorSecondary(context)) |
|
||||||
.setSelectedStrokeColor(ThemeStore.accentColor(context)) |
|
||||||
.setPressedBgColor(context.getCompatColor(R.color.transparent30)) |
|
||||||
.create() |
|
||||||
setTextColor( |
|
||||||
Selector.colorBuild() |
|
||||||
.setDefaultColor(ThemeStore.textColorSecondary(context)) |
|
||||||
.setSelectedColor(ThemeStore.accentColor(context)) |
|
||||||
.setDisabledColor(context.getCompatColor(R.color.md_grey_500)) |
|
||||||
.create() |
|
||||||
) |
|
||||||
} |
|
||||||
} |
|
@ -1,15 +0,0 @@ |
|||||||
package io.legado.app.lib.theme.view |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.util.AttributeSet |
|
||||||
import com.google.android.material.textfield.TextInputLayout |
|
||||||
import io.legado.app.lib.theme.Selector |
|
||||||
import io.legado.app.lib.theme.ThemeStore |
|
||||||
|
|
||||||
class ATETextInputLayout(context: Context, attrs: AttributeSet?) : TextInputLayout(context, attrs) { |
|
||||||
|
|
||||||
init { |
|
||||||
defaultHintTextColor = Selector.colorBuild().setDefaultColor(ThemeStore.accentColor(context)).create() |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
@ -1,2 +1,6 @@ |
|||||||
## 放置一些模块类 |
## 放置一些模块类 |
||||||
* 书源解析 |
* analyzeRule 书源规则解析 |
||||||
|
* localBook 本地书籍解析 |
||||||
|
* rss 订阅规则解析 |
||||||
|
* webBook 获取网络书籍 |
||||||
|
|
||||||
|
@ -0,0 +1,247 @@ |
|||||||
|
package io.legado.app.model.localBook |
||||||
|
|
||||||
|
import android.content.Context |
||||||
|
import android.net.Uri |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.data.entities.BookChapter |
||||||
|
import io.legado.app.data.entities.TxtTocRule |
||||||
|
import io.legado.app.utils.* |
||||||
|
import java.io.File |
||||||
|
import java.io.RandomAccessFile |
||||||
|
import java.nio.charset.Charset |
||||||
|
import java.util.regex.Matcher |
||||||
|
import java.util.regex.Pattern |
||||||
|
|
||||||
|
object AnalyzeTxtFile { |
||||||
|
private const val folderName = "bookTxt" |
||||||
|
private const val BLANK: Byte = 0x0a |
||||||
|
//默认从文件中获取数据的长度 |
||||||
|
private const val BUFFER_SIZE = 512 * 1024 |
||||||
|
//没有标题的时候,每个章节的最大长度 |
||||||
|
private const val MAX_LENGTH_WITH_NO_CHAPTER = 10 * 1024 |
||||||
|
private val cacheFolder: File by lazy { |
||||||
|
val rootFile = App.INSTANCE.getExternalFilesDir(null) |
||||||
|
?: App.INSTANCE.externalCacheDir |
||||||
|
?: App.INSTANCE.cacheDir |
||||||
|
FileUtils.createFolderIfNotExist(rootFile, subDirs = *arrayOf(folderName)) |
||||||
|
} |
||||||
|
|
||||||
|
fun analyze(context: Context, book: Book): ArrayList<BookChapter> { |
||||||
|
val bookFile = getBookFile(context, book) |
||||||
|
book.charset = EncodingDetect.getEncode(bookFile) |
||||||
|
val charset = book.fileCharset() |
||||||
|
val toc = arrayListOf<BookChapter>() |
||||||
|
//获取文件流 |
||||||
|
val bookStream = RandomAccessFile(bookFile, "r") |
||||||
|
val rulePattern = getTocRule(book, bookStream, charset) |
||||||
|
|
||||||
|
//加载章节 |
||||||
|
val buffer = ByteArray(BUFFER_SIZE) |
||||||
|
//获取到的块起始点,在文件中的位置 |
||||||
|
var curOffset: Long = 0 |
||||||
|
//block的个数 |
||||||
|
var blockPos = 0 |
||||||
|
//读取的长度 |
||||||
|
var length: Int |
||||||
|
var allLength = 0 |
||||||
|
|
||||||
|
//获取文件中的数据到buffer,直到没有数据为止 |
||||||
|
while (bookStream.read(buffer, 0, buffer.size).also { length = it } > 0) { |
||||||
|
++blockPos |
||||||
|
//如果存在Chapter |
||||||
|
if (rulePattern != null) { //将数据转换成String |
||||||
|
var blockContent = String(buffer, 0, length, charset) |
||||||
|
val lastN = blockContent.lastIndexOf("\n") |
||||||
|
if (lastN != 0) { |
||||||
|
blockContent = blockContent.substring(0, lastN) |
||||||
|
length = blockContent.toByteArray(charset).size |
||||||
|
allLength += length |
||||||
|
bookStream.seek(allLength.toLong()) |
||||||
|
} |
||||||
|
//当前Block下使过的String的指针 |
||||||
|
var seekPos = 0 |
||||||
|
//进行正则匹配 |
||||||
|
val matcher: Matcher = rulePattern.matcher(blockContent) |
||||||
|
//如果存在相应章节 |
||||||
|
while (matcher.find()) { //获取匹配到的字符在字符串中的起始位置 |
||||||
|
val chapterStart = matcher.start() |
||||||
|
//如果 seekPos == 0 && nextChapterPos != 0 表示当前block处前面有一段内容 |
||||||
|
//第一种情况一定是序章 第二种情况可能是上一个章节的内容 |
||||||
|
if (seekPos == 0 && chapterStart != 0) { //获取当前章节的内容 |
||||||
|
val chapterContent = blockContent.substring(seekPos, chapterStart) |
||||||
|
//设置指针偏移 |
||||||
|
seekPos += chapterContent.length |
||||||
|
if (toc.size == 0) { //如果当前没有章节,那么就是序章 |
||||||
|
//加入简介 |
||||||
|
book.intro = chapterContent |
||||||
|
//创建当前章节 |
||||||
|
val curChapter = BookChapter() |
||||||
|
curChapter.title = matcher.group() |
||||||
|
curChapter.start = chapterContent.toByteArray(charset).size.toLong() |
||||||
|
toc.add(curChapter) |
||||||
|
} else { //否则就block分割之后,上一个章节的剩余内容 |
||||||
|
//获取上一章节 |
||||||
|
val lastChapter = toc.last() |
||||||
|
//将当前段落添加上一章去 |
||||||
|
lastChapter.end = |
||||||
|
lastChapter.end!! + chapterContent.toByteArray(charset).size |
||||||
|
//创建当前章节 |
||||||
|
val curChapter = BookChapter() |
||||||
|
curChapter.title = matcher.group() |
||||||
|
curChapter.start = lastChapter.end |
||||||
|
toc.add(curChapter) |
||||||
|
} |
||||||
|
} else { //是否存在章节 |
||||||
|
if (toc.size != 0) { //获取章节内容 |
||||||
|
val chapterContent = blockContent.substring(seekPos, matcher.start()) |
||||||
|
seekPos += chapterContent.length |
||||||
|
//获取上一章节 |
||||||
|
val lastChapter = toc.last() |
||||||
|
lastChapter.end = |
||||||
|
lastChapter.start!! + chapterContent.toByteArray(charset).size |
||||||
|
//创建当前章节 |
||||||
|
val curChapter = BookChapter() |
||||||
|
curChapter.title = matcher.group() |
||||||
|
curChapter.start = lastChapter.end |
||||||
|
toc.add(curChapter) |
||||||
|
} else { //如果章节不存在则创建章节 |
||||||
|
val curChapter = BookChapter() |
||||||
|
curChapter.title = matcher.group() |
||||||
|
curChapter.start = 0L |
||||||
|
curChapter.end = 0L |
||||||
|
toc.add(curChapter) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} else { //进行本地虚拟分章 |
||||||
|
//章节在buffer的偏移量 |
||||||
|
var chapterOffset = 0 |
||||||
|
//当前剩余可分配的长度 |
||||||
|
var strLength = length |
||||||
|
//分章的位置 |
||||||
|
var chapterPos = 0 |
||||||
|
while (strLength > 0) { |
||||||
|
++chapterPos |
||||||
|
//是否长度超过一章 |
||||||
|
if (strLength > MAX_LENGTH_WITH_NO_CHAPTER) { //在buffer中一章的终止点 |
||||||
|
var end = length |
||||||
|
//寻找换行符作为终止点 |
||||||
|
for (i in chapterOffset + MAX_LENGTH_WITH_NO_CHAPTER until length) { |
||||||
|
if (buffer[i] == BLANK) { |
||||||
|
end = i |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
val chapter = BookChapter() |
||||||
|
chapter.title = "第${blockPos}章($chapterPos)" |
||||||
|
chapter.start = curOffset + chapterOffset + 1 |
||||||
|
chapter.end = curOffset + end |
||||||
|
toc.add(chapter) |
||||||
|
//减去已经被分配的长度 |
||||||
|
strLength -= (end - chapterOffset) |
||||||
|
//设置偏移的位置 |
||||||
|
chapterOffset = end |
||||||
|
} else { |
||||||
|
val chapter = BookChapter() |
||||||
|
chapter.title = "第" + blockPos + "章" + "(" + chapterPos + ")" |
||||||
|
chapter.start = curOffset + chapterOffset + 1 |
||||||
|
chapter.end = curOffset + length |
||||||
|
toc.add(chapter) |
||||||
|
strLength = 0 |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
//block的偏移点 |
||||||
|
curOffset += length.toLong() |
||||||
|
|
||||||
|
if (rulePattern != null) { //设置上一章的结尾 |
||||||
|
val lastChapter = toc.last() |
||||||
|
lastChapter.end = curOffset |
||||||
|
} |
||||||
|
|
||||||
|
//当添加的block太多的时候,执行GC |
||||||
|
if (blockPos % 15 == 0) { |
||||||
|
System.gc() |
||||||
|
System.runFinalization() |
||||||
|
} |
||||||
|
} |
||||||
|
bookStream.close() |
||||||
|
for (i in toc.indices) { |
||||||
|
val bean = toc[i] |
||||||
|
bean.index = i |
||||||
|
bean.bookUrl = book.bookUrl |
||||||
|
bean.url = (MD5Utils.md5Encode16(book.originName + i + bean.title) ?: "") |
||||||
|
} |
||||||
|
book.latestChapterTitle = toc.last().title |
||||||
|
|
||||||
|
System.gc() |
||||||
|
System.runFinalization() |
||||||
|
return toc |
||||||
|
} |
||||||
|
|
||||||
|
fun getContent(book: Book, bookChapter: BookChapter): String { |
||||||
|
val bookFile = getBookFile(App.INSTANCE, book) |
||||||
|
//获取文件流 |
||||||
|
val bookStream = RandomAccessFile(bookFile, "r") |
||||||
|
bookStream.seek(bookChapter.start ?: 0) |
||||||
|
val extent = (bookChapter.end!! - bookChapter.start!!).toInt() |
||||||
|
val content = ByteArray(extent) |
||||||
|
bookStream.read(content, 0, extent) |
||||||
|
return String(content, book.fileCharset()) |
||||||
|
} |
||||||
|
|
||||||
|
private fun getBookFile(context: Context, book: Book): File { |
||||||
|
val uri = Uri.parse(book.bookUrl) |
||||||
|
val bookFile = FileUtils.getFile(cacheFolder, book.originName, subDirs = *arrayOf()) |
||||||
|
if (!bookFile.exists()) { |
||||||
|
bookFile.createNewFile() |
||||||
|
DocumentUtils.readBytes(context, uri)?.let { |
||||||
|
bookFile.writeBytes(it) |
||||||
|
} |
||||||
|
} |
||||||
|
return bookFile |
||||||
|
} |
||||||
|
|
||||||
|
private fun getTocRule(book: Book, bookStream: RandomAccessFile, charset: Charset): Pattern? { |
||||||
|
if (book.tocUrl.isNotEmpty()) { |
||||||
|
return Pattern.compile(book.tocUrl, Pattern.MULTILINE) |
||||||
|
} |
||||||
|
val tocRules = getTocRules() |
||||||
|
var rulePattern: Pattern? = null |
||||||
|
//首先获取128k的数据 |
||||||
|
val buffer = ByteArray(BUFFER_SIZE / 4) |
||||||
|
val length = bookStream.read(buffer, 0, buffer.size) |
||||||
|
val content = String(buffer, 0, length, charset) |
||||||
|
for (tocRule in tocRules) { |
||||||
|
val pattern = Pattern.compile(tocRule.rule, Pattern.MULTILINE) |
||||||
|
val matcher = pattern.matcher(content) |
||||||
|
if (matcher.find()) { |
||||||
|
book.tocUrl = tocRule.rule |
||||||
|
rulePattern = pattern |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
bookStream.seek(0) |
||||||
|
return rulePattern |
||||||
|
} |
||||||
|
|
||||||
|
private fun getTocRules(): List<TxtTocRule> { |
||||||
|
val rules = App.db.txtTocRule().all |
||||||
|
if (rules.isEmpty()) { |
||||||
|
return getDefaultRules() |
||||||
|
} |
||||||
|
return rules |
||||||
|
} |
||||||
|
|
||||||
|
fun getDefaultRules(): List<TxtTocRule> { |
||||||
|
App.INSTANCE.assets.open("txtTocRule.json").readBytes().let { byteArray -> |
||||||
|
GSON.fromJsonArray<TxtTocRule>(String(byteArray))?.let { |
||||||
|
App.db.txtTocRule().insert(*it.toTypedArray()) |
||||||
|
return it |
||||||
|
} |
||||||
|
} |
||||||
|
return emptyList() |
||||||
|
} |
||||||
|
} |
@ -0,0 +1,30 @@ |
|||||||
|
package io.legado.app.model.localBook |
||||||
|
|
||||||
|
import androidx.documentfile.provider.DocumentFile |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
|
||||||
|
|
||||||
|
object LocalBook { |
||||||
|
|
||||||
|
fun importFile(doc: DocumentFile) { |
||||||
|
doc.name?.let { fileName -> |
||||||
|
val str = fileName.substringBeforeLast(".") |
||||||
|
var name = str.substringBefore("作者") |
||||||
|
val author = str.substringAfter("作者", "") |
||||||
|
val smhStart = name.indexOf("《") |
||||||
|
val smhEnd = name.indexOf("》") |
||||||
|
if (smhStart != -1 && smhEnd != -1) { |
||||||
|
name = (name.substring(smhStart + 1, smhEnd)) |
||||||
|
} |
||||||
|
val book = Book( |
||||||
|
bookUrl = doc.uri.toString(), |
||||||
|
name = name, |
||||||
|
author = author, |
||||||
|
originName = fileName |
||||||
|
) |
||||||
|
App.db.bookDao().insert(book) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -0,0 +1,234 @@ |
|||||||
|
package io.legado.app.model.webBook |
||||||
|
|
||||||
|
import android.text.TextUtils |
||||||
|
import io.legado.app.App |
||||||
|
import io.legado.app.R |
||||||
|
import io.legado.app.data.entities.Book |
||||||
|
import io.legado.app.data.entities.BookChapter |
||||||
|
import io.legado.app.data.entities.BookSource |
||||||
|
import io.legado.app.data.entities.rule.TocRule |
||||||
|
import io.legado.app.help.coroutine.Coroutine |
||||||
|
import io.legado.app.model.Debug |
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeRule |
||||||
|
import io.legado.app.model.analyzeRule.AnalyzeUrl |
||||||
|
import kotlinx.coroutines.CoroutineScope |
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine |
||||||
|
import kotlin.coroutines.resume |
||||||
|
import kotlin.coroutines.resumeWithException |
||||||
|
|
||||||
|
object BookChapterList { |
||||||
|
|
||||||
|
suspend fun analyzeChapterList( |
||||||
|
coroutineScope: CoroutineScope, |
||||||
|
book: Book, |
||||||
|
body: String?, |
||||||
|
bookSource: BookSource, |
||||||
|
baseUrl: String |
||||||
|
): List<BookChapter> = suspendCancellableCoroutine { block -> |
||||||
|
try { |
||||||
|
val chapterList = ArrayList<BookChapter>() |
||||||
|
body ?: throw Exception( |
||||||
|
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) |
||||||
|
) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") |
||||||
|
val tocRule = bookSource.getTocRule() |
||||||
|
val nextUrlList = arrayListOf(baseUrl) |
||||||
|
var reverse = false |
||||||
|
var listRule = tocRule.chapterList ?: "" |
||||||
|
if (listRule.startsWith("-")) { |
||||||
|
reverse = true |
||||||
|
listRule = listRule.substring(1) |
||||||
|
} |
||||||
|
if (listRule.startsWith("+")) { |
||||||
|
listRule = listRule.substring(1) |
||||||
|
} |
||||||
|
var chapterData = |
||||||
|
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true) |
||||||
|
chapterData.chapterList?.let { |
||||||
|
chapterList.addAll(it) |
||||||
|
} |
||||||
|
when (chapterData.nextUrl.size) { |
||||||
|
0 -> { |
||||||
|
block.resume(finish(book, chapterList, reverse)) |
||||||
|
} |
||||||
|
1 -> { |
||||||
|
Coroutine.async(scope = coroutineScope) { |
||||||
|
var nextUrl = chapterData.nextUrl[0] |
||||||
|
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { |
||||||
|
nextUrlList.add(nextUrl) |
||||||
|
AnalyzeUrl( |
||||||
|
ruleUrl = nextUrl, |
||||||
|
book = book, |
||||||
|
headerMapF = bookSource.getHeaderMap() |
||||||
|
).getResponseAwait() |
||||||
|
.body?.let { nextBody -> |
||||||
|
chapterData = analyzeChapterList( |
||||||
|
nextBody, nextUrl, tocRule, listRule, |
||||||
|
book, bookSource, log = false |
||||||
|
) |
||||||
|
nextUrl = if (chapterData.nextUrl.isNotEmpty()) { |
||||||
|
chapterData.nextUrl[0] |
||||||
|
} else "" |
||||||
|
chapterData.chapterList?.let { |
||||||
|
chapterList.addAll(it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") |
||||||
|
block.resume(finish(book, chapterList, reverse)) |
||||||
|
}.onError { |
||||||
|
block.resumeWithException(it) |
||||||
|
} |
||||||
|
} |
||||||
|
else -> { |
||||||
|
val chapterDataList = arrayListOf<ChapterData<String>>() |
||||||
|
for (item in chapterData.nextUrl) { |
||||||
|
if (!nextUrlList.contains(item)) { |
||||||
|
val data = ChapterData(nextUrl = item) |
||||||
|
chapterDataList.add(data) |
||||||
|
nextUrlList.add(item) |
||||||
|
} |
||||||
|
} |
||||||
|
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") |
||||||
|
for (item in chapterDataList) { |
||||||
|
Coroutine.async(scope = coroutineScope) { |
||||||
|
val nextBody = AnalyzeUrl( |
||||||
|
ruleUrl = item.nextUrl, |
||||||
|
book = book, |
||||||
|
headerMapF = bookSource.getHeaderMap() |
||||||
|
).getResponseAwait().body |
||||||
|
val nextChapterData = analyzeChapterList( |
||||||
|
nextBody, item.nextUrl, tocRule, listRule, book, bookSource |
||||||
|
) |
||||||
|
synchronized(chapterDataList) { |
||||||
|
val isFinished = addChapterListIsFinish( |
||||||
|
chapterDataList, |
||||||
|
item, |
||||||
|
nextChapterData.chapterList |
||||||
|
) |
||||||
|
if (isFinished) { |
||||||
|
chapterDataList.forEach { item -> |
||||||
|
item.chapterList?.let { |
||||||
|
chapterList.addAll(it) |
||||||
|
} |
||||||
|
} |
||||||
|
block.resume(finish(book, chapterList, reverse)) |
||||||
|
} |
||||||
|
} |
||||||
|
}.onError { |
||||||
|
block.resumeWithException(it) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
} catch (e: Exception) { |
||||||
|
block.resumeWithException(e) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private fun addChapterListIsFinish( |
||||||
|
chapterDataList: ArrayList<ChapterData<String>>, |
||||||
|
chapterData: ChapterData<String>, |
||||||
|
chapterList: List<BookChapter>? |
||||||
|
): Boolean { |
||||||
|
chapterData.chapterList = chapterList |
||||||
|
chapterDataList.forEach { |
||||||
|
if (it.chapterList == null) { |
||||||
|
return false |
||||||
|
} |
||||||
|
} |
||||||
|
return true |
||||||
|
} |
||||||
|
|
||||||
|
private fun finish( |
||||||
|
book: Book, |
||||||
|
chapterList: ArrayList<BookChapter>, |
||||||
|
reverse: Boolean |
||||||
|
): ArrayList<BookChapter> { |
||||||
|
//去重 |
||||||
|
if (!reverse) { |
||||||
|
chapterList.reverse() |
||||||
|
} |
||||||
|
val lh = LinkedHashSet(chapterList) |
||||||
|
val list = ArrayList(lh) |
||||||
|
list.reverse() |
||||||
|
Debug.log(book.origin, "◇目录总数:${list.size}") |
||||||
|
for ((index, item) in list.withIndex()) { |
||||||
|
item.index = index |
||||||
|
} |
||||||
|
book.latestChapterTitle = list.last().title |
||||||
|
book.durChapterTitle = |
||||||
|
list.getOrNull(book.durChapterIndex)?.title ?: book.latestChapterTitle |
||||||
|
if (book.totalChapterNum < list.size) { |
||||||
|
book.lastCheckCount = list.size - book.totalChapterNum |
||||||
|
} |
||||||
|
book.totalChapterNum = list.size |
||||||
|
return list |
||||||
|
} |
||||||
|
|
||||||
|
private fun analyzeChapterList( |
||||||
|
body: String?, |
||||||
|
baseUrl: String, |
||||||
|
tocRule: TocRule, |
||||||
|
listRule: String, |
||||||
|
book: Book, |
||||||
|
bookSource: BookSource, |
||||||
|
getNextUrl: Boolean = true, |
||||||
|
log: Boolean = false |
||||||
|
): ChapterData<List<String>> { |
||||||
|
val chapterList = arrayListOf<BookChapter>() |
||||||
|
val nextUrlList = arrayListOf<String>() |
||||||
|
val analyzeRule = AnalyzeRule(book) |
||||||
|
analyzeRule.setContent(body, baseUrl) |
||||||
|
val nextTocRule = tocRule.nextTocUrl |
||||||
|
if (getNextUrl && !nextTocRule.isNullOrEmpty()) { |
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) |
||||||
|
analyzeRule.getStringList(nextTocRule, true)?.let { |
||||||
|
for (item in it) { |
||||||
|
if (item != baseUrl) { |
||||||
|
nextUrlList.add(item) |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
|
Debug.log( |
||||||
|
bookSource.bookSourceUrl, |
||||||
|
"└" + TextUtils.join(",\n", nextUrlList), |
||||||
|
log |
||||||
|
) |
||||||
|
} |
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) |
||||||
|
val elements = analyzeRule.getElements(listRule) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) |
||||||
|
if (elements.isNotEmpty()) { |
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) |
||||||
|
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) |
||||||
|
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) |
||||||
|
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) |
||||||
|
val update = analyzeRule.splitSourceRule(tocRule.updateTime) |
||||||
|
var isVip: String? |
||||||
|
for (item in elements) { |
||||||
|
analyzeRule.setContent(item) |
||||||
|
val bookChapter = BookChapter(bookUrl = book.bookUrl) |
||||||
|
analyzeRule.chapter = bookChapter |
||||||
|
bookChapter.title = analyzeRule.getString(nameRule) |
||||||
|
bookChapter.url = analyzeRule.getString(urlRule, true) |
||||||
|
bookChapter.tag = analyzeRule.getString(update) |
||||||
|
isVip = analyzeRule.getString(vipRule) |
||||||
|
if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl |
||||||
|
if (bookChapter.title.isNotEmpty()) { |
||||||
|
if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") { |
||||||
|
bookChapter.title = "\uD83D\uDD12" + bookChapter.title |
||||||
|
} |
||||||
|
chapterList.add(bookChapter) |
||||||
|
} |
||||||
|
} |
||||||
|
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].title}", log) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].url}", log) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log) |
||||||
|
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].tag}", log) |
||||||
|
} |
||||||
|
return ChapterData(chapterList, nextUrlList) |
||||||
|
} |
||||||
|
|
||||||
|
} |
@ -1,4 +1,4 @@ |
|||||||
package io.legado.app.model.webbook |
package io.legado.app.model.webBook |
||||||
|
|
||||||
import io.legado.app.data.entities.BookChapter |
import io.legado.app.data.entities.BookChapter |
||||||
|
|
@ -1,4 +1,4 @@ |
|||||||
package io.legado.app.model.webbook |
package io.legado.app.model.webBook |
||||||
|
|
||||||
data class ContentData<T>( |
data class ContentData<T>( |
||||||
var content: String = "", |
var content: String = "", |
@ -1,176 +0,0 @@ |
|||||||
package io.legado.app.model.webbook |
|
||||||
|
|
||||||
import android.text.TextUtils |
|
||||||
import io.legado.app.App |
|
||||||
import io.legado.app.R |
|
||||||
import io.legado.app.data.entities.Book |
|
||||||
import io.legado.app.data.entities.BookChapter |
|
||||||
import io.legado.app.data.entities.BookSource |
|
||||||
import io.legado.app.data.entities.rule.TocRule |
|
||||||
import io.legado.app.model.Debug |
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeRule |
|
||||||
import io.legado.app.model.analyzeRule.AnalyzeUrl |
|
||||||
import kotlinx.coroutines.CoroutineScope |
|
||||||
import kotlinx.coroutines.withContext |
|
||||||
|
|
||||||
object BookChapterList { |
|
||||||
|
|
||||||
suspend fun analyzeChapterList( |
|
||||||
coroutineScope: CoroutineScope, |
|
||||||
book: Book, |
|
||||||
body: String?, |
|
||||||
bookSource: BookSource, |
|
||||||
baseUrl: String |
|
||||||
): List<BookChapter> { |
|
||||||
var chapterList = arrayListOf<BookChapter>() |
|
||||||
body ?: throw Exception( |
|
||||||
App.INSTANCE.getString(R.string.error_get_web_content, baseUrl) |
|
||||||
) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "≡获取成功:${baseUrl}") |
|
||||||
val tocRule = bookSource.getTocRule() |
|
||||||
val nextUrlList = arrayListOf(baseUrl) |
|
||||||
var reverse = false |
|
||||||
var listRule = tocRule.chapterList ?: "" |
|
||||||
if (listRule.startsWith("-")) { |
|
||||||
reverse = true |
|
||||||
listRule = listRule.substring(1) |
|
||||||
} |
|
||||||
if (listRule.startsWith("+")) { |
|
||||||
listRule = listRule.substring(1) |
|
||||||
} |
|
||||||
var chapterData = |
|
||||||
analyzeChapterList(body, baseUrl, tocRule, listRule, book, bookSource, log = true) |
|
||||||
chapterData.chapterList?.let { |
|
||||||
chapterList.addAll(it) |
|
||||||
} |
|
||||||
if (chapterData.nextUrl.size == 1) { |
|
||||||
var nextUrl = chapterData.nextUrl[0] |
|
||||||
while (nextUrl.isNotEmpty() && !nextUrlList.contains(nextUrl)) { |
|
||||||
nextUrlList.add(nextUrl) |
|
||||||
AnalyzeUrl( |
|
||||||
ruleUrl = nextUrl, book = book, headerMapF = bookSource.getHeaderMap() |
|
||||||
).getResponseAwait() |
|
||||||
.body?.let { nextBody -> |
|
||||||
chapterData = analyzeChapterList( |
|
||||||
nextBody, nextUrl, tocRule, listRule, |
|
||||||
book, bookSource, log = false |
|
||||||
) |
|
||||||
nextUrl = if (chapterData.nextUrl.isNotEmpty()) |
|
||||||
chapterData.nextUrl[0] |
|
||||||
else "" |
|
||||||
chapterData.chapterList?.let { |
|
||||||
chapterList.addAll(it) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Debug.log(bookSource.bookSourceUrl, "◇目录总页数:${nextUrlList.size}") |
|
||||||
} else if (chapterData.nextUrl.size > 1) { |
|
||||||
val chapterDataList = arrayListOf<ChapterData<String>>() |
|
||||||
for (item in chapterData.nextUrl) { |
|
||||||
val data = ChapterData(nextUrl = item) |
|
||||||
chapterDataList.add(data) |
|
||||||
} |
|
||||||
for (item in chapterDataList) { |
|
||||||
withContext(coroutineScope.coroutineContext) { |
|
||||||
val nextBody = AnalyzeUrl( |
|
||||||
ruleUrl = item.nextUrl, |
|
||||||
book = book, |
|
||||||
headerMapF = bookSource.getHeaderMap() |
|
||||||
).getResponseAwait().body |
|
||||||
val nextChapterData = analyzeChapterList( |
|
||||||
nextBody, item.nextUrl, tocRule, listRule, book, bookSource |
|
||||||
) |
|
||||||
item.chapterList = nextChapterData.chapterList |
|
||||||
} |
|
||||||
} |
|
||||||
for (item in chapterDataList) { |
|
||||||
item.chapterList?.let { |
|
||||||
chapterList.addAll(it) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
//去重 |
|
||||||
if (!reverse) { |
|
||||||
chapterList.reverse() |
|
||||||
} |
|
||||||
val lh = LinkedHashSet(chapterList) |
|
||||||
chapterList = ArrayList(lh) |
|
||||||
chapterList.reverse() |
|
||||||
for ((index, item) in chapterList.withIndex()) { |
|
||||||
item.index = index |
|
||||||
} |
|
||||||
book.latestChapterTitle = chapterList.last().title |
|
||||||
if (book.totalChapterNum < chapterList.size) { |
|
||||||
book.lastCheckCount = chapterList.size - book.totalChapterNum |
|
||||||
} |
|
||||||
book.totalChapterNum = chapterList.size |
|
||||||
return chapterList |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
private fun analyzeChapterList( |
|
||||||
body: String?, |
|
||||||
baseUrl: String, |
|
||||||
tocRule: TocRule, |
|
||||||
listRule: String, |
|
||||||
book: Book, |
|
||||||
bookSource: BookSource, |
|
||||||
getNextUrl: Boolean = true, |
|
||||||
log: Boolean = false |
|
||||||
): ChapterData<List<String>> { |
|
||||||
val chapterList = arrayListOf<BookChapter>() |
|
||||||
val nextUrlList = arrayListOf<String>() |
|
||||||
val analyzeRule = AnalyzeRule(book) |
|
||||||
analyzeRule.setContent(body, baseUrl) |
|
||||||
val nextTocRule = tocRule.nextTocUrl |
|
||||||
if (getNextUrl && !nextTocRule.isNullOrEmpty()) { |
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录下一页列表", log) |
|
||||||
analyzeRule.getStringList(nextTocRule, true)?.let { |
|
||||||
for (item in it) { |
|
||||||
if (item != baseUrl) { |
|
||||||
nextUrlList.add(item) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
Debug.log( |
|
||||||
bookSource.bookSourceUrl, |
|
||||||
"└" + TextUtils.join(",\n", nextUrlList), |
|
||||||
log |
|
||||||
) |
|
||||||
} |
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取目录列表", log) |
|
||||||
val elements = analyzeRule.getElements(listRule) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "└列表大小:${elements.size}", log) |
|
||||||
if (elements.isNotEmpty()) { |
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取首章名称", log) |
|
||||||
val nameRule = analyzeRule.splitSourceRule(tocRule.chapterName) |
|
||||||
val urlRule = analyzeRule.splitSourceRule(tocRule.chapterUrl) |
|
||||||
val vipRule = analyzeRule.splitSourceRule(tocRule.isVip) |
|
||||||
val update = analyzeRule.splitSourceRule(tocRule.updateTime) |
|
||||||
var isVip: String? |
|
||||||
for (item in elements) { |
|
||||||
analyzeRule.setContent(item) |
|
||||||
val bookChapter = BookChapter(bookUrl = book.bookUrl) |
|
||||||
analyzeRule.chapter = bookChapter |
|
||||||
bookChapter.title = analyzeRule.getString(nameRule) |
|
||||||
bookChapter.url = analyzeRule.getString(urlRule, true) |
|
||||||
bookChapter.tag = analyzeRule.getString(update) |
|
||||||
isVip = analyzeRule.getString(vipRule) |
|
||||||
if (bookChapter.url.isEmpty()) bookChapter.url = baseUrl |
|
||||||
if (bookChapter.title.isNotEmpty()) { |
|
||||||
if (isVip.isNotEmpty() && isVip != "null" && isVip != "false" && isVip != "0") { |
|
||||||
bookChapter.title = "\uD83D\uDD12" + bookChapter.title |
|
||||||
} |
|
||||||
chapterList.add(bookChapter) |
|
||||||
} |
|
||||||
} |
|
||||||
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].title}", log) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取首章链接", log) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].url}", log) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "┌获取首章信息", log) |
|
||||||
Debug.log(bookSource.bookSourceUrl, "└${chapterList[0].tag}", log) |
|
||||||
} |
|
||||||
return ChapterData(chapterList, nextUrlList) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue