commit
4e8093ebd0
@ -0,0 +1,4 @@ |
||||
# 1.0.0 (2020-02-09) |
||||
|
||||
|
||||
|
@ -1,2 +1,9 @@ |
||||
# legado |
||||
|
||||
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) |
||||
|
||||
## 阅读3.0 |
||||
书源规则 https://celeter.github.io/?tdsourcetag=s_pctim_aiomsg |
||||
|
||||
## 免责声明 |
||||
https://gedoor.github.io/MyBookshelf/disclaimer.html |
||||
|
@ -1,72 +1,62 @@ |
||||
[ |
||||
{ |
||||
"bgStr": "羊皮纸2.jpg", |
||||
"bgType": 1, |
||||
"bgStr": "#EBD9BB", |
||||
"bgStrNight": "#1E2021", |
||||
"textColor": "#63543C", |
||||
"textColorNight": "#DCDFE1", |
||||
"bgType": 0, |
||||
"bgTypeNight": 0, |
||||
"darkStatusIcon": true, |
||||
"textColor": "#5E432E", |
||||
"textSize": 24, |
||||
"letterSpacing": 0, |
||||
"lineSpacingExtra": 10, |
||||
"lineSpacingMultiplier": 1.2, |
||||
"paddingLeft": 16, |
||||
"paddingRight": 16, |
||||
"paddingTop": 0, |
||||
"paddingBottom": 0 |
||||
"lineSpacingExtra": 10 |
||||
}, |
||||
{ |
||||
"bgStr": "#C6BAA1", |
||||
"bgStr": "#DDC090", |
||||
"bgStrNight": "#3C3F43", |
||||
"textColor": "#3E3422", |
||||
"textColorNight": "#DCDFE1", |
||||
"bgType": 0, |
||||
"bgTypeNight": 0, |
||||
"darkStatusIcon": true, |
||||
"textColor": "#5E432E", |
||||
"textSize": 24, |
||||
"letterSpacing": 0, |
||||
"lineSpacingExtra": 10, |
||||
"lineSpacingMultiplier": 1.2, |
||||
"paddingLeft": 16, |
||||
"paddingRight": 16, |
||||
"paddingTop": 0, |
||||
"paddingBottom": 0 |
||||
"lineSpacingExtra": 10 |
||||
}, |
||||
{ |
||||
"bgStr": "#015A86", |
||||
"bgStr": "#C2D8AA", |
||||
"bgStrNight": "#3C3F43", |
||||
"textColor": "#596C44", |
||||
"textColorNight": "#88C16F", |
||||
"bgType": 0, |
||||
"bgTypeNight": 0, |
||||
"darkStatusIcon": false, |
||||
"textColor": "#FFFFFF", |
||||
"textSize": 24, |
||||
"letterSpacing": 0, |
||||
"lineSpacingExtra": 10, |
||||
"lineSpacingMultiplier": 1.2, |
||||
"paddingLeft": 16, |
||||
"paddingRight": 16, |
||||
"paddingTop": 0, |
||||
"paddingBottom": 0 |
||||
"lineSpacingExtra": 10 |
||||
}, |
||||
{ |
||||
"bgStr": "宁静夜色", |
||||
"bgType": 1, |
||||
"bgStr": "#DBB8E2", |
||||
"bgStrNight": "#3C3F43", |
||||
"textColor": "#68516C", |
||||
"textColorNight": "#F6AEAE", |
||||
"bgType": 0, |
||||
"bgTypeNight": 0, |
||||
"darkStatusIcon": false, |
||||
"textColor": "#adadad", |
||||
"textSize": 24, |
||||
"letterSpacing": 0, |
||||
"lineSpacingExtra": 10, |
||||
"lineSpacingMultiplier": 1.2, |
||||
"paddingLeft": 16, |
||||
"paddingRight": 16, |
||||
"paddingTop": 0, |
||||
"paddingBottom": 0 |
||||
"lineSpacingExtra": 10 |
||||
}, |
||||
{ |
||||
"bgStr": "#000000", |
||||
"bgStr": "#ABCEE0", |
||||
"bgStrNight": "#3C3F43", |
||||
"textColor": "#3D4C54", |
||||
"textColorNight": "#90BFF5", |
||||
"bgType": 0, |
||||
"bgTypeNight": 0, |
||||
"darkStatusIcon": false, |
||||
"textColor": "#adadad", |
||||
"textSize": 24, |
||||
"letterSpacing": 0, |
||||
"lineSpacingExtra": 10, |
||||
"lineSpacingMultiplier": 1.2, |
||||
"paddingLeft": 16, |
||||
"paddingRight": 16, |
||||
"paddingTop": 0, |
||||
"paddingBottom": 0 |
||||
"lineSpacingExtra": 10 |
||||
} |
||||
] |
@ -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 |
||||
|
||||
interface OnPermissionsDeniedCallback { |
||||
|
||||
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过来的库 |
||||
* 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 |
||||
|
@ -1,4 +1,4 @@ |
||||
package io.legado.app.model.webbook |
||||
package io.legado.app.model.webBook |
||||
|
||||
data class ContentData<T>( |
||||
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