fetch太慢去除历史提交记录
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package io.legado.app
|
||||
|
||||
import android.app.NotificationChannel
|
||||
import android.app.NotificationManager
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.os.Build
|
||||
import androidx.multidex.MultiDexApplication
|
||||
import com.jeremyliao.liveeventbus.LiveEventBus
|
||||
import io.legado.app.base.AppContextWrapper
|
||||
import io.legado.app.constant.AppConst.channelIdDownload
|
||||
import io.legado.app.constant.AppConst.channelIdReadAloud
|
||||
import io.legado.app.constant.AppConst.channelIdWeb
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.CrashHandler
|
||||
import io.legado.app.help.LifecycleHelp
|
||||
import io.legado.app.help.ThemeConfig.applyDayNight
|
||||
import io.legado.app.help.http.cronet.CronetLoader
|
||||
import io.legado.app.utils.defaultSharedPreferences
|
||||
import timber.log.Timber
|
||||
|
||||
class App : MultiDexApplication() {
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
CrashHandler(this)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
//预下载Cronet so
|
||||
CronetLoader.preDownload()
|
||||
createNotificationChannels()
|
||||
applyDayNight(this)
|
||||
LiveEventBus.config()
|
||||
.lifecycleObserverAlwaysActive(true)
|
||||
.autoClear(false)
|
||||
registerActivityLifecycleCallbacks(LifecycleHelp)
|
||||
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(AppConfig)
|
||||
}
|
||||
|
||||
override fun attachBaseContext(base: Context) {
|
||||
super.attachBaseContext(AppContextWrapper.wrap(base))
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
when (newConfig.uiMode and Configuration.UI_MODE_NIGHT_MASK) {
|
||||
Configuration.UI_MODE_NIGHT_YES,
|
||||
Configuration.UI_MODE_NIGHT_NO -> applyDayNight(this)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建通知ID
|
||||
*/
|
||||
private fun createNotificationChannels() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
||||
(getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager)?.let {
|
||||
val downloadChannel = NotificationChannel(
|
||||
channelIdDownload,
|
||||
getString(R.string.action_download),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
|
||||
val readAloudChannel = NotificationChannel(
|
||||
channelIdReadAloud,
|
||||
getString(R.string.read_aloud),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
|
||||
val webChannel = NotificationChannel(
|
||||
channelIdWeb,
|
||||
getString(R.string.web_service),
|
||||
NotificationManager.IMPORTANCE_DEFAULT
|
||||
).apply {
|
||||
enableLights(false)
|
||||
enableVibration(false)
|
||||
setSound(null, null)
|
||||
}
|
||||
|
||||
//向notification manager 提交channel
|
||||
it.createNotificationChannels(listOf(downloadChannel, readAloudChannel, webChannel))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
# 文件结构介绍
|
||||
|
||||
* base 基类
|
||||
* constant 常量
|
||||
* data 数据
|
||||
* help 帮助
|
||||
* lib 库
|
||||
* model 解析
|
||||
* receiver 广播侦听
|
||||
* service 服务
|
||||
* ui 界面
|
||||
* web web服务
|
||||
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* Copyright (C) 2020 w568w
|
||||
*/
|
||||
package io.legado.app.api
|
||||
|
||||
import android.content.ContentProvider
|
||||
import android.content.ContentValues
|
||||
import android.content.UriMatcher
|
||||
import android.database.Cursor
|
||||
import android.database.MatrixCursor
|
||||
import android.net.Uri
|
||||
import com.google.gson.Gson
|
||||
import io.legado.app.api.controller.BookController
|
||||
import io.legado.app.api.controller.BookSourceController
|
||||
import io.legado.app.api.controller.RssSourceController
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Export book data to other app.
|
||||
*/
|
||||
class ReaderProvider : ContentProvider() {
|
||||
private enum class RequestCode {
|
||||
SaveBookSource, SaveBookSources, DeleteBookSources, GetBookSource, GetBookSources,
|
||||
SaveRssSource, SaveRssSources, DeleteRssSources, GetRssSource, GetRssSources,
|
||||
SaveBook, GetBookshelf, RefreshToc, GetChapterList, GetBookContent, GetBookCover
|
||||
}
|
||||
|
||||
private val postBodyKey = "json"
|
||||
private val sMatcher by lazy {
|
||||
UriMatcher(UriMatcher.NO_MATCH).apply {
|
||||
"${context?.applicationInfo?.packageName}.readerProvider".also { authority ->
|
||||
addURI(authority, "bookSource/insert", RequestCode.SaveBookSource.ordinal)
|
||||
addURI(authority, "bookSources/insert", RequestCode.SaveBookSources.ordinal)
|
||||
addURI(authority, "bookSources/delete", RequestCode.DeleteBookSources.ordinal)
|
||||
addURI(authority, "bookSource/query", RequestCode.GetBookSource.ordinal)
|
||||
addURI(authority, "bookSources/query", RequestCode.GetBookSources.ordinal)
|
||||
addURI(authority, "rssSource/insert", RequestCode.SaveBookSource.ordinal)
|
||||
addURI(authority, "rssSources/insert", RequestCode.SaveBookSources.ordinal)
|
||||
addURI(authority, "rssSources/delete", RequestCode.DeleteBookSources.ordinal)
|
||||
addURI(authority, "rssSource/query", RequestCode.GetBookSource.ordinal)
|
||||
addURI(authority, "rssSources/query", RequestCode.GetBookSources.ordinal)
|
||||
addURI(authority, "book/insert", RequestCode.SaveBook.ordinal)
|
||||
addURI(authority, "books/query", RequestCode.GetBookshelf.ordinal)
|
||||
addURI(authority, "book/refreshToc/query", RequestCode.RefreshToc.ordinal)
|
||||
addURI(authority, "book/chapter/query", RequestCode.GetChapterList.ordinal)
|
||||
addURI(authority, "book/content/query", RequestCode.GetBookContent.ordinal)
|
||||
addURI(authority, "book/cover/query", RequestCode.GetBookCover.ordinal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreate() = false
|
||||
|
||||
override fun delete(
|
||||
uri: Uri,
|
||||
selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
): Int {
|
||||
if (sMatcher.match(uri) < 0) return -1
|
||||
when (RequestCode.values()[sMatcher.match(uri)]) {
|
||||
RequestCode.DeleteBookSources -> BookSourceController.deleteSources(selection)
|
||||
RequestCode.DeleteRssSources -> BookSourceController.deleteSources(selection)
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||
)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun getType(uri: Uri) = throw UnsupportedOperationException("Not yet implemented")
|
||||
|
||||
override fun insert(uri: Uri, values: ContentValues?): Uri? {
|
||||
if (sMatcher.match(uri) < 0) return null
|
||||
when (RequestCode.values()[sMatcher.match(uri)]) {
|
||||
RequestCode.SaveBookSource -> values?.let {
|
||||
BookSourceController.saveSource(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveBookSources -> values?.let {
|
||||
BookSourceController.saveSources(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveRssSource -> values?.let {
|
||||
RssSourceController.saveSource(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveRssSources -> values?.let {
|
||||
RssSourceController.saveSources(values.getAsString(postBodyKey))
|
||||
}
|
||||
RequestCode.SaveBook -> values?.let {
|
||||
BookController.saveBook(values.getAsString(postBodyKey))
|
||||
}
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||
)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
override fun query(
|
||||
uri: Uri, projection: Array<String>?, selection: String?,
|
||||
selectionArgs: Array<String>?, sortOrder: String?
|
||||
): Cursor? {
|
||||
val map: MutableMap<String, ArrayList<String>> = HashMap()
|
||||
uri.getQueryParameter("url")?.let {
|
||||
map["url"] = arrayListOf(it)
|
||||
}
|
||||
uri.getQueryParameter("index")?.let {
|
||||
map["index"] = arrayListOf(it)
|
||||
}
|
||||
uri.getQueryParameter("path")?.let {
|
||||
map["path"] = arrayListOf(it)
|
||||
}
|
||||
return if (sMatcher.match(uri) < 0) null else when (RequestCode.values()[sMatcher.match(uri)]) {
|
||||
RequestCode.GetBookSource -> SimpleCursor(BookSourceController.getSource(map))
|
||||
RequestCode.GetBookSources -> SimpleCursor(BookSourceController.sources)
|
||||
RequestCode.GetRssSource -> SimpleCursor(RssSourceController.getSource(map))
|
||||
RequestCode.GetRssSources -> SimpleCursor(RssSourceController.sources)
|
||||
RequestCode.GetBookshelf -> SimpleCursor(BookController.bookshelf)
|
||||
RequestCode.GetBookContent -> SimpleCursor(BookController.getBookContent(map))
|
||||
RequestCode.RefreshToc -> SimpleCursor(BookController.refreshToc(map))
|
||||
RequestCode.GetChapterList -> SimpleCursor(BookController.getChapterList(map))
|
||||
RequestCode.GetBookCover -> SimpleCursor(BookController.getCover(map))
|
||||
else -> throw IllegalStateException(
|
||||
"Unexpected value: " + RequestCode.values()[sMatcher.match(uri)].name
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
override fun update(
|
||||
uri: Uri, values: ContentValues?, selection: String?,
|
||||
selectionArgs: Array<String>?
|
||||
) = throw UnsupportedOperationException("Not yet implemented")
|
||||
|
||||
|
||||
/**
|
||||
* Simple inner class to deliver json callback data.
|
||||
*
|
||||
* Only getString() makes sense.
|
||||
*/
|
||||
private class SimpleCursor(data: ReturnData?) : MatrixCursor(arrayOf("result"), 1) {
|
||||
|
||||
private val mData: String = Gson().toJson(data)
|
||||
|
||||
init {
|
||||
addRow(arrayOf(mData))
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legado.app.api
|
||||
|
||||
|
||||
class ReturnData {
|
||||
|
||||
var isSuccess: Boolean = false
|
||||
private set
|
||||
|
||||
var errorMsg: String = "未知错误,请联系开发者!"
|
||||
private set
|
||||
|
||||
var data: Any? = null
|
||||
private set
|
||||
|
||||
fun setErrorMsg(errorMsg: String): ReturnData {
|
||||
this.isSuccess = false
|
||||
this.errorMsg = errorMsg
|
||||
return this
|
||||
}
|
||||
|
||||
fun setData(data: Any): ReturnData {
|
||||
this.isSuccess = true
|
||||
this.errorMsg = ""
|
||||
this.data = data
|
||||
return this
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package io.legado.app.api.controller
|
||||
|
||||
import android.util.Base64
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import io.legado.app.R
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.help.BookHelp
|
||||
import io.legado.app.help.ContentProcessor
|
||||
import io.legado.app.help.glide.ImageLoader
|
||||
import io.legado.app.help.storage.AppWebDav
|
||||
import io.legado.app.model.BookCover
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.model.localBook.EpubFile
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.model.localBook.UmdFile
|
||||
import io.legado.app.model.webBook.WebBook
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import splitties.init.appCtx
|
||||
import timber.log.Timber
|
||||
|
||||
object BookController {
|
||||
|
||||
/**
|
||||
* 书架所有书籍
|
||||
*/
|
||||
val bookshelf: ReturnData
|
||||
get() {
|
||||
val books = appDb.bookDao.all
|
||||
val returnData = ReturnData()
|
||||
return if (books.isEmpty()) {
|
||||
returnData.setErrorMsg("还没有添加小说")
|
||||
} else {
|
||||
val data = when (appCtx.getPrefInt(PreferKey.bookshelfSort)) {
|
||||
1 -> books.sortedByDescending { it.latestChapterTime }
|
||||
2 -> books.sortedWith { o1, o2 ->
|
||||
o1.name.cnCompare(o2.name)
|
||||
}
|
||||
3 -> books.sortedBy { it.order }
|
||||
else -> books.sortedByDescending { it.durChapterTime }
|
||||
}
|
||||
returnData.setData(data)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCover(parameters: Map<String, List<String>>): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
val coverPath = parameters["path"]?.firstOrNull()
|
||||
val ftBitmap = ImageLoader.loadBitmap(appCtx, coverPath).submit()
|
||||
return try {
|
||||
returnData.setData(ftBitmap.get())
|
||||
} catch (e: Exception) {
|
||||
returnData.setData(BookCover.defaultDrawable.toBitmap())
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新目录
|
||||
*/
|
||||
fun refreshToc(parameters: Map<String, List<String>>): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
try {
|
||||
val bookUrl = parameters["url"]?.firstOrNull()
|
||||
if (bookUrl.isNullOrEmpty()) {
|
||||
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
|
||||
}
|
||||
val book = appDb.bookDao.getBook(bookUrl)
|
||||
?: return returnData.setErrorMsg("bookUrl不对")
|
||||
if (book.isLocalBook()) {
|
||||
val toc = LocalBook.getChapterList(book)
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
appDb.bookDao.update(book)
|
||||
return returnData.setData(toc)
|
||||
} else {
|
||||
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: return returnData.setErrorMsg("未找到对应书源,请换源")
|
||||
val toc = runBlocking {
|
||||
if (book.tocUrl.isBlank()) {
|
||||
WebBook.getBookInfoAwait(this, bookSource, book)
|
||||
}
|
||||
WebBook.getChapterListAwait(this, bookSource, book)
|
||||
}
|
||||
appDb.bookChapterDao.delByBook(book.bookUrl)
|
||||
appDb.bookChapterDao.insert(*toc.toTypedArray())
|
||||
appDb.bookDao.update(book)
|
||||
return returnData.setData(toc)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return returnData.setErrorMsg(e.localizedMessage ?: "refresh toc error")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取目录
|
||||
*/
|
||||
fun getChapterList(parameters: Map<String, List<String>>): ReturnData {
|
||||
val bookUrl = parameters["url"]?.firstOrNull()
|
||||
val returnData = ReturnData()
|
||||
if (bookUrl.isNullOrEmpty()) {
|
||||
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
|
||||
}
|
||||
val chapterList = appDb.bookChapterDao.getChapterList(bookUrl)
|
||||
if (chapterList.isEmpty()) {
|
||||
return refreshToc(parameters)
|
||||
}
|
||||
return returnData.setData(chapterList)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取正文
|
||||
*/
|
||||
fun getBookContent(parameters: Map<String, List<String>>): ReturnData {
|
||||
val bookUrl = parameters["url"]?.firstOrNull()
|
||||
val index = parameters["index"]?.firstOrNull()?.toInt()
|
||||
val returnData = ReturnData()
|
||||
if (bookUrl.isNullOrEmpty()) {
|
||||
return returnData.setErrorMsg("参数url不能为空,请指定书籍地址")
|
||||
}
|
||||
if (index == null) {
|
||||
return returnData.setErrorMsg("参数index不能为空, 请指定目录序号")
|
||||
}
|
||||
val book = appDb.bookDao.getBook(bookUrl)
|
||||
val chapter = appDb.bookChapterDao.getChapter(bookUrl, index)
|
||||
if (book == null || chapter == null) {
|
||||
return returnData.setErrorMsg("未找到")
|
||||
}
|
||||
var content: String? = BookHelp.getContent(book, chapter)
|
||||
if (content != null) {
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
return returnData.setData(
|
||||
contentProcessor.getContent(book, chapter, content, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
)
|
||||
}
|
||||
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
|
||||
?: return returnData.setErrorMsg("未找到书源")
|
||||
try {
|
||||
content = runBlocking {
|
||||
WebBook.getContentAwait(this, bookSource, book, chapter)
|
||||
}
|
||||
val contentProcessor = ContentProcessor.get(book.name, book.origin)
|
||||
saveBookReadIndex(book, index)
|
||||
returnData.setData(
|
||||
contentProcessor.getContent(book, chapter, content, includeTitle = false)
|
||||
.joinToString("\n")
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
returnData.setErrorMsg(e.msg)
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
|
||||
fun saveBook(postData: String?): ReturnData {
|
||||
val book = GSON.fromJsonObject<Book>(postData)
|
||||
val returnData = ReturnData()
|
||||
if (book != null) {
|
||||
book.save()
|
||||
AppWebDav.uploadBookProgress(book)
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = book
|
||||
ReadBook.durChapterIndex = book.durChapterIndex
|
||||
}
|
||||
return returnData.setData("")
|
||||
}
|
||||
return returnData.setErrorMsg("格式不对")
|
||||
}
|
||||
|
||||
private fun saveBookReadIndex(book: Book, index: Int) {
|
||||
book.durChapterIndex = index
|
||||
book.durChapterTime = System.currentTimeMillis()
|
||||
appDb.bookChapterDao.getChapter(book.bookUrl, index)?.let {
|
||||
book.durChapterTitle = it.title
|
||||
}
|
||||
appDb.bookDao.update(book)
|
||||
AppWebDav.uploadBookProgress(book)
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = book
|
||||
ReadBook.durChapterIndex = index
|
||||
ReadBook.loadContent(index)
|
||||
}
|
||||
}
|
||||
|
||||
fun addLocalBook(parameters: Map<String, List<String>>): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
try {
|
||||
val fileName = parameters["fileName"]?.firstOrNull()
|
||||
?: return returnData.setErrorMsg("fileName 不能为空")
|
||||
val fileData = parameters["fileData"]?.firstOrNull()
|
||||
?: return returnData.setErrorMsg("fileData 不能为空")
|
||||
val file = FileUtils.createFileIfNotExist(LocalBook.cacheFolder, fileName)
|
||||
val fileBytes = Base64.decode(fileData.substringAfter("base64,"), Base64.DEFAULT)
|
||||
file.writeBytes(fileBytes)
|
||||
val nameAuthor = LocalBook.analyzeNameAuthor(fileName)
|
||||
val book = Book(
|
||||
bookUrl = file.absolutePath,
|
||||
name = nameAuthor.first,
|
||||
author = nameAuthor.second,
|
||||
originName = fileName,
|
||||
coverUrl = FileUtils.getPath(
|
||||
appCtx.externalFiles,
|
||||
"covers",
|
||||
"${MD5Utils.md5Encode16(file.absolutePath)}.jpg"
|
||||
)
|
||||
)
|
||||
if (book.isEpub()) EpubFile.upBookInfo(book)
|
||||
if (book.isUmd()) UmdFile.upBookInfo(book)
|
||||
appDb.bookDao.insert(book)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return returnData.setErrorMsg(
|
||||
e.localizedMessage ?: appCtx.getString(R.string.unknown_error)
|
||||
)
|
||||
}
|
||||
return returnData.setData(true)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package io.legado.app.api.controller
|
||||
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.msg
|
||||
|
||||
object BookSourceController {
|
||||
|
||||
val sources: ReturnData
|
||||
get() {
|
||||
val bookSources = appDb.bookSourceDao.all
|
||||
val returnData = ReturnData()
|
||||
return if (bookSources.isEmpty()) {
|
||||
returnData.setErrorMsg("设备源列表为空")
|
||||
} else returnData.setData(bookSources)
|
||||
}
|
||||
|
||||
fun saveSource(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
postData ?: return returnData.setErrorMsg("数据不能为空")
|
||||
kotlin.runCatching {
|
||||
val bookSource = BookSource.fromJson(postData)
|
||||
if (bookSource != null) {
|
||||
if (TextUtils.isEmpty(bookSource.bookSourceName) || TextUtils.isEmpty(bookSource.bookSourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
returnData.setData("")
|
||||
}
|
||||
} else {
|
||||
returnData.setErrorMsg("转换源失败")
|
||||
}
|
||||
}.onFailure {
|
||||
returnData.setErrorMsg(it.msg)
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
|
||||
fun saveSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("数据为空")
|
||||
val okSources = arrayListOf<BookSource>()
|
||||
val bookSources = BookSource.fromJsonArray(postData)
|
||||
if (bookSources.isNotEmpty()) {
|
||||
bookSources.forEach { bookSource ->
|
||||
if (bookSource.bookSourceName.isNotBlank()
|
||||
&& bookSource.bookSourceUrl.isNotBlank()
|
||||
) {
|
||||
appDb.bookSourceDao.insert(bookSource)
|
||||
okSources.add(bookSource)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ReturnData().setErrorMsg("转换源失败")
|
||||
}
|
||||
return ReturnData().setData(okSources)
|
||||
}
|
||||
|
||||
fun getSource(parameters: Map<String, List<String>>): ReturnData {
|
||||
val url = parameters["url"]?.firstOrNull()
|
||||
val returnData = ReturnData()
|
||||
if (url.isNullOrEmpty()) {
|
||||
return returnData.setErrorMsg("参数url不能为空,请指定源地址")
|
||||
}
|
||||
val bookSource = appDb.bookSourceDao.getBookSource(url)
|
||||
?: return returnData.setErrorMsg("未找到源,请检查书源地址")
|
||||
return returnData.setData(bookSource)
|
||||
}
|
||||
|
||||
fun deleteSources(postData: String?): ReturnData {
|
||||
kotlin.runCatching {
|
||||
GSON.fromJsonArray<BookSource>(postData)?.let {
|
||||
it.forEach { source ->
|
||||
appDb.bookSourceDao.delete(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ReturnData().setData("已执行"/*okSources*/)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.legado.app.api.controller
|
||||
|
||||
|
||||
import android.text.TextUtils
|
||||
import io.legado.app.api.ReturnData
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.utils.msg
|
||||
|
||||
object RssSourceController {
|
||||
|
||||
val sources: ReturnData
|
||||
get() {
|
||||
val source = appDb.rssSourceDao.all
|
||||
val returnData = ReturnData()
|
||||
return if (source.isEmpty()) {
|
||||
returnData.setErrorMsg("源列表为空")
|
||||
} else returnData.setData(source)
|
||||
}
|
||||
|
||||
fun saveSource(postData: String?): ReturnData {
|
||||
val returnData = ReturnData()
|
||||
postData ?: return returnData.setErrorMsg("数据不能为空")
|
||||
kotlin.runCatching {
|
||||
val source = RssSource.fromJson(postData)
|
||||
if (source != null) {
|
||||
if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
|
||||
returnData.setErrorMsg("源名称和URL不能为空")
|
||||
} else {
|
||||
appDb.rssSourceDao.insert(source)
|
||||
returnData.setData("")
|
||||
}
|
||||
} else {
|
||||
returnData.setErrorMsg("转换源失败")
|
||||
}
|
||||
}.onFailure {
|
||||
returnData.setErrorMsg(it.msg)
|
||||
}
|
||||
return returnData
|
||||
}
|
||||
|
||||
fun saveSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("数据不能为空")
|
||||
val okSources = arrayListOf<RssSource>()
|
||||
val source = RssSource.fromJsonArray(postData)
|
||||
if (source.isNotEmpty()) {
|
||||
for (rssSource in source) {
|
||||
if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
|
||||
continue
|
||||
}
|
||||
appDb.rssSourceDao.insert(rssSource)
|
||||
okSources.add(rssSource)
|
||||
}
|
||||
} else {
|
||||
return ReturnData().setErrorMsg("转换源失败")
|
||||
}
|
||||
return ReturnData().setData(okSources)
|
||||
}
|
||||
|
||||
fun getSource(parameters: Map<String, List<String>>): ReturnData {
|
||||
val url = parameters["url"]?.firstOrNull()
|
||||
val returnData = ReturnData()
|
||||
if (url.isNullOrEmpty()) {
|
||||
return returnData.setErrorMsg("参数url不能为空,请指定书源地址")
|
||||
}
|
||||
val source = appDb.rssSourceDao.getByKey(url)
|
||||
?: return returnData.setErrorMsg("未找到源,请检查源地址")
|
||||
return returnData.setData(source)
|
||||
}
|
||||
|
||||
fun deleteSources(postData: String?): ReturnData {
|
||||
postData ?: return ReturnData().setErrorMsg("没有传递数据")
|
||||
kotlin.runCatching {
|
||||
RssSource.fromJsonArray(postData).let {
|
||||
it.forEach { source ->
|
||||
appDb.rssSourceDao.delete(source)
|
||||
}
|
||||
}
|
||||
}
|
||||
return ReturnData().setData("已执行"/*okSources*/)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.content.res.Resources
|
||||
import android.os.Build
|
||||
import android.os.LocaleList
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.utils.getPrefInt
|
||||
import io.legado.app.utils.getPrefString
|
||||
import io.legado.app.utils.sysConfiguration
|
||||
import java.util.*
|
||||
|
||||
|
||||
@Suppress("unused")
|
||||
object AppContextWrapper {
|
||||
|
||||
fun wrap(context: Context): Context {
|
||||
|
||||
val resources: Resources = context.resources
|
||||
val configuration: Configuration = resources.configuration
|
||||
val targetLocale = getSetLocale(context)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
configuration.setLocale(targetLocale)
|
||||
configuration.setLocales(LocaleList(targetLocale))
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
configuration.locale = targetLocale
|
||||
}
|
||||
configuration.fontScale = getFontScale(context)
|
||||
return context.createConfigurationContext(configuration)
|
||||
}
|
||||
|
||||
fun getFontScale(context: Context): Float {
|
||||
var fontScale = context.getPrefInt(PreferKey.fontScale) / 10f
|
||||
if (fontScale !in 0.8f..1.6f) {
|
||||
fontScale = sysConfiguration.fontScale
|
||||
}
|
||||
return fontScale
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前系统语言
|
||||
*/
|
||||
private fun getSystemLocale(): Locale {
|
||||
val locale: Locale
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { //7.0有多语言设置获取顶部的语言
|
||||
locale = sysConfiguration.locales.get(0)
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
locale = sysConfiguration.locale
|
||||
}
|
||||
return locale
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前App语言
|
||||
*/
|
||||
private fun getAppLocale(context: Context): Locale {
|
||||
val locale: Locale
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
locale = context.resources.configuration.locales[0]
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
locale = context.resources.configuration.locale
|
||||
}
|
||||
return locale
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* 当前设置语言
|
||||
*/
|
||||
private fun getSetLocale(context: Context): Locale {
|
||||
return when (context.getPrefString(PreferKey.language)) {
|
||||
"zh" -> Locale.SIMPLIFIED_CHINESE
|
||||
"tw" -> Locale.TRADITIONAL_CHINESE
|
||||
"en" -> Locale.ENGLISH
|
||||
else -> getSystemLocale()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断App语言和设置语言是否相同
|
||||
*/
|
||||
fun isSameWithSetting(context: Context): Boolean {
|
||||
val locale = getAppLocale(context)
|
||||
val language = locale.language
|
||||
val country = locale.country
|
||||
val pfLocale = getSetLocale(context)
|
||||
val pfLanguage = pfLocale.language
|
||||
val pfCountry = pfLocale.country
|
||||
return language == pfLanguage && country == pfCountry
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,190 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.content.Context
|
||||
import android.content.res.Configuration
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.AttributeSet
|
||||
import android.view.Menu
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import android.widget.FrameLayout
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.constant.Theme
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.ThemeConfig
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import io.legado.app.lib.theme.backgroundColor
|
||||
import io.legado.app.lib.theme.primaryColor
|
||||
import io.legado.app.ui.widget.TitleBar
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
|
||||
abstract class BaseActivity<VB : ViewBinding>(
|
||||
val fullScreen: Boolean = true,
|
||||
private val theme: Theme = Theme.Auto,
|
||||
private val toolBarTheme: Theme = Theme.Auto,
|
||||
private val transparent: Boolean = false,
|
||||
private val imageBg: Boolean = true
|
||||
) : AppCompatActivity(), CoroutineScope by MainScope() {
|
||||
|
||||
protected abstract val binding: VB
|
||||
|
||||
val isInMultiWindow: Boolean
|
||||
get() {
|
||||
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
isInMultiWindowMode
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
override fun attachBaseContext(newBase: Context) {
|
||||
super.attachBaseContext(AppContextWrapper.wrap(newBase))
|
||||
}
|
||||
|
||||
override fun onCreateView(
|
||||
parent: View?,
|
||||
name: String,
|
||||
context: Context,
|
||||
attrs: AttributeSet
|
||||
): View? {
|
||||
if (AppConst.menuViewNames.contains(name) && parent?.parent is FrameLayout) {
|
||||
(parent.parent as View).setBackgroundColor(backgroundColor)
|
||||
}
|
||||
return super.onCreateView(parent, name, context, attrs)
|
||||
}
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
window.decorView.disableAutoFill()
|
||||
initTheme()
|
||||
super.onCreate(savedInstanceState)
|
||||
setContentView(binding.root)
|
||||
setupSystemBar()
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
||||
findViewById<TitleBar>(R.id.title_bar)
|
||||
?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
|
||||
}
|
||||
onActivityCreated(savedInstanceState)
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean, newConfig: Configuration?) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode, newConfig)
|
||||
findViewById<TitleBar>(R.id.title_bar)
|
||||
?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
|
||||
setupSystemBar()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
findViewById<TitleBar>(R.id.title_bar)
|
||||
?.onMultiWindowModeChanged(isInMultiWindow, fullScreen)
|
||||
setupSystemBar()
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cancel()
|
||||
}
|
||||
|
||||
abstract fun onActivityCreated(savedInstanceState: Bundle?)
|
||||
|
||||
final override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
||||
return menu.let {
|
||||
val bool = onCompatCreateOptionsMenu(it)
|
||||
it.applyTint(this, toolBarTheme)
|
||||
bool
|
||||
}
|
||||
}
|
||||
|
||||
override fun onMenuOpened(featureId: Int, menu: Menu): Boolean {
|
||||
menu.applyOpenTint(this)
|
||||
return super.onMenuOpened(featureId, menu)
|
||||
}
|
||||
|
||||
open fun onCompatCreateOptionsMenu(menu: Menu) = super.onCreateOptionsMenu(menu)
|
||||
|
||||
final override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
if (item.itemId == android.R.id.home) {
|
||||
supportFinishAfterTransition()
|
||||
return true
|
||||
}
|
||||
return onCompatOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
open fun onCompatOptionsItemSelected(item: MenuItem) = super.onOptionsItemSelected(item)
|
||||
|
||||
private fun initTheme() {
|
||||
when (theme) {
|
||||
Theme.Transparent -> setTheme(R.style.AppTheme_Transparent)
|
||||
Theme.Dark -> {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
Theme.Light -> {
|
||||
setTheme(R.style.AppTheme_Light)
|
||||
window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
else -> {
|
||||
if (ColorUtils.isColorLight(primaryColor)) {
|
||||
setTheme(R.style.AppTheme_Light)
|
||||
} else {
|
||||
setTheme(R.style.AppTheme_Dark)
|
||||
}
|
||||
window.decorView.applyBackgroundTint(backgroundColor)
|
||||
}
|
||||
}
|
||||
if (imageBg) {
|
||||
try {
|
||||
ThemeConfig.getBgImage(this, windowSize)?.let {
|
||||
window.decorView.background = BitmapDrawable(resources, it)
|
||||
}
|
||||
} catch (e: OutOfMemoryError) {
|
||||
toastOnUi("背景图片太大,内存溢出")
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("加载背景出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun setupSystemBar() {
|
||||
if (fullScreen && !isInMultiWindow) {
|
||||
fullScreen()
|
||||
}
|
||||
val isTransparentStatusBar = AppConfig.isTransparentStatusBar
|
||||
val statusBarColor = ThemeStore.statusBarColor(this, isTransparentStatusBar)
|
||||
setStatusBarColorAuto(statusBarColor, isTransparentStatusBar, fullScreen)
|
||||
if (toolBarTheme == Theme.Dark) {
|
||||
setLightStatusBar(false)
|
||||
} else if (toolBarTheme == Theme.Light) {
|
||||
setLightStatusBar(true)
|
||||
}
|
||||
upNavigationBarColor()
|
||||
}
|
||||
|
||||
open fun upNavigationBarColor() {
|
||||
if (AppConfig.immNavigationBar) {
|
||||
setNavigationBarColorAuto(ThemeStore.navigationBarColor(this))
|
||||
} else {
|
||||
val nbColor = ColorUtils.darkenColor(ThemeStore.navigationBarColor(this))
|
||||
setNavigationBarColorAuto(nbColor)
|
||||
}
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
override fun finish() {
|
||||
currentFocus?.hideSoftInput()
|
||||
super.finish()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.lib.theme.ThemeStore
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
|
||||
abstract class BaseDialogFragment(@LayoutRes layoutID: Int) : DialogFragment(layoutID),
|
||||
CoroutineScope by MainScope() {
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
view.setBackgroundColor(ThemeStore.backgroundColor())
|
||||
onFragmentCreated(view, savedInstanceState)
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun show(manager: FragmentManager, tag: String?) {
|
||||
kotlin.runCatching {
|
||||
//在每个add事务前增加一个remove事务,防止连续的add
|
||||
manager.beginTransaction().remove(this).commit()
|
||||
super.show(manager, tag)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = this,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = Coroutine.async(scope, context) { block() }
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.res.Configuration
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import android.view.MenuInflater
|
||||
import android.view.MenuItem
|
||||
import android.view.View
|
||||
import androidx.annotation.LayoutRes
|
||||
import androidx.appcompat.view.SupportMenuInflater
|
||||
import androidx.appcompat.widget.Toolbar
|
||||
import androidx.fragment.app.Fragment
|
||||
import io.legado.app.R
|
||||
import io.legado.app.ui.widget.TitleBar
|
||||
import io.legado.app.utils.applyTint
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
abstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID),
|
||||
CoroutineScope by MainScope() {
|
||||
|
||||
var supportToolbar: Toolbar? = null
|
||||
private set
|
||||
|
||||
val menuInflater: MenuInflater
|
||||
@SuppressLint("RestrictedApi")
|
||||
get() = SupportMenuInflater(requireContext())
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
super.onViewCreated(view, savedInstanceState)
|
||||
onMultiWindowModeChanged()
|
||||
onFragmentCreated(view, savedInstanceState)
|
||||
observeLiveBus()
|
||||
}
|
||||
|
||||
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
|
||||
|
||||
override fun onMultiWindowModeChanged(isInMultiWindowMode: Boolean) {
|
||||
super.onMultiWindowModeChanged(isInMultiWindowMode)
|
||||
onMultiWindowModeChanged()
|
||||
}
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
super.onConfigurationChanged(newConfig)
|
||||
onMultiWindowModeChanged()
|
||||
}
|
||||
|
||||
private fun onMultiWindowModeChanged() {
|
||||
(activity as? BaseActivity<*>)?.let {
|
||||
view?.findViewById<TitleBar>(R.id.title_bar)
|
||||
?.onMultiWindowModeChanged(it.isInMultiWindow, it.fullScreen)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cancel()
|
||||
}
|
||||
|
||||
fun setSupportToolbar(toolbar: Toolbar) {
|
||||
supportToolbar = toolbar
|
||||
supportToolbar?.let {
|
||||
it.menu.apply {
|
||||
onCompatCreateOptionsMenu(this)
|
||||
applyTint(requireContext())
|
||||
}
|
||||
|
||||
it.setOnMenuItemClickListener { item ->
|
||||
onCompatOptionsItemSelected(item)
|
||||
true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
open fun observeLiveBus() {
|
||||
}
|
||||
|
||||
open fun onCompatCreateOptionsMenu(menu: Menu) {
|
||||
}
|
||||
|
||||
open fun onCompatOptionsItemSelected(item: MenuItem) {
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import androidx.fragment.app.DialogFragment
|
||||
import androidx.preference.*
|
||||
import io.legado.app.ui.widget.prefs.EditTextPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.ListPreferenceDialog
|
||||
import io.legado.app.ui.widget.prefs.MultiSelectListPreferenceDialog
|
||||
|
||||
abstract class BasePreferenceFragment : PreferenceFragmentCompat() {
|
||||
|
||||
private val dialogFragmentTag = "androidx.preference.PreferenceFragment.DIALOG"
|
||||
|
||||
@SuppressLint("RestrictedApi")
|
||||
override fun onDisplayPreferenceDialog(preference: Preference) {
|
||||
|
||||
var handled = false
|
||||
if (callbackFragment is OnPreferenceDisplayDialogCallback) {
|
||||
handled =
|
||||
(callbackFragment as OnPreferenceDisplayDialogCallback)
|
||||
.onPreferenceDisplayDialog(this, preference)
|
||||
}
|
||||
if (!handled && activity is OnPreferenceDisplayDialogCallback) {
|
||||
handled = (activity as OnPreferenceDisplayDialogCallback)
|
||||
.onPreferenceDisplayDialog(this, preference)
|
||||
}
|
||||
|
||||
if (handled) {
|
||||
return
|
||||
}
|
||||
|
||||
// check if dialog is already showing
|
||||
if (parentFragmentManager.findFragmentByTag(dialogFragmentTag) != null) {
|
||||
return
|
||||
}
|
||||
|
||||
val f: DialogFragment = when (preference) {
|
||||
is EditTextPreference -> {
|
||||
EditTextPreferenceDialog.newInstance(preference.getKey())
|
||||
}
|
||||
is ListPreference -> {
|
||||
ListPreferenceDialog.newInstance(preference.getKey())
|
||||
}
|
||||
is MultiSelectListPreference -> {
|
||||
MultiSelectListPreferenceDialog.newInstance(preference.getKey())
|
||||
}
|
||||
else -> {
|
||||
throw IllegalArgumentException(
|
||||
"Cannot display dialog for an unknown Preference type: "
|
||||
+ preference.javaClass.simpleName
|
||||
+ ". Make sure to implement onPreferenceDisplayDialog() to handle "
|
||||
+ "displaying a custom dialog for this Preference."
|
||||
)
|
||||
}
|
||||
}
|
||||
@Suppress("DEPRECATION")
|
||||
f.setTargetFragment(this, 0)
|
||||
|
||||
f.show(parentFragmentManager, dialogFragmentTag)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.app.Service
|
||||
import android.content.Intent
|
||||
import android.os.IBinder
|
||||
import androidx.annotation.CallSuper
|
||||
import io.legado.app.help.LifecycleHelp
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.cancel
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
abstract class BaseService : Service(), CoroutineScope by MainScope() {
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = this,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
) = Coroutine.async(scope, context) { block() }
|
||||
|
||||
@CallSuper
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
LifecycleHelp.onServiceCreate(this)
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onTaskRemoved(rootIntent: Intent?) {
|
||||
super.onTaskRemoved(rootIntent)
|
||||
stopSelf()
|
||||
}
|
||||
|
||||
override fun onBind(intent: Intent?): IBinder? {
|
||||
return null
|
||||
}
|
||||
|
||||
@CallSuper
|
||||
override fun onDestroy() {
|
||||
super.onDestroy()
|
||||
cancel()
|
||||
LifecycleHelp.onServiceDestroy(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import io.legado.app.App
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
|
||||
@Suppress("unused")
|
||||
open class BaseViewModel(application: Application) : AndroidViewModel(application) {
|
||||
|
||||
val context: Context by lazy { this.getApplication<App>() }
|
||||
|
||||
fun <T> execute(
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> T
|
||||
): Coroutine<T> {
|
||||
return Coroutine.async(scope, context) { block() }
|
||||
}
|
||||
|
||||
fun <R> submit(
|
||||
scope: CoroutineScope = viewModelScope,
|
||||
context: CoroutineContext = Dispatchers.IO,
|
||||
block: suspend CoroutineScope.() -> Deferred<R>
|
||||
): Coroutine<R> {
|
||||
return Coroutine.async(scope, context) { block().await() }
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
# 基类
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import io.legado.app.constant.Theme
|
||||
|
||||
abstract class VMBaseActivity<VB : ViewBinding, VM : ViewModel>(
|
||||
fullScreen: Boolean = true,
|
||||
theme: Theme = Theme.Auto,
|
||||
toolBarTheme: Theme = Theme.Auto,
|
||||
transparent: Boolean = false,
|
||||
imageBg: Boolean = true
|
||||
) : BaseActivity<VB>(fullScreen, theme, toolBarTheme, transparent, imageBg) {
|
||||
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package io.legado.app.base
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
|
||||
abstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) {
|
||||
|
||||
protected abstract val viewModel: VM
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package io.legado.app.base.adapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.AsyncListDiffer
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import splitties.views.onLongClick
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/12/15.
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
abstract class DiffRecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :
|
||||
RecyclerView.Adapter<ItemViewHolder>() {
|
||||
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private val asyncListDiffer: AsyncListDiffer<ITEM> by lazy {
|
||||
AsyncListDiffer(this, diffItemCallback).apply {
|
||||
addListListener { _, _ ->
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
|
||||
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
|
||||
|
||||
var itemAnimation: ItemAnimation? = null
|
||||
|
||||
abstract val diffItemCallback: DiffUtil.ItemCallback<ITEM>
|
||||
|
||||
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
|
||||
itemLongClickListener = listener
|
||||
}
|
||||
|
||||
fun bindToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.adapter = this
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItems(items: List<ITEM>?) {
|
||||
kotlin.runCatching {
|
||||
asyncListDiffer.submitList(items)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItem(position: Int, item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val list = ArrayList(asyncListDiffer.currentList)
|
||||
list[position] = item
|
||||
asyncListDiffer.submitList(list)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val index = asyncListDiffer.currentList.indexOf(item)
|
||||
if (index >= 0) {
|
||||
asyncListDiffer.currentList[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(position: Int, payload: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = itemCount
|
||||
if (position in 0 until size) {
|
||||
notifyItemChanged(position, payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = itemCount
|
||||
if (fromPosition in 0 until size && toPosition in 0 until size) {
|
||||
notifyItemRangeChanged(
|
||||
fromPosition,
|
||||
toPosition - fromPosition + 1,
|
||||
payloads
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = asyncListDiffer.currentList.isEmpty()
|
||||
|
||||
fun isNotEmpty() = asyncListDiffer.currentList.isNotEmpty()
|
||||
|
||||
fun getItem(position: Int): ITEM? = asyncListDiffer.currentList.getOrNull(position)
|
||||
|
||||
fun getItems(): List<ITEM> = asyncListDiffer.currentList
|
||||
|
||||
/**
|
||||
* grid 模式下使用
|
||||
*/
|
||||
protected open fun getSpanSize(viewType: Int, position: Int) = 1
|
||||
|
||||
final override fun getItemCount() = getItems().size
|
||||
|
||||
final override fun getItemViewType(position: Int): Int {
|
||||
return 0
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
|
||||
val holder = ItemViewHolder(getViewBinding(parent))
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
registerListener(holder, (holder.binding as VB))
|
||||
|
||||
if (itemClickListener != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemLongClickListener != null) {
|
||||
holder.itemView.onLongClick {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemLongClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return holder
|
||||
}
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup): VB
|
||||
|
||||
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}
|
||||
|
||||
open fun onCurrentListChanged() {
|
||||
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
final override fun onBindViewHolder(
|
||||
holder: ItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
convert(holder, (holder.binding as VB), it, payloads)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
addAnimation(holder)
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
val manager = recyclerView.layoutManager
|
||||
if (manager is GridLayoutManager) {
|
||||
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return getSpanSize(getItemViewType(position), position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun addAnimation(holder: ItemViewHolder) {
|
||||
itemAnimation?.let {
|
||||
if (it.itemAnimEnabled) {
|
||||
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {
|
||||
startAnimation(holder, it)
|
||||
it.itemAnimStartPosition = holder.layoutPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
|
||||
item.itemAnimation?.let {
|
||||
for (anim in it.getAnimators(holder.itemView)) {
|
||||
anim.setDuration(item.itemAnimDuration).start()
|
||||
anim.interpolator = item.itemAnimInterpolator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
|
||||
* 使用getItem(holder.layoutPosition)来获取item
|
||||
*/
|
||||
abstract fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: VB,
|
||||
item: ITEM,
|
||||
payloads: MutableList<Any>
|
||||
)
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
abstract fun registerListener(holder: ItemViewHolder, binding: VB)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package io.legado.app.base.adapter
|
||||
|
||||
import android.view.animation.Interpolator
|
||||
import android.view.animation.LinearInterpolator
|
||||
import io.legado.app.base.adapter.animations.*
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/12/15.
|
||||
*/
|
||||
@Suppress("unused")
|
||||
class ItemAnimation private constructor() {
|
||||
|
||||
var itemAnimEnabled = false
|
||||
var itemAnimFirstOnly = true
|
||||
var itemAnimation: BaseAnimation? = null
|
||||
var itemAnimInterpolator: Interpolator = LinearInterpolator()
|
||||
var itemAnimDuration: Long = 300L
|
||||
var itemAnimStartPosition: Int = -1
|
||||
|
||||
fun interpolator(interpolator: Interpolator) = apply {
|
||||
itemAnimInterpolator = interpolator
|
||||
}
|
||||
|
||||
fun duration(duration: Long) = apply {
|
||||
itemAnimDuration = duration
|
||||
}
|
||||
|
||||
fun startPosition(startPos: Int) = apply {
|
||||
itemAnimStartPosition = startPos
|
||||
}
|
||||
|
||||
fun animation(animationType: Int = NONE, animation: BaseAnimation? = null) = apply {
|
||||
if (animation != null) {
|
||||
itemAnimation = animation
|
||||
} else {
|
||||
when (animationType) {
|
||||
FADE_IN -> itemAnimation = AlphaInAnimation()
|
||||
SCALE_IN -> itemAnimation = ScaleInAnimation()
|
||||
BOTTOM_SLIDE_IN -> itemAnimation = SlideInBottomAnimation()
|
||||
LEFT_SLIDE_IN -> itemAnimation = SlideInLeftAnimation()
|
||||
RIGHT_SLIDE_IN -> itemAnimation = SlideInRightAnimation()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun enabled(enabled: Boolean) = apply {
|
||||
itemAnimEnabled = enabled
|
||||
}
|
||||
|
||||
fun firstOnly(firstOnly: Boolean) = apply {
|
||||
itemAnimFirstOnly = firstOnly
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val NONE: Int = 0x00000000
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val FADE_IN: Int = 0x00000001
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val SCALE_IN: Int = 0x00000002
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val BOTTOM_SLIDE_IN: Int = 0x00000003
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val LEFT_SLIDE_IN: Int = 0x00000004
|
||||
|
||||
/**
|
||||
* Use with [.openLoadAnimation]
|
||||
*/
|
||||
const val RIGHT_SLIDE_IN: Int = 0x00000005
|
||||
|
||||
fun create() = ItemAnimation()
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.legado.app.base.adapter
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/11/28.
|
||||
*/
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
class ItemViewHolder(val binding: ViewBinding) : RecyclerView.ViewHolder(binding.root)
|
||||
@@ -0,0 +1,458 @@
|
||||
package io.legado.app.base.adapter
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.SparseArray
|
||||
import android.view.LayoutInflater
|
||||
import android.view.ViewGroup
|
||||
import androidx.recyclerview.widget.DiffUtil
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import androidx.viewbinding.ViewBinding
|
||||
import splitties.views.onLongClick
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Created by Invincible on 2017/11/24.
|
||||
*
|
||||
* 通用的adapter 可添加header,footer,以及不同类型item
|
||||
*/
|
||||
@Suppress("unused", "MemberVisibilityCanBePrivate")
|
||||
abstract class RecyclerAdapter<ITEM, VB : ViewBinding>(protected val context: Context) :
|
||||
RecyclerView.Adapter<ItemViewHolder>() {
|
||||
|
||||
val inflater: LayoutInflater = LayoutInflater.from(context)
|
||||
|
||||
private val headerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }
|
||||
private val footerItems: SparseArray<(parent: ViewGroup) -> ViewBinding> by lazy { SparseArray() }
|
||||
|
||||
private val items: MutableList<ITEM> = mutableListOf()
|
||||
|
||||
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
|
||||
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
|
||||
|
||||
var itemAnimation: ItemAnimation? = null
|
||||
|
||||
fun setOnItemClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Unit) {
|
||||
itemClickListener = listener
|
||||
}
|
||||
|
||||
fun setOnItemLongClickListener(listener: (holder: ItemViewHolder, item: ITEM) -> Boolean) {
|
||||
itemLongClickListener = listener
|
||||
}
|
||||
|
||||
fun bindToRecyclerView(recyclerView: RecyclerView) {
|
||||
recyclerView.adapter = this
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = headerItems.size()
|
||||
headerItems.put(TYPE_HEADER_VIEW + headerItems.size(), header)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = getActualItemCount() + footerItems.size()
|
||||
footerItems.put(TYPE_FOOTER_VIEW + footerItems.size(), footer)
|
||||
notifyItemInserted(index)
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeHeaderView(header: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = headerItems.indexOfValue(header)
|
||||
if (index >= 0) {
|
||||
headerItems.remove(index)
|
||||
notifyItemRemoved(index)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeFooterView(footer: ((parent: ViewGroup) -> ViewBinding)) {
|
||||
kotlin.runCatching {
|
||||
val index = footerItems.indexOfValue(footer)
|
||||
if (index >= 0) {
|
||||
footerItems.remove(index)
|
||||
notifyItemRemoved(getActualItemCount() + index - 2)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun setItems(items: List<ITEM>?) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.isNotEmpty()) {
|
||||
this.items.clear()
|
||||
}
|
||||
if (items != null) {
|
||||
this.items.addAll(items)
|
||||
}
|
||||
notifyDataSetChanged()
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItems(items: List<ITEM>?, itemCallback: DiffUtil.ItemCallback<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
val callback = object : DiffUtil.Callback() {
|
||||
override fun getOldListSize(): Int {
|
||||
return itemCount
|
||||
}
|
||||
|
||||
override fun getNewListSize(): Int {
|
||||
return (items?.size ?: 0) + getHeaderCount() + getFooterCount()
|
||||
}
|
||||
|
||||
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
|
||||
val oldItem = getItem(oldItemPosition - getHeaderCount())
|
||||
?: return true
|
||||
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
|
||||
?: return true
|
||||
return itemCallback.areItemsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun areContentsTheSame(
|
||||
oldItemPosition: Int,
|
||||
newItemPosition: Int
|
||||
): Boolean {
|
||||
val oldItem = getItem(oldItemPosition - getHeaderCount())
|
||||
?: return true
|
||||
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
|
||||
?: return true
|
||||
return itemCallback.areContentsTheSame(oldItem, newItem)
|
||||
}
|
||||
|
||||
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
|
||||
val oldItem = getItem(oldItemPosition - getHeaderCount())
|
||||
?: return null
|
||||
val newItem = items?.getOrNull(newItemPosition - getHeaderCount())
|
||||
?: return null
|
||||
return itemCallback.getChangePayload(oldItem, newItem)
|
||||
}
|
||||
}
|
||||
val diffResult = DiffUtil.calculateDiff(callback)
|
||||
if (this.items.isNotEmpty()) {
|
||||
this.items.clear()
|
||||
}
|
||||
if (items != null) {
|
||||
this.items.addAll(items)
|
||||
}
|
||||
diffResult.dispatchUpdatesTo(this)
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun setItem(position: Int, item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (position in 0 until oldSize) {
|
||||
this.items[position] = item
|
||||
notifyItemChanged(position + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (this.items.add(item)) {
|
||||
notifyItemInserted(oldSize + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun addItems(position: Int, newItems: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.addAll(position, newItems)) {
|
||||
notifyItemRangeInserted(position + getHeaderCount(), newItems.size)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun addItems(newItems: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
val oldSize = getActualItemCount()
|
||||
if (this.items.addAll(newItems)) {
|
||||
if (oldSize == 0 && getHeaderCount() == 0) {
|
||||
notifyDataSetChanged()
|
||||
} else {
|
||||
notifyItemRangeInserted(oldSize + getHeaderCount(), newItems.size)
|
||||
}
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeItem(position: Int) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.removeAt(position) != null) {
|
||||
notifyItemRemoved(position + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun removeItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.remove(item)) {
|
||||
notifyItemRemoved(this.items.indexOf(item) + getHeaderCount())
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun removeItems(items: List<ITEM>) {
|
||||
kotlin.runCatching {
|
||||
if (this.items.removeAll(items)) {
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun swapItem(oldPosition: Int, newPosition: Int) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (oldPosition in 0 until size && newPosition in 0 until size) {
|
||||
val srcPosition = oldPosition + getHeaderCount()
|
||||
val targetPosition = newPosition + getHeaderCount()
|
||||
Collections.swap(this.items, srcPosition, targetPosition)
|
||||
notifyItemMoved(srcPosition, targetPosition)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(item: ITEM) {
|
||||
kotlin.runCatching {
|
||||
val index = this.items.indexOf(item)
|
||||
if (index >= 0) {
|
||||
this.items[index] = item
|
||||
notifyItemChanged(index)
|
||||
}
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItem(position: Int, payload: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (position in 0 until size) {
|
||||
notifyItemChanged(position + getHeaderCount(), payload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun updateItems(fromPosition: Int, toPosition: Int, payloads: Any) {
|
||||
kotlin.runCatching {
|
||||
val size = getActualItemCount()
|
||||
if (fromPosition in 0 until size && toPosition in 0 until size) {
|
||||
notifyItemRangeChanged(
|
||||
fromPosition + getHeaderCount(),
|
||||
toPosition - fromPosition + 1,
|
||||
payloads
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@SuppressLint("NotifyDataSetChanged")
|
||||
@Synchronized
|
||||
fun clearItems() {
|
||||
kotlin.runCatching {
|
||||
this.items.clear()
|
||||
notifyDataSetChanged()
|
||||
onCurrentListChanged()
|
||||
}
|
||||
}
|
||||
|
||||
fun isEmpty() = items.isEmpty()
|
||||
|
||||
fun isNotEmpty() = items.isNotEmpty()
|
||||
|
||||
/**
|
||||
* 除去header和footer
|
||||
*/
|
||||
fun getActualItemCount() = items.size
|
||||
|
||||
|
||||
fun getHeaderCount() = headerItems.size()
|
||||
|
||||
|
||||
fun getFooterCount() = footerItems.size()
|
||||
|
||||
fun getItem(position: Int): ITEM? = items.getOrNull(position)
|
||||
|
||||
fun getItemByLayoutPosition(position: Int) = items.getOrNull(position - getHeaderCount())
|
||||
|
||||
fun getItems(): List<ITEM> = items
|
||||
|
||||
protected open fun getItemViewType(item: ITEM, position: Int) = 0
|
||||
|
||||
/**
|
||||
* grid 模式下使用
|
||||
*/
|
||||
protected open fun getSpanSize(viewType: Int, position: Int) = 1
|
||||
|
||||
final override fun getItemCount() = getActualItemCount() + getHeaderCount() + getFooterCount()
|
||||
|
||||
final override fun getItemViewType(position: Int) = when {
|
||||
isHeader(position) -> TYPE_HEADER_VIEW + position
|
||||
isFooter(position) -> TYPE_FOOTER_VIEW + position - getActualItemCount() - getHeaderCount()
|
||||
else -> getItem(getActualPosition(position))?.let {
|
||||
getItemViewType(it, getActualPosition(position))
|
||||
} ?: 0
|
||||
}
|
||||
|
||||
open fun onCurrentListChanged() {
|
||||
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int) = when {
|
||||
viewType < TYPE_HEADER_VIEW + getHeaderCount() -> {
|
||||
ItemViewHolder(headerItems.get(viewType).invoke(parent))
|
||||
}
|
||||
|
||||
viewType >= TYPE_FOOTER_VIEW -> {
|
||||
ItemViewHolder(footerItems.get(viewType).invoke(parent))
|
||||
}
|
||||
|
||||
else -> {
|
||||
val holder = ItemViewHolder(getViewBinding(parent))
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
registerListener(holder, (holder.binding as VB))
|
||||
|
||||
if (itemClickListener != null) {
|
||||
holder.itemView.setOnClickListener {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (itemLongClickListener != null) {
|
||||
holder.itemView.onLongClick {
|
||||
getItem(holder.layoutPosition)?.let {
|
||||
itemLongClickListener?.invoke(holder, it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
holder
|
||||
}
|
||||
}
|
||||
|
||||
protected abstract fun getViewBinding(parent: ViewGroup): VB
|
||||
|
||||
final override fun onBindViewHolder(holder: ItemViewHolder, position: Int) {}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
final override fun onBindViewHolder(
|
||||
holder: ItemViewHolder,
|
||||
position: Int,
|
||||
payloads: MutableList<Any>
|
||||
) {
|
||||
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
|
||||
getItem(holder.layoutPosition - getHeaderCount())?.let {
|
||||
convert(holder, (holder.binding as VB), it, payloads)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
|
||||
super.onViewAttachedToWindow(holder)
|
||||
if (!isHeader(holder.layoutPosition) && !isFooter(holder.layoutPosition)) {
|
||||
addAnimation(holder)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
|
||||
super.onAttachedToRecyclerView(recyclerView)
|
||||
val manager = recyclerView.layoutManager
|
||||
if (manager is GridLayoutManager) {
|
||||
manager.spanSizeLookup = object : GridLayoutManager.SpanSizeLookup() {
|
||||
override fun getSpanSize(position: Int): Int {
|
||||
return getSpanSize(getItemViewType(position), position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun isHeader(position: Int) = position < getHeaderCount()
|
||||
|
||||
private fun isFooter(position: Int) = position >= getActualItemCount() + getHeaderCount()
|
||||
|
||||
private fun getActualPosition(position: Int) = position - getHeaderCount()
|
||||
|
||||
private fun addAnimation(holder: ItemViewHolder) {
|
||||
itemAnimation?.let {
|
||||
if (it.itemAnimEnabled) {
|
||||
if (!it.itemAnimFirstOnly || holder.layoutPosition > it.itemAnimStartPosition) {
|
||||
startAnimation(holder, it)
|
||||
it.itemAnimStartPosition = holder.layoutPosition
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected open fun startAnimation(holder: ItemViewHolder, item: ItemAnimation) {
|
||||
item.itemAnimation?.let {
|
||||
for (anim in it.getAnimators(holder.itemView)) {
|
||||
anim.setDuration(item.itemAnimDuration).start()
|
||||
anim.interpolator = item.itemAnimInterpolator
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 如果使用了事件回调,回调里不要直接使用item,会出现不更新的问题,
|
||||
* 使用getItem(holder.layoutPosition)来获取item
|
||||
*/
|
||||
abstract fun convert(
|
||||
holder: ItemViewHolder,
|
||||
binding: VB,
|
||||
item: ITEM,
|
||||
payloads: MutableList<Any>
|
||||
)
|
||||
|
||||
/**
|
||||
* 注册事件
|
||||
*/
|
||||
abstract fun registerListener(holder: ItemViewHolder, binding: VB)
|
||||
|
||||
companion object {
|
||||
private const val TYPE_HEADER_VIEW = Int.MIN_VALUE
|
||||
private const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class AlphaInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_ALPHA_FROM) :
|
||||
BaseAnimation {
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "alpha", mFrom, 1f))
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_ALPHA_FROM = 0f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.view.View
|
||||
|
||||
/**
|
||||
* adapter item 动画
|
||||
*/
|
||||
interface BaseAnimation {
|
||||
|
||||
fun getAnimators(view: View): Array<Animator>
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class ScaleInAnimation @JvmOverloads constructor(private val mFrom: Float = DEFAULT_SCALE_FROM) :
|
||||
BaseAnimation {
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> {
|
||||
val scaleX = ObjectAnimator.ofFloat(view, "scaleX", mFrom, 1f)
|
||||
val scaleY = ObjectAnimator.ofFloat(view, "scaleY", mFrom, 1f)
|
||||
return arrayOf(scaleX, scaleY)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DEFAULT_SCALE_FROM = .5f
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
class SlideInBottomAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationY", view.measuredHeight.toFloat(), 0f))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class SlideInLeftAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationX", -view.rootView.width.toFloat(), 0f))
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.base.adapter.animations
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.ObjectAnimator
|
||||
import android.view.View
|
||||
|
||||
|
||||
class SlideInRightAnimation : BaseAnimation {
|
||||
|
||||
|
||||
override fun getAnimators(view: View): Array<Animator> =
|
||||
arrayOf(ObjectAnimator.ofFloat(view, "translationX", view.rootView.width.toFloat(), 0f))
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.pm.PackageManager
|
||||
import android.provider.Settings
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.R
|
||||
import splitties.init.appCtx
|
||||
import java.text.SimpleDateFormat
|
||||
import javax.script.ScriptEngine
|
||||
import javax.script.ScriptEngineManager
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
object AppConst {
|
||||
|
||||
const val APP_TAG = "Legado"
|
||||
|
||||
const val channelIdDownload = "channel_download"
|
||||
const val channelIdReadAloud = "channel_read_aloud"
|
||||
const val channelIdWeb = "channel_web"
|
||||
|
||||
const val UA_NAME = "User-Agent"
|
||||
|
||||
const val MAX_THREAD = 9
|
||||
|
||||
val SCRIPT_ENGINE: ScriptEngine by lazy {
|
||||
ScriptEngineManager().getEngineByName("rhino")
|
||||
}
|
||||
|
||||
val timeFormat: SimpleDateFormat by lazy {
|
||||
SimpleDateFormat("HH:mm")
|
||||
}
|
||||
|
||||
val dateFormat: SimpleDateFormat by lazy {
|
||||
SimpleDateFormat("yyyy/MM/dd HH:mm")
|
||||
}
|
||||
|
||||
val fileNameFormat: SimpleDateFormat by lazy {
|
||||
SimpleDateFormat("yy-MM-dd-HH-mm-ss")
|
||||
}
|
||||
|
||||
val keyboardToolChars: List<String> by lazy {
|
||||
arrayListOf(
|
||||
"❓", "@css:", "<js></js>", "{{}}", "##", "&&", "%%", "||", "//", "\\", "$.",
|
||||
"@", ":", "class", "text", "href", "textNodes", "ownText", "all", "html",
|
||||
"[", "]", "<", ">", "#", "!", ".", "+", "-", "*", "=", "{'webView': true}"
|
||||
)
|
||||
}
|
||||
|
||||
const val bookGroupAllId = -1L
|
||||
const val bookGroupLocalId = -2L
|
||||
const val bookGroupAudioId = -3L
|
||||
const val bookGroupNoneId = -4L
|
||||
|
||||
const val notificationIdRead = -1122391
|
||||
const val notificationIdAudio = -1122392
|
||||
const val notificationIdCache = -1122393
|
||||
const val notificationIdWeb = -1122394
|
||||
const val notificationIdDownload = -1122395
|
||||
const val notificationIdCheckSource = -1122395
|
||||
|
||||
val urlOption: String by lazy {
|
||||
"""
|
||||
,{
|
||||
'charset': '',
|
||||
'method': 'POST',
|
||||
'body': '',
|
||||
'headers': {
|
||||
'User-Agent': ''
|
||||
}
|
||||
}
|
||||
""".trimIndent()
|
||||
}
|
||||
|
||||
val menuViewNames = arrayOf(
|
||||
"com.android.internal.view.menu.ListMenuItemView",
|
||||
"androidx.appcompat.view.menu.ListMenuItemView"
|
||||
)
|
||||
|
||||
val sysElevation = appCtx.resources.getDimension(R.dimen.design_appbar_elevation).toInt()
|
||||
|
||||
val androidId: String by lazy {
|
||||
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID)
|
||||
}
|
||||
|
||||
val appInfo: AppInfo by lazy {
|
||||
val appInfo = AppInfo()
|
||||
appCtx.packageManager.getPackageInfo(appCtx.packageName, PackageManager.GET_ACTIVITIES)
|
||||
?.let {
|
||||
appInfo.versionName = it.versionName
|
||||
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.P) {
|
||||
appInfo.versionCode = it.longVersionCode
|
||||
} else {
|
||||
@Suppress("DEPRECATION")
|
||||
appInfo.versionCode = it.versionCode.toLong()
|
||||
}
|
||||
}
|
||||
appInfo
|
||||
}
|
||||
|
||||
val charsets =
|
||||
arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII")
|
||||
|
||||
data class AppInfo(
|
||||
var versionCode: Long = 0L,
|
||||
var versionName: String = ""
|
||||
)
|
||||
|
||||
/**
|
||||
* The authority of a FileProvider defined in a <provider> element in your app's manifest.
|
||||
*/
|
||||
const val authority = BuildConfig.APPLICATION_ID + ".fileProvider"
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object AppLog {
|
||||
|
||||
private val mLogs = arrayListOf<Triple<Long, String, Throwable?>>()
|
||||
|
||||
val logs get() = mLogs.toList()
|
||||
|
||||
@Synchronized
|
||||
fun put(message: String?, throwable: Throwable? = null) {
|
||||
message ?: return
|
||||
if (mLogs.size > 100) {
|
||||
mLogs.removeLastOrNull()
|
||||
}
|
||||
mLogs.add(0, Triple(System.currentTimeMillis(), message, throwable))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun clear() {
|
||||
mLogs.clear()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
import java.util.regex.Pattern
|
||||
|
||||
@Suppress("RegExpRedundantEscape")
|
||||
object AppPattern {
|
||||
val JS_PATTERN: Pattern =
|
||||
Pattern.compile("<js>([\\w\\W]*?)</js>|@js:([\\w\\W]*)", Pattern.CASE_INSENSITIVE)
|
||||
val EXP_PATTERN: Pattern = Pattern.compile("\\{\\{([\\w\\W]*?)\\}\\}")
|
||||
|
||||
//匹配格式化后的图片格式
|
||||
val imgPattern: Pattern = Pattern.compile("<img[^>]*src=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
|
||||
|
||||
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
|
||||
val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著")
|
||||
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
|
||||
val splitGroupRegex = Regex("[,;,;]")
|
||||
|
||||
/**
|
||||
* 所有标点
|
||||
*/
|
||||
val bdRegex = Regex("(\\p{P})+")
|
||||
|
||||
/**
|
||||
* 换行
|
||||
*/
|
||||
val rnRegex = Regex("[\\r\\n]")
|
||||
|
||||
/**
|
||||
* 不发音段落判断
|
||||
*/
|
||||
val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$")
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object BookType {
|
||||
const val default = 0 // 0 文本
|
||||
const val audio = 1 // 1 音频
|
||||
const val image = 3 //图片
|
||||
const val local = "loc_book"
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object EventBus {
|
||||
const val MEDIA_BUTTON = "mediaButton"
|
||||
const val RECREATE = "RECREATE"
|
||||
const val UP_BOOKSHELF = "upBookToc"
|
||||
const val BOOKSHELF_REFRESH = "bookshelfRefresh"
|
||||
const val ALOUD_STATE = "aloud_state"
|
||||
const val TTS_PROGRESS = "ttsStart"
|
||||
const val TTS_DS = "ttsDs"
|
||||
const val BATTERY_CHANGED = "batteryChanged"
|
||||
const val TIME_CHANGED = "timeChanged"
|
||||
const val UP_CONFIG = "upConfig"
|
||||
const val OPEN_CHAPTER = "openChapter"
|
||||
const val AUDIO_SUB_TITLE = "audioSubTitle"
|
||||
const val AUDIO_STATE = "audioState"
|
||||
const val AUDIO_PROGRESS = "audioProgress"
|
||||
const val AUDIO_SIZE = "audioSize"
|
||||
const val AUDIO_SPEED = "audioSpeed"
|
||||
const val AUDIO_ERROR = "audioError"
|
||||
const val NOTIFY_MAIN = "notifyMain"
|
||||
const val WEB_SERVICE = "webService"
|
||||
const val UP_DOWNLOAD = "upDownload"
|
||||
const val SAVE_CONTENT = "saveContent"
|
||||
const val CHECK_SOURCE = "checkSource"
|
||||
const val CHECK_SOURCE_MESSAGE = "checkSourceMessage"
|
||||
const val CHECK_SOURCE_DONE = "checkSourceDone"
|
||||
const val TIP_COLOR = "tipColor"
|
||||
const val SOURCE_CHANGED = "sourceChanged"
|
||||
const val SEARCH_RESULT = "searchResult"
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object IntentAction {
|
||||
const val start = "start"
|
||||
const val play = "play"
|
||||
const val stop = "stop"
|
||||
const val resume = "resume"
|
||||
const val pause = "pause"
|
||||
const val addTimer = "addTimer"
|
||||
const val setTimer = "setTimer"
|
||||
const val prevParagraph = "prevParagraph"
|
||||
const val nextParagraph = "nextParagraph"
|
||||
const val upTtsSpeechRate = "upTtsSpeechRate"
|
||||
const val adjustProgress = "adjustProgress"
|
||||
const val adjustSpeed = "adjustSpeed"
|
||||
const val prev = "prev"
|
||||
const val next = "next"
|
||||
const val moveTo = "moveTo"
|
||||
const val init = "init"
|
||||
const val remove = "remove"
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object PreferKey {
|
||||
const val language = "language"
|
||||
const val fontScale = "fontScale"
|
||||
const val themeMode = "themeMode"
|
||||
const val userAgent = "userAgent"
|
||||
const val showUnread = "showUnread"
|
||||
const val bookGroupStyle = "bookGroupStyle"
|
||||
const val useDefaultCover = "useDefaultCover"
|
||||
const val coverShowName = "coverShowName"
|
||||
const val coverShowAuthor = "coverShowAuthor"
|
||||
const val coverShowNameN = "coverShowNameN"
|
||||
const val coverShowAuthorN = "coverShowAuthorN"
|
||||
const val hideStatusBar = "hideStatusBar"
|
||||
const val clickActionTL = "clickActionTopLeft"
|
||||
const val clickActionTC = "clickActionTopCenter"
|
||||
const val clickActionTR = "clickActionTopRight"
|
||||
const val clickActionML = "clickActionMiddleLeft"
|
||||
const val clickActionMC = "clickActionMiddleCenter"
|
||||
const val clickActionMR = "clickActionMiddleRight"
|
||||
const val clickActionBL = "clickActionBottomLeft"
|
||||
const val clickActionBC = "clickActionBottomCenter"
|
||||
const val clickActionBR = "clickActionBottomRight"
|
||||
const val hideNavigationBar = "hideNavigationBar"
|
||||
const val precisionSearch = "precisionSearch"
|
||||
const val readAloudByPage = "readAloudByPage"
|
||||
const val ttsEngine = "appTtsEngine"
|
||||
const val ttsSpeechRate = "ttsSpeechRate"
|
||||
const val prevKeys = "prevKeyCodes"
|
||||
const val nextKeys = "nextKeyCodes"
|
||||
const val showDiscovery = "showDiscovery"
|
||||
const val showRss = "showRss"
|
||||
const val bookshelfLayout = "bookshelfLayout"
|
||||
const val bookshelfSort = "bookshelfSort"
|
||||
const val bookExportFileName = "bookExportFileName"
|
||||
const val bookImportFileName = "bookImportFileName"
|
||||
const val recordLog = "recordLog"
|
||||
const val processText = "process_text"
|
||||
const val cleanCache = "cleanCache"
|
||||
const val saveTabPosition = "saveTabPosition"
|
||||
const val fontFolder = "fontFolder"
|
||||
const val backupPath = "backupUri"
|
||||
const val restoreIgnore = "restoreIgnore"
|
||||
const val threadCount = "threadCount"
|
||||
const val webPort = "webPort"
|
||||
const val keepLight = "keep_light"
|
||||
const val webService = "webService"
|
||||
const val webDavUrl = "web_dav_url"
|
||||
const val webDavAccount = "web_dav_account"
|
||||
const val webDavPassword = "web_dav_password"
|
||||
const val webDavCreateDir = "webDavCreateDir"
|
||||
const val exportToWebDav = "webDavCacheBackup"
|
||||
const val exportNoChapterName = "exportNoChapterName"
|
||||
const val exportType = "exportType"
|
||||
const val changeSourceCheckAuthor = "changeSourceCheckAuthor"
|
||||
const val changeSourceLoadToc = "changeSourceLoadToc"
|
||||
const val changeSourceLoadInfo = "changeSourceLoadInfo"
|
||||
const val chineseConverterType = "chineseConverterType"
|
||||
const val launcherIcon = "launcherIcon"
|
||||
const val textSelectAble = "selectText"
|
||||
const val lastBackup = "lastBackup"
|
||||
const val shareLayout = "shareLayout"
|
||||
const val readStyleSelect = "readStyleSelect"
|
||||
const val systemTypefaces = "system_typefaces"
|
||||
const val readBodyToLh = "readBodyToLh"
|
||||
const val textFullJustify = "textFullJustify"
|
||||
const val textBottomJustify = "textBottomJustify"
|
||||
const val autoReadSpeed = "autoReadSpeed"
|
||||
const val barElevation = "barElevation"
|
||||
const val transparentStatusBar = "transparentStatusBar"
|
||||
const val immNavigationBar = "immNavigationBar"
|
||||
const val defaultCover = "defaultCover"
|
||||
const val defaultCoverDark = "defaultCoverDark"
|
||||
const val replaceEnableDefault = "replaceEnableDefault"
|
||||
const val showBrightnessView = "showBrightnessView"
|
||||
const val autoClearExpired = "autoClearExpired"
|
||||
const val autoChangeSource = "autoChangeSource"
|
||||
const val importKeepName = "importKeepName"
|
||||
const val screenOrientation = "screenOrientation"
|
||||
const val syncBookProgress = "syncBookProgress"
|
||||
const val preDownloadNum = "preDownloadNum"
|
||||
const val autoRefresh = "auto_refresh"
|
||||
const val defaultToRead = "defaultToRead"
|
||||
const val exportCharset = "exportCharset"
|
||||
const val exportUseReplace = "exportUseReplace"
|
||||
const val useZhLayout = "useZhLayout"
|
||||
const val brightness = "brightness"
|
||||
const val nightBrightness = "nightBrightness"
|
||||
const val expandTextMenu = "expandTextMenu"
|
||||
const val doublePageHorizontal = "doublePageHorizontal"
|
||||
const val readUrlOpenInBrowser = "readUrlInBrowser"
|
||||
const val defaultBookTreeUri = "defaultBookTreeUri"
|
||||
|
||||
const val cPrimary = "colorPrimary"
|
||||
const val cAccent = "colorAccent"
|
||||
const val cBackground = "colorBackground"
|
||||
const val cBBackground = "colorBottomBackground"
|
||||
const val bgImage = "backgroundImage"
|
||||
const val bgImageBlurring = "backgroundImageBlurring"
|
||||
|
||||
const val cNPrimary = "colorPrimaryNight"
|
||||
const val cNAccent = "colorAccentNight"
|
||||
const val cNBackground = "colorBackgroundNight"
|
||||
const val cNBBackground = "colorBottomBackgroundNight"
|
||||
const val bgImageN = "backgroundImageNight"
|
||||
const val bgImageNBlurring = "backgroundImageNightBlurring"
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
object Status {
|
||||
const val STOP = 0
|
||||
const val PLAY = 1
|
||||
const val PAUSE = 3
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
package io.legado.app.constant
|
||||
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.utils.ColorUtils
|
||||
|
||||
enum class Theme {
|
||||
Dark, Light, Auto, Transparent, EInk;
|
||||
|
||||
companion object {
|
||||
fun getTheme() = when {
|
||||
AppConfig.isEInkMode -> EInk
|
||||
AppConfig.isNightTheme -> Dark
|
||||
else -> Light
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun fromBackground(backgroundColor: Int) =
|
||||
if (ColorUtils.isColorLight(backgroundColor)) Light
|
||||
else Dark
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
package io.legado.app.data
|
||||
|
||||
import android.content.Context
|
||||
import androidx.room.Database
|
||||
import androidx.room.Room
|
||||
import androidx.room.RoomDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.dao.*
|
||||
import io.legado.app.data.entities.*
|
||||
import splitties.init.appCtx
|
||||
import java.util.*
|
||||
|
||||
val appDb by lazy {
|
||||
AppDatabase.createDatabase(appCtx)
|
||||
}
|
||||
|
||||
@Database(
|
||||
version = 42,
|
||||
exportSchema = true,
|
||||
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
|
||||
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
|
||||
RssSource::class, Bookmark::class, RssArticle::class, RssReadRecord::class,
|
||||
RssStar::class, TxtTocRule::class, ReadRecord::class, HttpTTS::class, Cache::class,
|
||||
RuleSub::class]
|
||||
)
|
||||
abstract class AppDatabase : RoomDatabase() {
|
||||
|
||||
abstract val bookDao: BookDao
|
||||
abstract val bookGroupDao: BookGroupDao
|
||||
abstract val bookSourceDao: BookSourceDao
|
||||
abstract val bookChapterDao: BookChapterDao
|
||||
abstract val replaceRuleDao: ReplaceRuleDao
|
||||
abstract val searchBookDao: SearchBookDao
|
||||
abstract val searchKeywordDao: SearchKeywordDao
|
||||
abstract val rssSourceDao: RssSourceDao
|
||||
abstract val bookmarkDao: BookmarkDao
|
||||
abstract val rssArticleDao: RssArticleDao
|
||||
abstract val rssStarDao: RssStarDao
|
||||
abstract val cookieDao: CookieDao
|
||||
abstract val txtTocRuleDao: TxtTocRuleDao
|
||||
abstract val readRecordDao: ReadRecordDao
|
||||
abstract val httpTTSDao: HttpTTSDao
|
||||
abstract val cacheDao: CacheDao
|
||||
abstract val ruleSubDao: RuleSubDao
|
||||
|
||||
companion object {
|
||||
|
||||
private const val DATABASE_NAME = "legado.db"
|
||||
|
||||
fun createDatabase(context: Context) =
|
||||
Room.databaseBuilder(context, AppDatabase::class.java, DATABASE_NAME)
|
||||
.fallbackToDestructiveMigrationFrom(1, 2, 3, 4, 5, 6, 7, 8, 9)
|
||||
.addMigrations(*DatabaseMigrations.migrations)
|
||||
.allowMainThreadQueries()
|
||||
.addCallback(dbCallback)
|
||||
.build()
|
||||
|
||||
private val dbCallback = object : Callback() {
|
||||
|
||||
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||
db.setLocale(Locale.CHINESE)
|
||||
}
|
||||
|
||||
override fun onOpen(db: SupportSQLiteDatabase) {
|
||||
db.execSQL(
|
||||
"""insert into book_groups(groupId, groupName, 'order', show)
|
||||
select ${AppConst.bookGroupAllId}, '全部', -10, 1
|
||||
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAllId})"""
|
||||
)
|
||||
db.execSQL(
|
||||
"""insert into book_groups(groupId, groupName, 'order', show)
|
||||
select ${AppConst.bookGroupLocalId}, '本地', -9, 1
|
||||
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupLocalId})"""
|
||||
)
|
||||
db.execSQL(
|
||||
"""insert into book_groups(groupId, groupName, 'order', show)
|
||||
select ${AppConst.bookGroupAudioId}, '音频', -8, 1
|
||||
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupAudioId})"""
|
||||
)
|
||||
db.execSQL(
|
||||
"""insert into book_groups(groupId, groupName, 'order', show)
|
||||
select ${AppConst.bookGroupNoneId}, '未分组', -7, 1
|
||||
where not exists (select * from book_groups where groupId = ${AppConst.bookGroupNoneId})"""
|
||||
)
|
||||
db.execSQL("update book_sources set loginUi = null where loginUi = 'null'")
|
||||
db.execSQL("update rssSources set loginUi = null where loginUi = 'null'")
|
||||
db.execSQL("update httpTTS set loginUi = null where loginUi = 'null'")
|
||||
db.execSQL("update httpTTS set concurrentRate = '0' where loginUi is null")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,339 @@
|
||||
package io.legado.app.data
|
||||
|
||||
import androidx.room.migration.Migration
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import io.legado.app.constant.AppConst
|
||||
|
||||
object DatabaseMigrations {
|
||||
|
||||
val migrations: Array<Migration> by lazy {
|
||||
arrayOf(
|
||||
migration_10_11,
|
||||
migration_11_12,
|
||||
migration_12_13,
|
||||
migration_13_14,
|
||||
migration_14_15,
|
||||
migration_15_17,
|
||||
migration_17_18,
|
||||
migration_18_19,
|
||||
migration_19_20,
|
||||
migration_20_21,
|
||||
migration_21_22,
|
||||
migration_22_23,
|
||||
migration_23_24,
|
||||
migration_24_25,
|
||||
migration_25_26,
|
||||
migration_26_27,
|
||||
migration_27_28,
|
||||
migration_28_29,
|
||||
migration_29_30,
|
||||
migration_30_31,
|
||||
migration_31_32,
|
||||
migration_32_33,
|
||||
migration_33_34,
|
||||
migration_34_35,
|
||||
migration_35_36,
|
||||
migration_36_37,
|
||||
migration_37_38,
|
||||
migration_38_39,
|
||||
migration_39_40,
|
||||
migration_40_41,
|
||||
migration_41_42
|
||||
)
|
||||
}
|
||||
|
||||
private val migration_10_11 = object : Migration(10, 11) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("DROP TABLE txtTocRules")
|
||||
database.execSQL(
|
||||
"""CREATE TABLE txtTocRules(id INTEGER NOT NULL,
|
||||
name TEXT NOT NULL, rule TEXT NOT NULL, serialNumber INTEGER NOT NULL,
|
||||
enable INTEGER NOT NULL, PRIMARY KEY (id))"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_11_12 = object : Migration(11, 12) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE rssSources ADD style TEXT ")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_12_13 = object : Migration(12, 13) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE rssSources ADD articleStyle INTEGER NOT NULL DEFAULT 0 ")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_13_14 = object : Migration(13, 14) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
|
||||
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT,
|
||||
`customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL, `group` INTEGER NOT NULL,
|
||||
`latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL, `lastCheckCount` INTEGER NOT NULL,
|
||||
`totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL, `durChapterPos` INTEGER NOT NULL,
|
||||
`durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL, `order` INTEGER NOT NULL,
|
||||
`originOrder` INTEGER NOT NULL, `useReplaceRule` INTEGER NOT NULL, `variable` TEXT, PRIMARY KEY(`bookUrl`))"""
|
||||
)
|
||||
database.execSQL("INSERT INTO books_new select * from books ")
|
||||
database.execSQL("DROP TABLE books")
|
||||
database.execSQL("ALTER TABLE books_new RENAME TO books")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_14_15 = object : Migration(14, 15) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE bookmarks ADD bookAuthor TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_15_17 = object : Migration(15, 17) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `readRecord` (`bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`bookName`))")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_17_18 = object : Migration(17, 18) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `httpTTS` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, PRIMARY KEY(`id`))")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_18_19 = object : Migration(18, 19) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `readRecordNew` (`androidId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL,
|
||||
PRIMARY KEY(`androidId`, `bookName`))"""
|
||||
)
|
||||
database.execSQL("INSERT INTO readRecordNew(androidId, bookName, readTime) select '${AppConst.androidId}' as androidId, bookName, readTime from readRecord")
|
||||
database.execSQL("DROP TABLE readRecord")
|
||||
database.execSQL("ALTER TABLE readRecordNew RENAME TO readRecord")
|
||||
}
|
||||
}
|
||||
private val migration_19_20 = object : Migration(19, 20) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE book_sources ADD bookSourceComment TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_20_21 = object : Migration(20, 21) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE book_groups ADD show INTEGER NOT NULL DEFAULT 1")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_21_22 = object : Migration(21, 22) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `books_new` (`bookUrl` TEXT NOT NULL, `tocUrl` TEXT NOT NULL, `origin` TEXT NOT NULL,
|
||||
`originName` TEXT NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `customTag` TEXT,
|
||||
`coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `charset` TEXT, `type` INTEGER NOT NULL,
|
||||
`group` INTEGER NOT NULL, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL, `lastCheckTime` INTEGER NOT NULL,
|
||||
`lastCheckCount` INTEGER NOT NULL, `totalChapterNum` INTEGER NOT NULL, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL,
|
||||
`durChapterPos` INTEGER NOT NULL, `durChapterTime` INTEGER NOT NULL, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL,
|
||||
`order` INTEGER NOT NULL, `originOrder` INTEGER NOT NULL, `variable` TEXT, `readConfig` TEXT, PRIMARY KEY(`bookUrl`))"""
|
||||
)
|
||||
database.execSQL(
|
||||
"""INSERT INTO books_new select `bookUrl`, `tocUrl`, `origin`, `originName`, `name`, `author`, `kind`, `customTag`, `coverUrl`,
|
||||
`customCoverUrl`, `intro`, `customIntro`, `charset`, `type`, `group`, `latestChapterTitle`, `latestChapterTime`, `lastCheckTime`,
|
||||
`lastCheckCount`, `totalChapterNum`, `durChapterTitle`, `durChapterIndex`, `durChapterPos`, `durChapterTime`, `wordCount`, `canUpdate`,
|
||||
`order`, `originOrder`, `variable`, null
|
||||
from books"""
|
||||
)
|
||||
database.execSQL("DROP TABLE books")
|
||||
database.execSQL("ALTER TABLE books_new RENAME TO books")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `books` (`name`, `author`) ")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_22_23 = object : Migration(22, 23) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE chapters ADD baseUrl TEXT NOT NULL DEFAULT ''")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_23_24 = object : Migration(23, 24) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("CREATE TABLE IF NOT EXISTS `caches` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `caches` (`key`)")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_24_25 = object : Migration(24, 25) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `sourceSubs`
|
||||
(`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL,
|
||||
PRIMARY KEY(`id`))"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_25_26 = object : Migration(25, 26) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `ruleSubs` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `type` INTEGER NOT NULL,
|
||||
`customOrder` INTEGER NOT NULL, `autoUpdate` INTEGER NOT NULL, `update` INTEGER NOT NULL, PRIMARY KEY(`id`))"""
|
||||
)
|
||||
database.execSQL(" insert into `ruleSubs` select *, 0, 0 from `sourceSubs` ")
|
||||
database.execSQL("DROP TABLE `sourceSubs`")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_26_27 = object : Migration(26, 27) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL(" ALTER TABLE rssSources ADD singleUrl INTEGER NOT NULL DEFAULT 0 ")
|
||||
database.execSQL(
|
||||
"""CREATE TABLE IF NOT EXISTS `bookmarks1` (`time` INTEGER NOT NULL, `bookUrl` TEXT NOT NULL, `bookName` TEXT NOT NULL,
|
||||
`bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL,
|
||||
`bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))"""
|
||||
)
|
||||
database.execSQL(
|
||||
"""insert into `bookmarks1`
|
||||
select `time`, `bookUrl`, `bookName`, `bookAuthor`, `chapterIndex`, `pageIndex`, `chapterName`, '', `content`
|
||||
from bookmarks"""
|
||||
)
|
||||
database.execSQL(" DROP TABLE `bookmarks` ")
|
||||
database.execSQL(" ALTER TABLE bookmarks1 RENAME TO bookmarks ")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_bookmarks_time` ON `bookmarks` (`time`)")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_27_28 = object : Migration(27, 28) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE rssArticles ADD variable TEXT")
|
||||
database.execSQL("ALTER TABLE rssStars ADD variable TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_28_29 = object : Migration(28, 29) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE rssSources ADD sourceComment TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_29_30 = object : Migration(29, 30) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE chapters ADD `startFragmentId` TEXT")
|
||||
database.execSQL("ALTER TABLE chapters ADD `endFragmentId` TEXT")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `epubChapters`
|
||||
(`bookUrl` TEXT NOT NULL, `href` TEXT NOT NULL, `parentHref` TEXT,
|
||||
PRIMARY KEY(`bookUrl`, `href`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )
|
||||
"""
|
||||
)
|
||||
database.execSQL("CREATE INDEX IF NOT EXISTS `index_epubChapters_bookUrl` ON `epubChapters` (`bookUrl`)")
|
||||
database.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS `index_epubChapters_bookUrl_href` ON `epubChapters` (`bookUrl`, `href`)")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_30_31 = object : Migration(30, 31) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE readRecord RENAME TO readRecord1")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `readRecord` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL, PRIMARY KEY(`deviceId`, `bookName`))
|
||||
"""
|
||||
)
|
||||
database.execSQL("insert into readRecord (deviceId, bookName, readTime) select androidId, bookName, readTime from readRecord1")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_31_32 = object : Migration(31, 32) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("DROP TABLE `epubChapters`")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_32_33 = object : Migration(32, 33) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE bookmarks RENAME TO bookmarks_old")
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS `bookmarks` (`time` INTEGER NOT NULL,
|
||||
`bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL,
|
||||
`chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL,
|
||||
`content` TEXT NOT NULL, PRIMARY KEY(`time`))
|
||||
"""
|
||||
)
|
||||
database.execSQL(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `bookmarks` (`bookName`, `bookAuthor`)
|
||||
"""
|
||||
)
|
||||
database.execSQL(
|
||||
"""
|
||||
insert into bookmarks (time, bookName, bookAuthor, chapterIndex, chapterPos, chapterName, bookText, content)
|
||||
select time, ifNull(b.name, bookName) bookName, ifNull(b.author, bookAuthor) bookAuthor,
|
||||
chapterIndex, chapterPos, chapterName, bookText, content from bookmarks_old o
|
||||
left join books b on o.bookUrl = b.bookUrl
|
||||
"""
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_33_34 = object : Migration(33, 34) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `book_groups` ADD `cover` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_34_35 = object : Migration(34, 35) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `book_sources` ADD `concurrentRate` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_35_36 = object : Migration(35, 36) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `book_sources` ADD `loginUi` TEXT")
|
||||
database.execSQL("ALTER TABLE `book_sources` ADD`loginCheckJs` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_36_37 = object : Migration(36, 37) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `rssSources` ADD `loginUrl` TEXT")
|
||||
database.execSQL("ALTER TABLE `rssSources` ADD `loginUi` TEXT")
|
||||
database.execSQL("ALTER TABLE `rssSources` ADD `loginCheckJs` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_37_38 = object : Migration(37, 38) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `book_sources` ADD `respondTime` INTEGER NOT NULL DEFAULT 180000")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_38_39 = object : Migration(38, 39) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `rssSources` ADD `concurrentRate` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_39_40 = object : Migration(39, 40) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `chapters` ADD `isVip` INTEGER NOT NULL DEFAULT 0")
|
||||
database.execSQL("ALTER TABLE `chapters` ADD `isPay` INTEGER NOT NULL DEFAULT 0")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_40_41 = object : Migration(40, 41) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE `httpTTS` ADD `loginUrl` TEXT")
|
||||
database.execSQL("ALTER TABLE `httpTTS` ADD `loginUi` TEXT")
|
||||
database.execSQL("ALTER TABLE `httpTTS` ADD `loginCheckJs` TEXT")
|
||||
database.execSQL("ALTER TABLE `httpTTS` ADD `header` TEXT")
|
||||
database.execSQL("ALTER TABLE `httpTTS` ADD `concurrentRate` TEXT")
|
||||
}
|
||||
}
|
||||
|
||||
private val migration_41_42 = object : Migration(41, 42) {
|
||||
override fun migrate(database: SupportSQLiteDatabase) {
|
||||
database.execSQL("ALTER TABLE 'httpTTS' ADD `contentType` TEXT")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +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,40 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface BookChapterDao {
|
||||
|
||||
@Query("select * from chapters where bookUrl = :bookUrl order by `index`")
|
||||
fun flowByBook(bookUrl: String): Flow<List<BookChapter>>
|
||||
|
||||
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`")
|
||||
fun flowSearch(bookUrl: String, key: String): Flow<List<BookChapter>>
|
||||
|
||||
@Query("select * from chapters where bookUrl = :bookUrl order by `index`")
|
||||
fun getChapterList(bookUrl: String): List<BookChapter>
|
||||
|
||||
@Query("select * from chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end order by `index`")
|
||||
fun getChapterList(bookUrl: String, start: Int, end: Int): List<BookChapter>
|
||||
|
||||
@Query("select * from chapters where bookUrl = :bookUrl and `index` = :index")
|
||||
fun getChapter(bookUrl: String, index: Int): BookChapter?
|
||||
|
||||
@Query("select * from chapters where bookUrl = :bookUrl and `title` = :title")
|
||||
fun getChapter(bookUrl: String, title: String): BookChapter?
|
||||
|
||||
@Query("select count(url) from chapters where bookUrl = :bookUrl")
|
||||
fun getChapterCount(bookUrl: String): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg bookChapter: BookChapter)
|
||||
|
||||
@Update
|
||||
fun upDate(vararg bookChapter: BookChapter)
|
||||
|
||||
@Query("delete from chapters where bookUrl = :bookUrl")
|
||||
fun delByBook(bookUrl: String)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.Book
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface BookDao {
|
||||
|
||||
@Query("SELECT * FROM books order by durChapterTime desc")
|
||||
fun flowAll(): Flow<List<Book>>
|
||||
|
||||
@Query("SELECT * FROM books WHERE type = ${BookType.audio}")
|
||||
fun flowAudio(): Flow<List<Book>>
|
||||
|
||||
@Query("SELECT * FROM books WHERE origin = '${BookType.local}'")
|
||||
fun flowLocal(): Flow<List<Book>>
|
||||
|
||||
@Query("select * from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0")
|
||||
fun flowNoGroup(): Flow<List<Book>>
|
||||
|
||||
@Query("SELECT bookUrl FROM books WHERE origin = '${BookType.local}'")
|
||||
fun flowLocalUri(): Flow<List<String>>
|
||||
|
||||
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
|
||||
fun flowByGroup(group: Long): Flow<List<Book>>
|
||||
|
||||
@Query("SELECT * FROM books WHERE name like '%'||:key||'%' or author like '%'||:key||'%'")
|
||||
fun flowSearch(key: String): Flow<List<Book>>
|
||||
|
||||
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
|
||||
fun getBooksByGroup(group: Long): List<Book>
|
||||
|
||||
@Query("SELECT * FROM books WHERE `name` in (:names)")
|
||||
fun findByName(vararg names: String): List<Book>
|
||||
|
||||
@Query("SELECT * FROM books WHERE bookUrl = :bookUrl")
|
||||
fun getBook(bookUrl: String): Book?
|
||||
|
||||
@Query("SELECT * FROM books WHERE name = :name and author = :author")
|
||||
fun getBook(name: String, author: String): Book?
|
||||
|
||||
@get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups) & `group` = 0")
|
||||
val noGroupSize: Int
|
||||
|
||||
@get:Query("SELECT * FROM books where origin <> '${BookType.local}' and type = 0")
|
||||
val webBooks: List<Book>
|
||||
|
||||
@get:Query("SELECT * FROM books where origin <> '${BookType.local}' and canUpdate = 1")
|
||||
val hasUpdateBooks: List<Book>
|
||||
|
||||
@get:Query("SELECT * FROM books")
|
||||
val all: List<Book>
|
||||
|
||||
@get:Query("SELECT * FROM books where type = 0 ORDER BY durChapterTime DESC limit 1")
|
||||
val lastReadBook: Book?
|
||||
|
||||
@get:Query("SELECT bookUrl FROM books")
|
||||
val allBookUrls: List<String>
|
||||
|
||||
@get:Query("SELECT COUNT(*) FROM books")
|
||||
val allBookCount: Int
|
||||
|
||||
@get:Query("select min(`order`) from books")
|
||||
val minOrder: Int
|
||||
|
||||
@get:Query("select max(`order`) from books")
|
||||
val maxOrder: Int
|
||||
|
||||
@Query("select 1 from books where bookUrl = :bookUrl")
|
||||
fun has(bookUrl: String): Boolean?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg book: Book)
|
||||
|
||||
@Update
|
||||
fun update(vararg book: Book)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg book: Book)
|
||||
|
||||
@Query("update books set durChapterPos = :pos where bookUrl = :bookUrl")
|
||||
fun upProgress(bookUrl: String, pos: Int)
|
||||
|
||||
@Query("update books set `group` = :newGroupId where `group` = :oldGroupId")
|
||||
fun upGroup(oldGroupId: Long, newGroupId: Long)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.lifecycle.LiveData
|
||||
import androidx.room.*
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.BookGroup
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface BookGroupDao {
|
||||
|
||||
@Query("select * from book_groups where groupId = :id")
|
||||
fun getByID(id: Long): BookGroup?
|
||||
|
||||
@Query("select * from book_groups where groupName = :groupName")
|
||||
fun getByName(groupName: String): BookGroup?
|
||||
|
||||
@Query("SELECT * FROM book_groups ORDER BY `order`")
|
||||
fun flowAll(): Flow<List<BookGroup>>
|
||||
|
||||
@get:Query(
|
||||
"""
|
||||
SELECT * FROM book_groups where (groupId >= 0 and show > 0)
|
||||
or (groupId = -4 and show > 0 and (select count(bookUrl) from books where type != ${BookType.audio} and origin != '${BookType.local}' and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0) > 0)
|
||||
or (groupId = -3 and show > 0 and (select count(bookUrl) from books where type = ${BookType.audio}) > 0)
|
||||
or (groupId = -2 and show > 0 and (select count(bookUrl) from books where origin = '${BookType.local}') > 0)
|
||||
or (groupId = -1 and show > 0)
|
||||
ORDER BY `order`"""
|
||||
)
|
||||
val show: LiveData<List<BookGroup>>
|
||||
|
||||
@Query("SELECT * FROM book_groups where groupId >= 0 ORDER BY `order`")
|
||||
fun flowSelect(): Flow<List<BookGroup>>
|
||||
|
||||
@get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0")
|
||||
val idsSum: Long
|
||||
|
||||
@get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0")
|
||||
val maxOrder: Int
|
||||
|
||||
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
|
||||
val all: List<BookGroup>
|
||||
|
||||
@Query("update book_groups set show = 1 where groupId = :groupId")
|
||||
fun enableGroup(groupId: Long)
|
||||
|
||||
@Query("select groupName from book_groups where groupId > 0 and (groupId & :id) > 0")
|
||||
fun getGroupNames(id: Long): List<String>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg bookGroup: BookGroup)
|
||||
|
||||
@Update
|
||||
fun update(vararg bookGroup: BookGroup)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg bookGroup: BookGroup)
|
||||
|
||||
fun isInRules(id: Long): Boolean {
|
||||
if (id < 0) {
|
||||
return true
|
||||
}
|
||||
return id and (id - 1) == 0L
|
||||
}
|
||||
|
||||
fun getUnusedId(): Long {
|
||||
var id = 1L
|
||||
val idsSum = idsSum
|
||||
while (id and idsSum != 0L) {
|
||||
id = id.shl(1)
|
||||
}
|
||||
return id
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface BookSourceDao {
|
||||
|
||||
@Query("select * from book_sources order by customOrder asc")
|
||||
fun flowAll(): Flow<List<BookSource>>
|
||||
|
||||
@Query(
|
||||
"""select * from book_sources
|
||||
where bookSourceName like :searchKey
|
||||
or bookSourceGroup like :searchKey
|
||||
or bookSourceUrl like :searchKey
|
||||
or bookSourceComment like :searchKey
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowSearch(searchKey: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where bookSourceGroup like :searchKey order by customOrder asc")
|
||||
fun flowGroupSearch(searchKey: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where enabled = 1 order by customOrder asc")
|
||||
fun flowEnabled(): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where enabled = 0 order by customOrder asc")
|
||||
fun flowDisabled(): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
|
||||
fun flowExplore(): Flow<List<BookSource>>
|
||||
|
||||
@Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
|
||||
fun flowLogin(): Flow<List<BookSource>>
|
||||
|
||||
@Query(
|
||||
"""select * from book_sources
|
||||
where enabledExplore = 1
|
||||
and trim(exploreUrl) <> ''
|
||||
and (bookSourceGroup like :key or bookSourceName like :key)
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowExplore(key: String): Flow<List<BookSource>>
|
||||
|
||||
@Query(
|
||||
"""select * from book_sources
|
||||
where enabledExplore = 1
|
||||
and trim(exploreUrl) <> ''
|
||||
and (bookSourceGroup like :key)
|
||||
order by customOrder asc"""
|
||||
)
|
||||
fun flowGroupExplore(key: String): Flow<List<BookSource>>
|
||||
|
||||
@Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
|
||||
fun flowGroup(): Flow<List<String>>
|
||||
|
||||
@Query("select distinct bookSourceGroup from book_sources where enabled = 1 and trim(bookSourceGroup) <> ''")
|
||||
fun flowGroupEnabled(): Flow<List<String>>
|
||||
|
||||
@Query(
|
||||
"""select distinct bookSourceGroup from book_sources
|
||||
where enabledExplore = 1
|
||||
and trim(exploreUrl) <> ''
|
||||
and trim(bookSourceGroup) <> ''
|
||||
order by customOrder"""
|
||||
)
|
||||
fun flowExploreGroup(): Flow<List<String>>
|
||||
|
||||
@Query("select * from book_sources where bookSourceGroup like '%' || :group || '%'")
|
||||
fun getByGroup(group: String): List<BookSource>
|
||||
|
||||
@Query("select * from book_sources where enabled = 1 and bookSourceGroup like '%' || :group || '%'")
|
||||
fun getEnabledByGroup(group: String): List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources where trim(bookUrlPattern) <> ''")
|
||||
val hasBookUrlPattern: List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources where bookSourceGroup is null or bookSourceGroup = ''")
|
||||
val noGroup: List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources order by customOrder asc")
|
||||
val all: List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources where enabled = 1 order by customOrder")
|
||||
val allEnabled: List<BookSource>
|
||||
|
||||
@get:Query("select * from book_sources where enabled = 1 and bookSourceType = 0 order by customOrder")
|
||||
val allTextEnabled: List<BookSource>
|
||||
|
||||
@get:Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
|
||||
val allGroup: List<String>
|
||||
|
||||
@Query("select * from book_sources where bookSourceUrl = :key")
|
||||
fun getBookSource(key: String): BookSource?
|
||||
|
||||
@Query("select count(*) from book_sources")
|
||||
fun allCount(): Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg bookSource: BookSource)
|
||||
|
||||
@Update
|
||||
fun update(vararg bookSource: BookSource)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg bookSource: BookSource)
|
||||
|
||||
@Query("delete from book_sources where bookSourceUrl = :key")
|
||||
fun delete(key: String)
|
||||
|
||||
@get:Query("select min(customOrder) from book_sources")
|
||||
val minOrder: Int
|
||||
|
||||
@get:Query("select max(customOrder) from book_sources")
|
||||
val maxOrder: Int
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.Bookmark
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
@Dao
|
||||
interface BookmarkDao {
|
||||
|
||||
@get:Query("select * from bookmarks")
|
||||
val all: List<Bookmark>
|
||||
|
||||
@Query(
|
||||
"""select * from bookmarks
|
||||
where bookName = :bookName and bookAuthor = :bookAuthor
|
||||
order by chapterIndex"""
|
||||
)
|
||||
fun flowByBook(bookName: String, bookAuthor: String): Flow<List<Bookmark>>
|
||||
|
||||
@Query(
|
||||
"""SELECT * FROM bookmarks
|
||||
where bookName = :bookName and bookAuthor = :bookAuthor
|
||||
and chapterName like '%'||:key||'%' or content like '%'||:key||'%'
|
||||
order by chapterIndex"""
|
||||
)
|
||||
fun flowSearch(bookName: String, bookAuthor: String, key: String): Flow<List<Bookmark>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg bookmark: Bookmark)
|
||||
|
||||
@Update
|
||||
fun update(bookmark: Bookmark)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg bookmark: Bookmark)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.Dao
|
||||
import androidx.room.Insert
|
||||
import androidx.room.OnConflictStrategy
|
||||
import androidx.room.Query
|
||||
import io.legado.app.data.entities.Cache
|
||||
|
||||
@Dao
|
||||
interface CacheDao {
|
||||
|
||||
@Query("select value from caches where `key` = :key and (deadline = 0 or deadline > :now)")
|
||||
fun get(key: String, now: Long): String?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg cache: Cache)
|
||||
|
||||
@Query("delete from caches where `key` = :key")
|
||||
fun delete(key: String)
|
||||
|
||||
@Query("delete from caches where deadline > 0 and deadline < :now")
|
||||
fun clearDeadline(now: Long)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.Cookie
|
||||
|
||||
@Dao
|
||||
interface CookieDao {
|
||||
|
||||
@Query("SELECT * FROM cookies Where url = :url")
|
||||
fun get(url: String): Cookie?
|
||||
|
||||
@Query("select * from cookies where url like '%|%'")
|
||||
fun getOkHttpCookies(): List<Cookie>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg cookie: Cookie)
|
||||
|
||||
@Update
|
||||
fun update(vararg cookie: Cookie)
|
||||
|
||||
@Query("delete from cookies where url = :url")
|
||||
fun delete(url: String)
|
||||
|
||||
@Query("delete from cookies where url like '%|%'")
|
||||
fun deleteOkHttp()
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface HttpTTSDao {
|
||||
|
||||
@get:Query("select * from httpTTS order by name")
|
||||
val all: List<HttpTTS>
|
||||
|
||||
@Query("select * from httpTTS order by name")
|
||||
fun flowAll(): Flow<List<HttpTTS>>
|
||||
|
||||
@get:Query("select count(*) from httpTTS")
|
||||
val count: Int
|
||||
|
||||
@Query("select * from httpTTS where id = :id")
|
||||
fun get(id: Long): HttpTTS?
|
||||
|
||||
@Query("select name from httpTTS where id = :id")
|
||||
fun getName(id: Long): String?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg httpTTS: HttpTTS)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg httpTTS: HttpTTS)
|
||||
|
||||
@Update
|
||||
fun update(vararg httpTTS: HttpTTS)
|
||||
|
||||
@Query("delete from httpTTS where id < 0")
|
||||
fun deleteDefault()
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.ReadRecord
|
||||
import io.legado.app.data.entities.ReadRecordShow
|
||||
|
||||
@Dao
|
||||
interface ReadRecordDao {
|
||||
|
||||
@get:Query("select * from readRecord")
|
||||
val all: List<ReadRecord>
|
||||
|
||||
@get:Query("select bookName, sum(readTime) as readTime from readRecord group by bookName order by bookName collate localized")
|
||||
val allShow: List<ReadRecordShow>
|
||||
|
||||
@get:Query("select sum(readTime) from readRecord")
|
||||
val allTime: Long
|
||||
|
||||
@Query("select sum(readTime) from readRecord where bookName = :bookName")
|
||||
fun getReadTime(bookName: String): Long?
|
||||
|
||||
@Query("select readTime from readRecord where deviceId = :androidId and bookName = :bookName")
|
||||
fun getReadTime(androidId: String, bookName: String): Long?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg readRecord: ReadRecord)
|
||||
|
||||
@Update
|
||||
fun update(vararg record: ReadRecord)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg record: ReadRecord)
|
||||
|
||||
@Query("delete from readRecord")
|
||||
fun clear()
|
||||
|
||||
@Query("delete from readRecord where bookName = :bookName")
|
||||
fun deleteByName(bookName: String)
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
@Dao
|
||||
interface ReplaceRuleDao {
|
||||
|
||||
@Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
|
||||
fun flowAll(): Flow<List<ReplaceRule>>
|
||||
|
||||
@Query("SELECT * FROM replace_rules where `group` like :key or name like :key ORDER BY sortOrder ASC")
|
||||
fun flowSearch(key: String): Flow<List<ReplaceRule>>
|
||||
|
||||
@Query("SELECT * FROM replace_rules where `group` like :key ORDER BY sortOrder ASC")
|
||||
fun flowGroupSearch(key: String): Flow<List<ReplaceRule>>
|
||||
|
||||
@Query("select `group` from replace_rules where `group` is not null and `group` <> ''")
|
||||
fun flowGroup(): Flow<List<String>>
|
||||
|
||||
@get:Query("SELECT MIN(sortOrder) FROM replace_rules")
|
||||
val minOrder: Int
|
||||
|
||||
@get:Query("SELECT MAX(sortOrder) FROM replace_rules")
|
||||
val maxOrder: Int
|
||||
|
||||
@get:Query("SELECT * FROM replace_rules ORDER BY sortOrder ASC")
|
||||
val all: List<ReplaceRule>
|
||||
|
||||
@get:Query("select distinct `group` from replace_rules where trim(`group`) <> ''")
|
||||
val allGroup: List<String>
|
||||
|
||||
@get:Query("SELECT * FROM replace_rules WHERE isEnabled = 1 ORDER BY sortOrder ASC")
|
||||
val allEnabled: List<ReplaceRule>
|
||||
|
||||
@Query("SELECT * FROM replace_rules WHERE id = :id")
|
||||
fun findById(id: Long): ReplaceRule?
|
||||
|
||||
@Query("SELECT * FROM replace_rules WHERE id in (:ids)")
|
||||
fun findByIds(vararg ids: Long): List<ReplaceRule>
|
||||
|
||||
@Query(
|
||||
"""SELECT * FROM replace_rules WHERE isEnabled = 1
|
||||
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '')
|
||||
order by sortOrder"""
|
||||
)
|
||||
fun findEnabledByScope(name: String, origin: String): List<ReplaceRule>
|
||||
|
||||
@Query("select * from replace_rules where `group` like '%' || :group || '%'")
|
||||
fun getByGroup(group: String): List<ReplaceRule>
|
||||
|
||||
@get:Query("select * from replace_rules where `group` is null or `group` = ''")
|
||||
val noGroup: List<ReplaceRule>
|
||||
|
||||
@get:Query("SELECT COUNT(*) - SUM(isEnabled) FROM replace_rules")
|
||||
val summary: Int
|
||||
|
||||
@Query("UPDATE replace_rules SET isEnabled = :enable")
|
||||
fun enableAll(enable: Boolean)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg replaceRule: ReplaceRule): List<Long>
|
||||
|
||||
@Update
|
||||
fun update(vararg replaceRules: ReplaceRule)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg replaceRules: ReplaceRule)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.RssArticle
|
||||
import io.legado.app.data.entities.RssReadRecord
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RssArticleDao {
|
||||
|
||||
@Query("select * from rssArticles where origin = :origin and link = :link")
|
||||
fun get(origin: String, link: String): RssArticle?
|
||||
|
||||
@Query(
|
||||
"""select t1.link, t1.sort, t1.origin, t1.`order`, t1.title, t1.content,
|
||||
t1.description, t1.image, t1.pubDate, t1.variable, ifNull(t2.read, 0) as read
|
||||
from rssArticles as t1 left join rssReadRecords as t2
|
||||
on t1.link = t2.record where origin = :origin and sort = :sort
|
||||
order by `order` desc"""
|
||||
)
|
||||
fun flowByOriginSort(origin: String, sort: String): Flow<List<RssArticle>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg rssArticle: RssArticle)
|
||||
|
||||
@Query("delete from rssArticles where origin = :origin and sort = :sort and `order` < :order")
|
||||
fun clearOld(origin: String, sort: String, order: Long)
|
||||
|
||||
@Update
|
||||
fun update(vararg rssArticle: RssArticle)
|
||||
|
||||
@Query("delete from rssArticles where origin = :origin")
|
||||
fun delete(origin: String)
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
fun insertRecord(vararg rssReadRecord: RssReadRecord)
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RssSourceDao {
|
||||
|
||||
@Query("select * from rssSources where sourceUrl = :key")
|
||||
fun getByKey(key: String): RssSource?
|
||||
|
||||
@Query("select * from rssSources where sourceUrl in (:sourceUrls)")
|
||||
fun getRssSources(vararg sourceUrls: String): List<RssSource>
|
||||
|
||||
@get:Query("SELECT * FROM rssSources")
|
||||
val all: List<RssSource>
|
||||
|
||||
@get:Query("select count(sourceUrl) from rssSources")
|
||||
val size: Int
|
||||
|
||||
@Query("SELECT * FROM rssSources order by customOrder")
|
||||
fun flowAll(): Flow<List<RssSource>>
|
||||
|
||||
@Query("SELECT * FROM rssSources where sourceName like :key or sourceUrl like :key or sourceGroup like :key order by customOrder")
|
||||
fun flowSearch(key: String): Flow<List<RssSource>>
|
||||
|
||||
@Query("SELECT * FROM rssSources where sourceGroup like :key order by customOrder")
|
||||
fun flowGroupSearch(key: String): Flow<List<RssSource>>
|
||||
|
||||
@Query("SELECT * FROM rssSources where enabled = 1 order by customOrder")
|
||||
fun flowEnabled(): Flow<List<RssSource>>
|
||||
|
||||
@Query(
|
||||
"""SELECT * FROM rssSources
|
||||
where enabled = 1
|
||||
and (sourceName like :searchKey or sourceGroup like :searchKey or sourceUrl like :searchKey)
|
||||
order by customOrder"""
|
||||
)
|
||||
fun flowEnabled(searchKey: String): Flow<List<RssSource>>
|
||||
|
||||
@Query("SELECT * FROM rssSources where enabled = 1 and sourceGroup like :searchKey order by customOrder")
|
||||
fun flowEnabledByGroup(searchKey: String): Flow<List<RssSource>>
|
||||
|
||||
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
|
||||
fun flowGroup(): Flow<List<String>>
|
||||
|
||||
@get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
|
||||
val allGroup: List<String>
|
||||
|
||||
@get:Query("select min(customOrder) from rssSources")
|
||||
val minOrder: Int
|
||||
|
||||
@get:Query("select max(customOrder) from rssSources")
|
||||
val maxOrder: Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg rssSource: RssSource)
|
||||
|
||||
@Update
|
||||
fun update(vararg rssSource: RssSource)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg rssSource: RssSource)
|
||||
|
||||
@Query("delete from rssSources where sourceUrl = :sourceUrl")
|
||||
fun delete(sourceUrl: String)
|
||||
|
||||
@get:Query("select * from rssSources where sourceGroup is null or sourceGroup = ''")
|
||||
val noGroup: List<RssSource>
|
||||
|
||||
@Query("select * from rssSources where sourceGroup like '%' || :group || '%'")
|
||||
fun getByGroup(group: String): List<RssSource>
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.RssStar
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RssStarDao {
|
||||
|
||||
@get:Query("select * from rssStars order by starTime desc")
|
||||
val all: List<RssStar>
|
||||
|
||||
@Query("select * from rssStars where origin = :origin and link = :link")
|
||||
fun get(origin: String, link: String): RssStar?
|
||||
|
||||
@Query("select * from rssStars order by starTime desc")
|
||||
fun liveAll(): Flow<List<RssStar>>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg rssStar: RssStar)
|
||||
|
||||
@Update
|
||||
fun update(vararg rssStar: RssStar)
|
||||
|
||||
@Query("delete from rssStars where origin = :origin")
|
||||
fun delete(origin: String)
|
||||
|
||||
@Query("delete from rssStars where origin = :origin and link = :link")
|
||||
fun delete(origin: String, link: String)
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.RuleSub
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface RuleSubDao {
|
||||
|
||||
@get:Query("select * from ruleSubs order by customOrder")
|
||||
val all: List<RuleSub>
|
||||
|
||||
@Query("select * from ruleSubs order by customOrder")
|
||||
fun flowAll(): Flow<List<RuleSub>>
|
||||
|
||||
@get:Query("select customOrder from ruleSubs order by customOrder limit 0,1")
|
||||
val maxOrder: Int
|
||||
|
||||
@Query("select * from ruleSubs where url = :url")
|
||||
fun findByUrl(url: String): RuleSub?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg ruleSub: RuleSub)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg ruleSub: RuleSub)
|
||||
|
||||
@Update
|
||||
fun update(vararg ruleSub: RuleSub)
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.SearchBook
|
||||
|
||||
@Dao
|
||||
interface SearchBookDao {
|
||||
|
||||
@Query("select * from searchBooks where bookUrl = :bookUrl")
|
||||
fun getSearchBook(bookUrl: String): SearchBook?
|
||||
|
||||
@Query("select * from searchBooks where name = :name and author = :author and origin in (select bookSourceUrl from book_sources) order by originOrder limit 1")
|
||||
fun getFirstByNameAuthor(name: String, author: String): SearchBook?
|
||||
|
||||
@Query(
|
||||
"""select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl,
|
||||
t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable,
|
||||
t1.wordCount, t2.customOrder as originOrder
|
||||
from searchBooks as t1 inner join book_sources as t2
|
||||
on t1.origin = t2.bookSourceUrl
|
||||
where t1.name = :name and t1.author like '%'||:author||'%'
|
||||
and t2.enabled = 1 and t2.bookSourceGroup like '%'||:sourceGroup||'%'
|
||||
order by t2.customOrder"""
|
||||
)
|
||||
fun getChangeSourceSearch(name: String, author: String, sourceGroup: String): List<SearchBook>
|
||||
|
||||
@Query(
|
||||
"""select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl,
|
||||
t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable,
|
||||
t1.wordCount, t2.customOrder as originOrder
|
||||
from searchBooks as t1 inner join book_sources as t2
|
||||
on t1.origin = t2.bookSourceUrl
|
||||
where t1.name = :name and t1.author = :author
|
||||
and originName like '%'||:key||'%' and t2.enabled = 1
|
||||
and t2.bookSourceGroup like '%'||:sourceGroup||'%'
|
||||
order by t2.customOrder"""
|
||||
)
|
||||
fun getChangeSourceSearch(
|
||||
name: String,
|
||||
author: String,
|
||||
key: String,
|
||||
sourceGroup: String
|
||||
): List<SearchBook>
|
||||
|
||||
@Query(
|
||||
"""
|
||||
select t1.name, t1.author, t1.origin, t1.originName, t1.coverUrl, t1.bookUrl, t1.type, t1.time, t1.intro, t1.kind, t1.latestChapterTitle, t1.tocUrl, t1.variable, t1.wordCount, t2.customOrder as originOrder
|
||||
from searchBooks as t1 inner join book_sources as t2
|
||||
on t1.origin = t2.bookSourceUrl
|
||||
where t1.name = :name and t1.author = :author and t1.coverUrl is not null and t1.coverUrl <> '' and t2.enabled = 1
|
||||
order by t2.customOrder
|
||||
"""
|
||||
)
|
||||
fun getEnableHasCover(name: String, author: String): List<SearchBook>
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg searchBook: SearchBook): List<Long>
|
||||
|
||||
@Query("delete from searchBooks where name = :name and author = :author")
|
||||
fun clear(name: String, author: String)
|
||||
|
||||
@Query("delete from searchBooks where time < :time")
|
||||
fun clearExpired(time: Long)
|
||||
|
||||
@Update
|
||||
fun update(vararg searchBook: SearchBook)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg searchBook: SearchBook)
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.SearchKeyword
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
|
||||
@Dao
|
||||
interface SearchKeywordDao {
|
||||
|
||||
@get:Query("SELECT * FROM search_keywords")
|
||||
val all: List<SearchKeyword>
|
||||
|
||||
@Query("SELECT * FROM search_keywords ORDER BY usage DESC")
|
||||
fun flowByUsage(): Flow<List<SearchKeyword>>
|
||||
|
||||
@Query("SELECT * FROM search_keywords ORDER BY lastUseTime DESC")
|
||||
fun flowByTime(): Flow<List<SearchKeyword>>
|
||||
|
||||
@Query("SELECT * FROM search_keywords where word like '%'||:key||'%' ORDER BY usage DESC")
|
||||
fun flowSearch(key: String): Flow<List<SearchKeyword>>
|
||||
|
||||
@Query("select * from search_keywords where word = :key")
|
||||
fun get(key: String): SearchKeyword?
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg keywords: SearchKeyword)
|
||||
|
||||
@Update
|
||||
fun update(vararg keywords: SearchKeyword)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg keywords: SearchKeyword)
|
||||
|
||||
@Query("DELETE FROM search_keywords")
|
||||
fun deleteAll()
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package io.legado.app.data.dao
|
||||
|
||||
import androidx.room.*
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
@Dao
|
||||
interface TxtTocRuleDao {
|
||||
|
||||
@Query("select * from txtTocRules order by serialNumber")
|
||||
fun observeAll(): Flow<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>
|
||||
|
||||
@get:Query("select ifNull(min(serialNumber), 0) from txtTocRules")
|
||||
val minOrder: Int
|
||||
|
||||
@get:Query("select ifNull(max(serialNumber), 0) from txtTocRules")
|
||||
val maxOrder: Int
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun insert(vararg rule: TxtTocRule)
|
||||
|
||||
@Update(onConflict = OnConflictStrategy.REPLACE)
|
||||
fun update(vararg rule: TxtTocRule)
|
||||
|
||||
@Delete
|
||||
fun delete(vararg rule: TxtTocRule)
|
||||
|
||||
@Query("delete from txtTocRules where id < 0")
|
||||
fun deleteDefault()
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||
import io.legado.app.utils.splitNotBlank
|
||||
|
||||
interface BaseBook : RuleDataInterface {
|
||||
var name: String
|
||||
var author: String
|
||||
var bookUrl: String
|
||||
var kind: String?
|
||||
var wordCount: String?
|
||||
|
||||
var infoHtml: String?
|
||||
var tocHtml: String?
|
||||
|
||||
fun getKindList(): List<String> {
|
||||
val kindList = arrayListOf<String>()
|
||||
wordCount?.let {
|
||||
if (it.isNotBlank()) kindList.add(it)
|
||||
}
|
||||
kind?.let {
|
||||
val kinds = it.splitNotBlank(",", "\n")
|
||||
kindList.addAll(kinds)
|
||||
}
|
||||
return kindList
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.util.Base64
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.data.entities.rule.RowUi
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.help.CacheManager
|
||||
import io.legado.app.help.JsExtensions
|
||||
import io.legado.app.help.http.CookieStore
|
||||
import io.legado.app.utils.EncoderUtils
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import timber.log.Timber
|
||||
import javax.script.SimpleBindings
|
||||
|
||||
/**
|
||||
* 可在js里调用,source.xxx()
|
||||
*/
|
||||
@Suppress("unused")
|
||||
interface BaseSource : JsExtensions {
|
||||
|
||||
var concurrentRate: String? // 并发率
|
||||
var loginUrl: String? // 登录地址
|
||||
var loginUi: String? // 登录UI
|
||||
var header: String? // 请求头
|
||||
|
||||
fun getTag(): String
|
||||
|
||||
fun getKey(): String
|
||||
|
||||
fun loginUi(): List<RowUi>? {
|
||||
return GSON.fromJsonArray(loginUi)
|
||||
}
|
||||
|
||||
fun getLoginJs(): String? {
|
||||
val loginJs = loginUrl
|
||||
return when {
|
||||
loginJs == null -> null
|
||||
loginJs.startsWith("@js:") -> loginJs.substring(4)
|
||||
loginJs.startsWith("<js>") ->
|
||||
loginJs.substring(4, loginJs.lastIndexOf("<"))
|
||||
else -> loginJs
|
||||
}
|
||||
}
|
||||
|
||||
fun login() {
|
||||
getLoginJs()?.let {
|
||||
evalJS(it)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析header规则
|
||||
*/
|
||||
fun getHeaderMap(hasLoginHeader: Boolean = false) = HashMap<String, String>().apply {
|
||||
this[AppConst.UA_NAME] = AppConfig.userAgent
|
||||
header?.let {
|
||||
GSON.fromJsonObject<Map<String, String>>(
|
||||
when {
|
||||
it.startsWith("@js:", true) ->
|
||||
evalJS(it.substring(4)).toString()
|
||||
it.startsWith("<js>", true) ->
|
||||
evalJS(it.substring(4, it.lastIndexOf("<"))).toString()
|
||||
else -> it
|
||||
}
|
||||
)?.let { map ->
|
||||
putAll(map)
|
||||
}
|
||||
}
|
||||
if (hasLoginHeader) {
|
||||
getLoginHeaderMap()?.let {
|
||||
putAll(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用于登录的头部信息
|
||||
*/
|
||||
fun getLoginHeader(): String? {
|
||||
return CacheManager.get("loginHeader_${getKey()}")
|
||||
}
|
||||
|
||||
fun getLoginHeaderMap(): Map<String, String>? {
|
||||
val cache = getLoginHeader() ?: return null
|
||||
return GSON.fromJsonObject(cache)
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存登录头部信息,map格式,访问时自动添加
|
||||
*/
|
||||
fun putLoginHeader(header: String) {
|
||||
CacheManager.put("loginHeader_${getKey()}", header)
|
||||
}
|
||||
|
||||
fun removeLoginHeader() {
|
||||
CacheManager.delete("loginHeader_${getKey()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户信息,可以用来登录
|
||||
* 用户信息采用aes加密存储
|
||||
*/
|
||||
fun getLoginInfo(): String? {
|
||||
try {
|
||||
val key = AppConst.androidId.encodeToByteArray(0, 8)
|
||||
val cache = CacheManager.get("userInfo_${getKey()}") ?: return null
|
||||
val encodeBytes = Base64.decode(cache, Base64.DEFAULT)
|
||||
val decodeBytes = EncoderUtils.decryptAES(encodeBytes, key)
|
||||
?: return null
|
||||
return String(decodeBytes)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getLoginInfoMap(): Map<String, String>? {
|
||||
return GSON.fromJsonObject(getLoginInfo())
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存用户信息,aes加密
|
||||
*/
|
||||
fun putLoginInfo(info: String): Boolean {
|
||||
return try {
|
||||
val key = (AppConst.androidId).encodeToByteArray(0, 8)
|
||||
val encodeBytes = EncoderUtils.encryptAES(info.toByteArray(), key)
|
||||
val encodeStr = Base64.encodeToString(encodeBytes, Base64.DEFAULT)
|
||||
CacheManager.put("userInfo_${getKey()}", encodeStr)
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun removeLoginInfo() {
|
||||
CacheManager.delete("userInfo_${getKey()}")
|
||||
}
|
||||
|
||||
fun setVariable(variable: String?) {
|
||||
if (variable != null) {
|
||||
CacheManager.put("sourceVariable_${getKey()}", variable)
|
||||
} else {
|
||||
CacheManager.delete("sourceVariable_${getKey()}")
|
||||
}
|
||||
}
|
||||
|
||||
fun getVariable(): String? {
|
||||
return CacheManager.get("sourceVariable_${getKey()}")
|
||||
}
|
||||
|
||||
/**
|
||||
* 执行JS
|
||||
*/
|
||||
@Throws(Exception::class)
|
||||
fun evalJS(jsStr: String, bindingsConfig: SimpleBindings.() -> Unit = {}): Any? {
|
||||
val bindings = SimpleBindings()
|
||||
bindings.apply(bindingsConfig)
|
||||
bindings["java"] = this
|
||||
bindings["source"] = this
|
||||
bindings["baseUrl"] = getKey()
|
||||
bindings["cookie"] = CookieStore
|
||||
bindings["cache"] = CacheManager
|
||||
return AppConst.SCRIPT_ENGINE.eval(jsStr, bindings)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,294 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.*
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.model.ReadBook
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.MD5Utils
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.nio.charset.Charset
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Parcelize
|
||||
@TypeConverters(Book.Converters::class)
|
||||
@Entity(
|
||||
tableName = "books",
|
||||
indices = [Index(value = ["name", "author"], unique = true)]
|
||||
)
|
||||
data class Book(
|
||||
@PrimaryKey
|
||||
override var bookUrl: String = "", // 详情页Url(本地书源存储完整文件路径)
|
||||
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
|
||||
var origin: String = BookType.local, // 书源URL(默认BookType.local)
|
||||
var originName: String = "", //书源名称 or 本地书籍文件名
|
||||
override var name: String = "", // 书籍名称(书源获取)
|
||||
override var author: String = "", // 作者名称(书源获取)
|
||||
override var kind: String? = null, // 分类信息(书源获取)
|
||||
var customTag: String? = null, // 分类信息(用户修改)
|
||||
var coverUrl: String? = null, // 封面Url(书源获取)
|
||||
var customCoverUrl: String? = null, // 封面Url(用户修改)
|
||||
var intro: String? = null, // 简介内容(书源获取)
|
||||
var customIntro: String? = null, // 简介内容(用户修改)
|
||||
var charset: String? = null, // 自定义字符集名称(仅适用于本地书籍)
|
||||
var type: Int = 0, // 0:text 1:audio
|
||||
var group: Long = 0, // 自定义分组索引号
|
||||
var latestChapterTitle: String? = null, // 最新章节标题
|
||||
var latestChapterTime: Long = System.currentTimeMillis(), // 最新章节标题更新时间
|
||||
var lastCheckTime: Long = System.currentTimeMillis(), // 最近一次更新书籍信息的时间
|
||||
var lastCheckCount: Int = 0, // 最近一次发现新章节的数量
|
||||
var totalChapterNum: Int = 0, // 书籍目录总数
|
||||
var durChapterTitle: String? = null, // 当前章节名称
|
||||
var durChapterIndex: Int = 0, // 当前章节索引
|
||||
var durChapterPos: Int = 0, // 当前阅读的进度(首行字符的索引位置)
|
||||
var durChapterTime: Long = System.currentTimeMillis(), // 最近一次阅读书籍的时间(打开正文的时间)
|
||||
override var wordCount: String? = null,
|
||||
var canUpdate: Boolean = true, // 刷新书架时更新书籍信息
|
||||
var order: Int = 0, // 手动排序
|
||||
var originOrder: Int = 0, //书源排序
|
||||
var variable: String? = null, // 自定义书籍变量信息(用于书源规则检索书籍信息)
|
||||
var readConfig: ReadConfig? = null
|
||||
) : Parcelable, BaseBook {
|
||||
|
||||
fun isLocalBook(): Boolean {
|
||||
return origin == BookType.local
|
||||
}
|
||||
|
||||
fun isLocalTxt(): Boolean {
|
||||
return isLocalBook() && originName.endsWith(".txt", true)
|
||||
}
|
||||
|
||||
fun isEpub(): Boolean {
|
||||
return originName.endsWith(".epub", true)
|
||||
}
|
||||
|
||||
fun isUmd(): Boolean {
|
||||
return originName.endsWith(".umd", true)
|
||||
}
|
||||
|
||||
fun isOnLineTxt(): Boolean {
|
||||
return !isLocalBook() && type == 0
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is Book) {
|
||||
return other.bookUrl == bookUrl
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return bookUrl.hashCode()
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
override val variableMap by lazy {
|
||||
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
|
||||
}
|
||||
|
||||
override fun putVariable(key: String, value: String?) {
|
||||
if (value != null) {
|
||||
variableMap[key] = value
|
||||
} else {
|
||||
variableMap.remove(key)
|
||||
}
|
||||
variable = GSON.toJson(variableMap)
|
||||
}
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override var infoHtml: String? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override var tocHtml: String? = null
|
||||
|
||||
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
|
||||
|
||||
fun getUnreadChapterNum() = max(totalChapterNum - durChapterIndex - 1, 0)
|
||||
|
||||
fun getDisplayTag() = if (customTag.isNullOrBlank()) kind else customTag
|
||||
|
||||
fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
|
||||
|
||||
fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
|
||||
|
||||
//自定义简介有自动更新的需求时,可通过更新intro再调用upCustomIntro()完成
|
||||
@Suppress("unused")
|
||||
fun upCustomIntro() {
|
||||
customIntro = intro
|
||||
}
|
||||
|
||||
fun fileCharset(): Charset {
|
||||
return charset(charset ?: "UTF-8")
|
||||
}
|
||||
|
||||
private fun config(): ReadConfig {
|
||||
if (readConfig == null) {
|
||||
readConfig = ReadConfig()
|
||||
}
|
||||
return readConfig!!
|
||||
}
|
||||
|
||||
fun setReverseToc(reverseToc: Boolean) {
|
||||
config().reverseToc = reverseToc
|
||||
}
|
||||
|
||||
fun getReverseToc(): Boolean {
|
||||
return config().reverseToc
|
||||
}
|
||||
|
||||
fun setUseReplaceRule(useReplaceRule: Boolean) {
|
||||
config().useReplaceRule = useReplaceRule
|
||||
}
|
||||
|
||||
fun getUseReplaceRule(): Boolean {
|
||||
return config().useReplaceRule
|
||||
}
|
||||
|
||||
fun getReSegment(): Boolean {
|
||||
return config().reSegment
|
||||
}
|
||||
|
||||
fun setReSegment(reSegment: Boolean) {
|
||||
config().reSegment = reSegment
|
||||
}
|
||||
|
||||
fun getPageAnim(): Int {
|
||||
return config().pageAnim
|
||||
}
|
||||
|
||||
fun setPageAnim(pageAnim: Int) {
|
||||
config().pageAnim = pageAnim
|
||||
}
|
||||
|
||||
fun getImageStyle(): String? {
|
||||
return config().imageStyle
|
||||
}
|
||||
|
||||
fun setImageStyle(imageStyle: String?) {
|
||||
config().imageStyle = imageStyle
|
||||
}
|
||||
|
||||
fun setDelTag(tag: Long) {
|
||||
config().delTag =
|
||||
if ((config().delTag and tag) == tag) config().delTag and tag.inv() else config().delTag or tag
|
||||
}
|
||||
|
||||
fun getDelTag(tag: Long): Boolean {
|
||||
return config().delTag and tag == tag
|
||||
}
|
||||
|
||||
fun getFolderName(): String {
|
||||
//防止书名过长,只取9位
|
||||
var folderName = name.replace(AppPattern.fileNameRegex, "")
|
||||
folderName = folderName.substring(0, min(9, folderName.length))
|
||||
return folderName + MD5Utils.md5Encode16(bookUrl)
|
||||
}
|
||||
|
||||
fun toSearchBook() = SearchBook(
|
||||
name = name,
|
||||
author = author,
|
||||
kind = kind,
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
originName = originName,
|
||||
type = type,
|
||||
wordCount = wordCount,
|
||||
latestChapterTitle = latestChapterTitle,
|
||||
coverUrl = coverUrl,
|
||||
intro = intro,
|
||||
tocUrl = tocUrl,
|
||||
originOrder = originOrder,
|
||||
variable = variable
|
||||
).apply {
|
||||
this.infoHtml = this@Book.infoHtml
|
||||
this.tocHtml = this@Book.tocHtml
|
||||
}
|
||||
|
||||
fun changeTo(newBook: Book) {
|
||||
newBook.group = group
|
||||
newBook.order = order
|
||||
newBook.customCoverUrl = customCoverUrl
|
||||
newBook.customIntro = customIntro
|
||||
newBook.customTag = customTag
|
||||
newBook.canUpdate = canUpdate
|
||||
newBook.readConfig = readConfig
|
||||
delete(this)
|
||||
appDb.bookDao.insert(newBook)
|
||||
}
|
||||
|
||||
fun upInfoFromOld(oldBook: Book?) {
|
||||
oldBook?.let {
|
||||
group = oldBook.group
|
||||
durChapterIndex = oldBook.durChapterIndex
|
||||
durChapterPos = oldBook.durChapterPos
|
||||
durChapterTitle = oldBook.durChapterTitle
|
||||
customCoverUrl = oldBook.customCoverUrl
|
||||
customIntro = oldBook.customIntro
|
||||
order = oldBook.order
|
||||
if (coverUrl.isNullOrEmpty()) {
|
||||
coverUrl = oldBook.getDisplayCover()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createBookMark(): Bookmark {
|
||||
return Bookmark(
|
||||
bookName = name,
|
||||
bookAuthor = author,
|
||||
)
|
||||
}
|
||||
|
||||
fun save() {
|
||||
if (appDb.bookDao.has(bookUrl) == true) {
|
||||
appDb.bookDao.update(this)
|
||||
} else {
|
||||
appDb.bookDao.insert(this)
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val hTag = 2L
|
||||
const val rubyTag = 4L
|
||||
const val imgTag = 8L
|
||||
const val imgStyleDefault = "DEFAULT"
|
||||
const val imgStyleFull = "FULL"
|
||||
const val imgStyleText = "TEXT"
|
||||
|
||||
fun delete(book: Book?) {
|
||||
book ?: return
|
||||
if (ReadBook.book?.bookUrl == book.bookUrl) {
|
||||
ReadBook.book = null
|
||||
}
|
||||
appDb.bookDao.delete(book)
|
||||
}
|
||||
}
|
||||
|
||||
@Parcelize
|
||||
data class ReadConfig(
|
||||
var reverseToc: Boolean = false,
|
||||
var pageAnim: Int = -1,
|
||||
var reSegment: Boolean = false,
|
||||
var imageStyle: String? = null,
|
||||
var useReplaceRule: Boolean = AppConfig.replaceEnableDefault,// 正文使用净化替换规则
|
||||
var delTag: Long = 0L,//去除标签
|
||||
) : Parcelable
|
||||
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.ForeignKey
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.Index
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.help.AppConfig
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Suppress("unused")
|
||||
@Parcelize
|
||||
@Entity(
|
||||
tableName = "chapters",
|
||||
primaryKeys = ["url", "bookUrl"],
|
||||
indices = [(Index(value = ["bookUrl"], unique = false)),
|
||||
(Index(value = ["bookUrl", "index"], unique = true))],
|
||||
foreignKeys = [(ForeignKey(
|
||||
entity = Book::class,
|
||||
parentColumns = ["bookUrl"],
|
||||
childColumns = ["bookUrl"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
))]
|
||||
) // 删除书籍时自动删除章节
|
||||
data class BookChapter(
|
||||
var url: String = "", // 章节地址
|
||||
var title: String = "", // 章节标题
|
||||
var baseUrl: String = "", // 用来拼接相对url
|
||||
var bookUrl: String = "", // 书籍地址
|
||||
var index: Int = 0, // 章节序号
|
||||
var isVip: Boolean = false, // 是否VIP
|
||||
var isPay: Boolean = false, // 是否已购买
|
||||
var resourceUrl: String? = null, // 音频真实URL
|
||||
var tag: String? = null, //
|
||||
var start: Long? = null, // 章节起始位置
|
||||
var end: Long? = null, // 章节终止位置
|
||||
var startFragmentId: String? = null, //EPUB书籍当前章节的fragmentId
|
||||
var endFragmentId: String? = null, //EPUB书籍下一章节的fragmentId
|
||||
var variable: String? = null //变量
|
||||
) : Parcelable, RuleDataInterface {
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
override val variableMap by lazy {
|
||||
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
|
||||
}
|
||||
|
||||
override fun putVariable(key: String, value: String?) {
|
||||
if (value != null) {
|
||||
variableMap[key] = value
|
||||
} else {
|
||||
variableMap.remove(key)
|
||||
}
|
||||
variable = GSON.toJson(variableMap)
|
||||
}
|
||||
|
||||
override fun hashCode() = url.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is BookChapter) {
|
||||
return other.url == url
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
fun getDisplayTitle(
|
||||
replaceRules: Array<ReplaceRule>? = null,
|
||||
useReplace: Boolean = true,
|
||||
chineseConvert: Boolean = true,
|
||||
): String {
|
||||
var displayTitle = title.replace(AppPattern.rnRegex, "")
|
||||
if (useReplace && replaceRules != null) {
|
||||
replaceRules.forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
try {
|
||||
displayTitle = if (item.isRegex) {
|
||||
displayTitle.replace(item.pattern.toRegex(), item.replacement)
|
||||
} else {
|
||||
displayTitle.replace(item.pattern, item.replacement)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chineseConvert) {
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> displayTitle = ChineseUtils.t2s(displayTitle)
|
||||
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
|
||||
}
|
||||
}
|
||||
return when {
|
||||
!isVip -> displayTitle
|
||||
isPay -> appCtx.getString(R.string.payed_title, displayTitle)
|
||||
else -> appCtx.getString(R.string.vip_title, displayTitle)
|
||||
}
|
||||
}
|
||||
|
||||
fun getAbsoluteURL(): String {
|
||||
val urlMatcher = AnalyzeUrl.paramPattern.matcher(url)
|
||||
val urlBefore = if (urlMatcher.find()) url.substring(0, urlMatcher.start()) else url
|
||||
val urlAbsoluteBefore = NetworkUtils.getAbsoluteURL(baseUrl, urlBefore)
|
||||
return if (urlBefore.length == url.length) {
|
||||
urlAbsoluteBefore
|
||||
} else {
|
||||
"$urlAbsoluteBefore," + url.substring(urlMatcher.end())
|
||||
}
|
||||
}
|
||||
|
||||
fun getFileName(): String = String.format("%05d-%s.nb", index, MD5Utils.md5Encode16(title))
|
||||
|
||||
fun getFontName(): String = String.format("%05d-%s.ttf", index, MD5Utils.md5Encode16(title))
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import io.legado.app.R
|
||||
import io.legado.app.constant.AppConst
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "book_groups")
|
||||
data class BookGroup(
|
||||
@PrimaryKey
|
||||
val groupId: Long = 0b1,
|
||||
var groupName: String,
|
||||
var cover: String? = null,
|
||||
var order: Int = 0,
|
||||
var show: Boolean = true
|
||||
) : Parcelable {
|
||||
|
||||
fun getManageName(context: Context): String {
|
||||
return when (groupId) {
|
||||
AppConst.bookGroupAllId -> "$groupName(${context.getString(R.string.all)})"
|
||||
AppConst.bookGroupAudioId -> "$groupName(${context.getString(R.string.audio)})"
|
||||
AppConst.bookGroupLocalId -> "$groupName(${context.getString(R.string.local)})"
|
||||
AppConst.bookGroupNoneId -> "$groupName(${context.getString(R.string.no_group)})"
|
||||
else -> groupName
|
||||
}
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return groupId.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is BookGroup) {
|
||||
return other.groupId == groupId
|
||||
&& other.groupName == groupName
|
||||
&& other.cover == cover
|
||||
&& other.order == order
|
||||
&& other.show == show
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
data class BookProgress(
|
||||
val name: String,
|
||||
val author: String,
|
||||
val durChapterIndex: Int,
|
||||
val durChapterPos: Int,
|
||||
val durChapterTime: Long,
|
||||
val durChapterTitle: String?
|
||||
) {
|
||||
|
||||
constructor(book: Book) : this(
|
||||
name = book.name,
|
||||
author = book.author,
|
||||
durChapterIndex = book.durChapterIndex,
|
||||
durChapterPos = book.durChapterPos,
|
||||
durChapterTime = book.durChapterTime,
|
||||
durChapterTitle = book.durChapterTitle
|
||||
)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import androidx.room.*
|
||||
import io.legado.app.constant.BookType
|
||||
import io.legado.app.data.entities.rule.*
|
||||
import io.legado.app.help.SourceAnalyzer
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
import timber.log.Timber
|
||||
|
||||
@Parcelize
|
||||
@TypeConverters(BookSource.Converters::class)
|
||||
@Entity(
|
||||
tableName = "book_sources",
|
||||
indices = [(Index(value = ["bookSourceUrl"], unique = false))]
|
||||
)
|
||||
data class BookSource(
|
||||
@PrimaryKey
|
||||
var bookSourceUrl: String = "", // 地址,包括 http/https
|
||||
var bookSourceName: String = "", // 名称
|
||||
var bookSourceGroup: String? = null, // 分组
|
||||
var bookSourceType: Int = BookType.default, // 类型,0 文本,1 音频, 3 图片
|
||||
var bookUrlPattern: String? = null, // 详情页url正则
|
||||
var customOrder: Int = 0, // 手动排序编号
|
||||
var enabled: Boolean = true, // 是否启用
|
||||
var enabledExplore: Boolean = true, // 启用发现
|
||||
override var concurrentRate: String? = null, // 并发率
|
||||
override var header: String? = null, // 请求头
|
||||
override var loginUrl: String? = null, // 登录地址
|
||||
override var loginUi: String? = null, // 登录UI
|
||||
var loginCheckJs: String? = null, // 登录检测js
|
||||
var bookSourceComment: String? = null, // 注释
|
||||
var lastUpdateTime: Long = 0, // 最后更新时间,用于排序
|
||||
var respondTime: Long = 180000L, // 响应时间,用于排序
|
||||
var weight: Int = 0, // 智能排序的权重
|
||||
var exploreUrl: String? = null, // 发现url
|
||||
var ruleExplore: ExploreRule? = null, // 发现规则
|
||||
var searchUrl: String? = null, // 搜索url
|
||||
var ruleSearch: SearchRule? = null, // 搜索规则
|
||||
var ruleBookInfo: BookInfoRule? = null, // 书籍信息页规则
|
||||
var ruleToc: TocRule? = null, // 目录页规则
|
||||
var ruleContent: ContentRule? = null // 正文页规则
|
||||
) : Parcelable, BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
return bookSourceName
|
||||
}
|
||||
|
||||
override fun getKey(): String {
|
||||
return bookSourceUrl
|
||||
}
|
||||
|
||||
override fun getSource(): BaseSource {
|
||||
return this
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
val exploreKinds: List<ExploreKind> by lazy {
|
||||
val exploreUrl = exploreUrl ?: return@lazy emptyList()
|
||||
val kinds = arrayListOf<ExploreKind>()
|
||||
var ruleStr = exploreUrl
|
||||
if (ruleStr.isNotBlank()) {
|
||||
kotlin.runCatching {
|
||||
if (exploreUrl.startsWith("<js>", false)
|
||||
|| exploreUrl.startsWith("@js:", false)
|
||||
) {
|
||||
val aCache = ACache.get(appCtx, "explore")
|
||||
ruleStr = aCache.getAsString(bookSourceUrl) ?: ""
|
||||
if (ruleStr.isBlank()) {
|
||||
val jsStr = if (exploreUrl.startsWith("@")) {
|
||||
exploreUrl.substring(4)
|
||||
} else {
|
||||
exploreUrl.substring(4, exploreUrl.lastIndexOf("<"))
|
||||
}
|
||||
ruleStr = evalJS(jsStr).toString().trim()
|
||||
aCache.put(bookSourceUrl, ruleStr)
|
||||
}
|
||||
}
|
||||
if (ruleStr.isJsonArray()) {
|
||||
GSON.fromJsonArray<ExploreKind>(ruleStr)?.let {
|
||||
kinds.addAll(it)
|
||||
}
|
||||
} else {
|
||||
ruleStr.split("(&&|\n)+".toRegex()).forEach { kindStr ->
|
||||
val kindCfg = kindStr.split("::")
|
||||
kinds.add(ExploreKind(kindCfg.first(), kindCfg.getOrNull(1)))
|
||||
}
|
||||
}
|
||||
}.onFailure {
|
||||
kinds.add(ExploreKind("ERROR:${it.localizedMessage}", it.stackTraceToString()))
|
||||
Timber.e(it)
|
||||
}
|
||||
}
|
||||
return@lazy kinds
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return bookSourceUrl.hashCode()
|
||||
}
|
||||
|
||||
override fun equals(other: Any?) =
|
||||
if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false
|
||||
|
||||
fun getSearchRule() = ruleSearch ?: SearchRule()
|
||||
|
||||
fun getExploreRule() = ruleExplore ?: ExploreRule()
|
||||
|
||||
fun getBookInfoRule() = ruleBookInfo ?: BookInfoRule()
|
||||
|
||||
fun getTocRule() = ruleToc ?: TocRule()
|
||||
|
||||
fun getContentRule() = ruleContent ?: ContentRule()
|
||||
|
||||
fun addGroup(group: String) {
|
||||
bookSourceGroup?.let {
|
||||
if (!it.contains(group)) {
|
||||
bookSourceGroup = "$it,$group"
|
||||
}
|
||||
} ?: let {
|
||||
bookSourceGroup = group
|
||||
}
|
||||
}
|
||||
|
||||
fun removeGroup(group: String) {
|
||||
bookSourceGroup?.splitNotBlank("[,;,;]".toRegex())?.toHashSet()?.let {
|
||||
it.remove(group)
|
||||
bookSourceGroup = TextUtils.join(",", it)
|
||||
}
|
||||
}
|
||||
|
||||
fun equal(source: BookSource) =
|
||||
equal(bookSourceName, source.bookSourceName)
|
||||
&& equal(bookSourceUrl, source.bookSourceUrl)
|
||||
&& equal(bookSourceGroup, source.bookSourceGroup)
|
||||
&& bookSourceType == source.bookSourceType
|
||||
&& equal(bookUrlPattern, source.bookUrlPattern)
|
||||
&& equal(bookSourceComment, source.bookSourceComment)
|
||||
&& enabled == source.enabled
|
||||
&& enabledExplore == source.enabledExplore
|
||||
&& equal(header, source.header)
|
||||
&& loginUrl == source.loginUrl
|
||||
&& equal(exploreUrl, source.exploreUrl)
|
||||
&& equal(searchUrl, source.searchUrl)
|
||||
&& getSearchRule() == source.getSearchRule()
|
||||
&& getExploreRule() == source.getExploreRule()
|
||||
&& getBookInfoRule() == source.getBookInfoRule()
|
||||
&& getTocRule() == source.getTocRule()
|
||||
&& getContentRule() == source.getContentRule()
|
||||
|
||||
private fun equal(a: String?, b: String?) = a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
|
||||
|
||||
companion object {
|
||||
|
||||
fun fromJson(json: String): BookSource? {
|
||||
return SourceAnalyzer.jsonToBookSource(json)
|
||||
}
|
||||
|
||||
fun fromJsonArray(json: String): List<BookSource> {
|
||||
return SourceAnalyzer.jsonToBookSources(json)
|
||||
}
|
||||
}
|
||||
|
||||
class Converters {
|
||||
|
||||
@TypeConverter
|
||||
fun exploreRuleToString(exploreRule: ExploreRule?): String = GSON.toJson(exploreRule)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToExploreRule(json: String?) = GSON.fromJsonObject<ExploreRule>(json)
|
||||
|
||||
@TypeConverter
|
||||
fun searchRuleToString(searchRule: SearchRule?): String = GSON.toJson(searchRule)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToSearchRule(json: String?) = GSON.fromJsonObject<SearchRule>(json)
|
||||
|
||||
@TypeConverter
|
||||
fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String = GSON.toJson(bookInfoRule)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToBookInfoRule(json: String?) = GSON.fromJsonObject<BookInfoRule>(json)
|
||||
|
||||
@TypeConverter
|
||||
fun tocRuleToString(tocRule: TocRule?): String = GSON.toJson(tocRule)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToTocRule(json: String?) = GSON.fromJsonObject<TocRule>(json)
|
||||
|
||||
@TypeConverter
|
||||
fun contentRuleToString(contentRule: ContentRule?): String = GSON.toJson(contentRule)
|
||||
|
||||
@TypeConverter
|
||||
fun stringToContentRule(json: String?) = GSON.fromJsonObject<ContentRule>(json)
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(
|
||||
tableName = "bookmarks",
|
||||
indices = [(Index(value = ["bookName", "bookAuthor"], unique = false))]
|
||||
)
|
||||
data class Bookmark(
|
||||
@PrimaryKey
|
||||
val time: Long = System.currentTimeMillis(),
|
||||
val bookName: String = "",
|
||||
val bookAuthor: String = "",
|
||||
var chapterIndex: Int = 0,
|
||||
var chapterPos: Int = 0,
|
||||
var chapterName: String = "",
|
||||
var bookText: String = "",
|
||||
var content: String = ""
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,13 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "caches", indices = [(Index(value = ["key"], unique = true))])
|
||||
data class Cache(
|
||||
@PrimaryKey
|
||||
val key: String = "",
|
||||
var value: String? = null,
|
||||
var deadline: Long = 0L
|
||||
)
|
||||
@@ -0,0 +1,12 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "cookies", indices = [(Index(value = ["url"], unique = true))])
|
||||
data class Cookie(
|
||||
@PrimaryKey
|
||||
var url: String = "",
|
||||
var cookie: String = ""
|
||||
)
|
||||
@@ -0,0 +1,78 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
import com.jayway.jsonpath.DocumentContext
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.jsonPath
|
||||
import io.legado.app.utils.readLong
|
||||
import io.legado.app.utils.readString
|
||||
|
||||
/**
|
||||
* 在线朗读引擎
|
||||
*/
|
||||
@Entity(tableName = "httpTTS")
|
||||
data class HttpTTS(
|
||||
@PrimaryKey
|
||||
val id: Long = System.currentTimeMillis(),
|
||||
var name: String = "",
|
||||
var url: String = "",
|
||||
var contentType: String? = null,
|
||||
override var concurrentRate: String? = "0",
|
||||
override var loginUrl: String? = null,
|
||||
override var loginUi: String? = null,
|
||||
override var header: String? = null,
|
||||
var loginCheckJs: String? = null,
|
||||
) : BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
return name
|
||||
}
|
||||
|
||||
override fun getKey(): String {
|
||||
return "httpTts:$id"
|
||||
}
|
||||
|
||||
override fun getSource(): BaseSource {
|
||||
return this
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
companion object {
|
||||
|
||||
fun fromJsonDoc(doc: DocumentContext): HttpTTS? {
|
||||
return kotlin.runCatching {
|
||||
val loginUi = doc.read<Any>("$.loginUi")
|
||||
HttpTTS(
|
||||
id = doc.readLong("$.id") ?: System.currentTimeMillis(),
|
||||
name = doc.readString("$.name")!!,
|
||||
url = doc.readString("$.url")!!,
|
||||
contentType = doc.readString("$.contentType"),
|
||||
concurrentRate = doc.readString("$.concurrentRate"),
|
||||
loginUrl = doc.readString("$.loginUrl"),
|
||||
loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(),
|
||||
header = doc.readString("$.header"),
|
||||
loginCheckJs = doc.readString("$.loginCheckJs")
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun fromJson(json: String): HttpTTS? {
|
||||
return fromJsonDoc(jsonPath.parse(json))
|
||||
}
|
||||
|
||||
fun fromJsonArray(jsonArray: String): ArrayList<HttpTTS> {
|
||||
val sources = arrayListOf<HttpTTS>()
|
||||
val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
|
||||
doc.forEach {
|
||||
val jsonItem = jsonPath.parse(it)
|
||||
fromJsonDoc(jsonItem)?.let { source ->
|
||||
sources.add(source)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
|
||||
@Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"])
|
||||
data class ReadRecord(
|
||||
var deviceId: String = "",
|
||||
var bookName: String = "",
|
||||
var readTime: Long = 0L
|
||||
)
|
||||
@@ -0,0 +1,6 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
data class ReadRecordShow(
|
||||
var bookName: String = "",
|
||||
var readTime: Long = 0L
|
||||
)
|
||||
@@ -0,0 +1,57 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import android.text.TextUtils
|
||||
import androidx.room.ColumnInfo
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import java.util.regex.Pattern
|
||||
import java.util.regex.PatternSyntaxException
|
||||
|
||||
@Parcelize
|
||||
@Entity(
|
||||
tableName = "replace_rules",
|
||||
indices = [(Index(value = ["id"]))]
|
||||
)
|
||||
data class ReplaceRule(
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
var id: Long = System.currentTimeMillis(),
|
||||
var name: String = "",
|
||||
var group: String? = null,
|
||||
var pattern: String = "",
|
||||
var replacement: String = "",
|
||||
var scope: String? = null,
|
||||
var isEnabled: Boolean = true,
|
||||
var isRegex: Boolean = true,
|
||||
@ColumnInfo(name = "sortOrder")
|
||||
var order: Int = 0
|
||||
) : Parcelable {
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is ReplaceRule) {
|
||||
return other.id == id
|
||||
}
|
||||
return super.equals(other)
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return id.hashCode()
|
||||
}
|
||||
|
||||
fun isValid(): Boolean {
|
||||
if (TextUtils.isEmpty(pattern)) {
|
||||
return false
|
||||
}
|
||||
//判断正则表达式是否正确
|
||||
if (isRegex) {
|
||||
try {
|
||||
Pattern.compile(pattern)
|
||||
} catch (ex: PatternSyntaxException) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
|
||||
|
||||
@Entity(
|
||||
tableName = "rssArticles",
|
||||
primaryKeys = ["origin", "link"]
|
||||
)
|
||||
data class RssArticle(
|
||||
var origin: String = "",
|
||||
var sort: String = "",
|
||||
var title: String = "",
|
||||
var order: Long = 0,
|
||||
var link: String = "",
|
||||
var pubDate: String? = null,
|
||||
var description: String? = null,
|
||||
var content: String? = null,
|
||||
var image: String? = null,
|
||||
var read: Boolean = false,
|
||||
var variable: String? = null
|
||||
) : RuleDataInterface {
|
||||
|
||||
override fun hashCode() = link.hashCode()
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
other ?: return false
|
||||
return if (other is RssArticle) origin == other.origin && link == other.link else false
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
override val variableMap by lazy {
|
||||
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
|
||||
}
|
||||
|
||||
override fun putVariable(key: String, value: String?) {
|
||||
if (value != null) {
|
||||
variableMap[key] = value
|
||||
} else {
|
||||
variableMap.remove(key)
|
||||
}
|
||||
variable = GSON.toJson(variableMap)
|
||||
}
|
||||
|
||||
fun toStar() = RssStar(
|
||||
origin = origin,
|
||||
sort = sort,
|
||||
title = title,
|
||||
starTime = System.currentTimeMillis(),
|
||||
link = link,
|
||||
pubDate = pubDate,
|
||||
description = description,
|
||||
content = content,
|
||||
image = image
|
||||
)
|
||||
}
|
||||
@@ -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,170 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import com.jayway.jsonpath.DocumentContext
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.parcelize.Parcelize
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "rssSources", indices = [(Index(value = ["sourceUrl"], unique = false))])
|
||||
data class RssSource(
|
||||
@PrimaryKey
|
||||
var sourceUrl: String = "",
|
||||
var sourceName: String = "",
|
||||
var sourceIcon: String = "",
|
||||
var sourceGroup: String? = null,
|
||||
var sourceComment: String? = null,
|
||||
var enabled: Boolean = true,
|
||||
override var concurrentRate: String? = null, //并发率
|
||||
override var header: String? = null, // 请求头
|
||||
override var loginUrl: String? = null, // 登录地址
|
||||
override var loginUi: String? = null, //登录UI
|
||||
var loginCheckJs: String? = null, //登录检测js
|
||||
var sortUrl: String? = null,
|
||||
var singleUrl: Boolean = false,
|
||||
//列表规则
|
||||
var articleStyle: Int = 0, //列表样式,0,1,2
|
||||
var ruleArticles: String? = null,
|
||||
var ruleNextPage: String? = null,
|
||||
var ruleTitle: String? = null,
|
||||
var rulePubDate: String? = null,
|
||||
//webView规则
|
||||
var ruleDescription: String? = null,
|
||||
var ruleImage: String? = null,
|
||||
var ruleLink: String? = null,
|
||||
var ruleContent: String? = null,
|
||||
var style: String? = null,
|
||||
var enableJs: Boolean = true,
|
||||
var loadWithBaseUrl: Boolean = true,
|
||||
var customOrder: Int = 0
|
||||
) : Parcelable, BaseSource {
|
||||
|
||||
override fun getTag(): String {
|
||||
return sourceName
|
||||
}
|
||||
|
||||
override fun getKey(): String {
|
||||
return sourceUrl
|
||||
}
|
||||
|
||||
override fun getSource(): BaseSource {
|
||||
return this
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (other is RssSource) {
|
||||
return other.sourceUrl == sourceUrl
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode() = sourceUrl.hashCode()
|
||||
|
||||
fun equal(source: RssSource): Boolean {
|
||||
return equal(sourceUrl, source.sourceUrl)
|
||||
&& equal(sourceIcon, source.sourceIcon)
|
||||
&& enabled == source.enabled
|
||||
&& equal(sourceGroup, source.sourceGroup)
|
||||
&& equal(ruleArticles, source.ruleArticles)
|
||||
&& equal(ruleNextPage, source.ruleNextPage)
|
||||
&& equal(ruleTitle, source.ruleTitle)
|
||||
&& equal(rulePubDate, source.rulePubDate)
|
||||
&& equal(ruleDescription, source.ruleDescription)
|
||||
&& equal(ruleLink, source.ruleLink)
|
||||
&& equal(ruleContent, source.ruleContent)
|
||||
&& enableJs == source.enableJs
|
||||
&& loadWithBaseUrl == source.loadWithBaseUrl
|
||||
}
|
||||
|
||||
private fun equal(a: String?, b: String?): Boolean {
|
||||
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
|
||||
}
|
||||
|
||||
fun sortUrls(): List<Pair<String, String>> = arrayListOf<Pair<String, String>>().apply {
|
||||
kotlin.runCatching {
|
||||
var a = sortUrl
|
||||
if (sortUrl?.startsWith("<js>", false) == true
|
||||
|| sortUrl?.startsWith("@js:", false) == true
|
||||
) {
|
||||
val aCache = ACache.get(appCtx, "rssSortUrl")
|
||||
a = aCache.getAsString(sourceUrl) ?: ""
|
||||
if (a.isBlank()) {
|
||||
val jsStr = if (sortUrl!!.startsWith("@")) {
|
||||
sortUrl!!.substring(4)
|
||||
} else {
|
||||
sortUrl!!.substring(4, sortUrl!!.lastIndexOf("<"))
|
||||
}
|
||||
a = evalJS(jsStr).toString()
|
||||
aCache.put(sourceUrl, a)
|
||||
}
|
||||
}
|
||||
a?.split("(&&|\n)+".toRegex())?.forEach { c ->
|
||||
val d = c.split("::")
|
||||
if (d.size > 1)
|
||||
add(Pair(d[0], d[1]))
|
||||
}
|
||||
if (isEmpty()) {
|
||||
add(Pair("", sourceUrl))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
companion object {
|
||||
|
||||
fun fromJsonDoc(doc: DocumentContext): RssSource? {
|
||||
return kotlin.runCatching {
|
||||
val loginUi = doc.read<Any>("$.loginUi")
|
||||
RssSource(
|
||||
sourceUrl = doc.readString("$.sourceUrl")!!,
|
||||
sourceName = doc.readString("$.sourceName")!!,
|
||||
sourceIcon = doc.readString("$.sourceIcon") ?: "",
|
||||
sourceGroup = doc.readString("$.sourceGroup"),
|
||||
sourceComment = doc.readString("$.sourceComment"),
|
||||
enabled = doc.readBool("$.enabled") ?: true,
|
||||
concurrentRate = doc.readString("$.concurrentRate"),
|
||||
header = doc.readString("$.header"),
|
||||
loginUrl = doc.readString("$.loginUrl"),
|
||||
loginUi = if (loginUi is List<*>) GSON.toJson(loginUi) else loginUi?.toString(),
|
||||
loginCheckJs = doc.readString("$.loginCheckJs"),
|
||||
sortUrl = doc.readString("$.sortUrl"),
|
||||
singleUrl = doc.readBool("$.singleUrl") ?: false,
|
||||
articleStyle = doc.readInt("$.articleStyle") ?: 0,
|
||||
ruleArticles = doc.readString("$.ruleArticles"),
|
||||
ruleNextPage = doc.readString("$.ruleNextPage"),
|
||||
ruleTitle = doc.readString("$.ruleTitle"),
|
||||
rulePubDate = doc.readString("$.rulePubDate"),
|
||||
ruleDescription = doc.readString("$.ruleDescription"),
|
||||
ruleImage = doc.readString("$.ruleImage"),
|
||||
ruleLink = doc.readString("$.ruleLink"),
|
||||
ruleContent = doc.readString("$.ruleContent"),
|
||||
style = doc.readString("$.style"),
|
||||
enableJs = doc.readBool("$.enableJs") ?: true,
|
||||
loadWithBaseUrl = doc.readBool("$.loadWithBaseUrl") ?: true,
|
||||
customOrder = doc.readInt("$.customOrder") ?: 0
|
||||
)
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
fun fromJson(json: String): RssSource? {
|
||||
return fromJsonDoc(jsonPath.parse(json))
|
||||
}
|
||||
|
||||
fun fromJsonArray(jsonArray: String): ArrayList<RssSource> {
|
||||
val sources = arrayListOf<RssSource>()
|
||||
val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
|
||||
doc.forEach {
|
||||
val jsonItem = jsonPath.parse(it)
|
||||
fromJsonDoc(jsonItem)?.let { source ->
|
||||
sources.add(source)
|
||||
}
|
||||
}
|
||||
return sources
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import io.legado.app.model.analyzeRule.RuleDataInterface
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
|
||||
|
||||
@Entity(
|
||||
tableName = "rssStars",
|
||||
primaryKeys = ["origin", "link"]
|
||||
)
|
||||
data class RssStar(
|
||||
var origin: String = "",
|
||||
var sort: String = "",
|
||||
var title: String = "",
|
||||
var starTime: Long = 0,
|
||||
var link: String = "",
|
||||
var pubDate: String? = null,
|
||||
var description: String? = null,
|
||||
var content: String? = null,
|
||||
var image: String? = null,
|
||||
var variable: String? = null
|
||||
) : RuleDataInterface {
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
override val variableMap by lazy {
|
||||
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
|
||||
}
|
||||
|
||||
override fun putVariable(key: String, value: String?) {
|
||||
if (value != null) {
|
||||
variableMap[key] = value
|
||||
} else {
|
||||
variableMap.remove(key)
|
||||
}
|
||||
variable = GSON.toJson(variableMap)
|
||||
}
|
||||
|
||||
fun toRssArticle() = RssArticle(
|
||||
origin = origin,
|
||||
sort = sort,
|
||||
title = title,
|
||||
link = link,
|
||||
pubDate = pubDate,
|
||||
description = description,
|
||||
content = content,
|
||||
image = image
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
@Entity(tableName = "ruleSubs")
|
||||
data class RuleSub(
|
||||
@PrimaryKey
|
||||
val id: Long = System.currentTimeMillis(),
|
||||
var name: String = "",
|
||||
var url: String = "",
|
||||
var type: Int = 0,
|
||||
var customOrder: Int = 0,
|
||||
var autoUpdate: Boolean = false,
|
||||
var update: Long = System.currentTimeMillis()
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Parcelable
|
||||
import androidx.room.*
|
||||
import io.legado.app.R
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonObject
|
||||
import kotlinx.parcelize.IgnoredOnParcel
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
@Entity(
|
||||
tableName = "searchBooks",
|
||||
indices = [(Index(value = ["bookUrl"], unique = true)),
|
||||
(Index(value = ["origin"], unique = false))],
|
||||
foreignKeys = [(ForeignKey(
|
||||
entity = BookSource::class,
|
||||
parentColumns = ["bookSourceUrl"],
|
||||
childColumns = ["origin"],
|
||||
onDelete = ForeignKey.CASCADE
|
||||
))]
|
||||
)
|
||||
data class SearchBook(
|
||||
@PrimaryKey
|
||||
override var bookUrl: String = "",
|
||||
var origin: String = "", // 书源规则
|
||||
var originName: String = "",
|
||||
var type: Int = 0, // @BookType
|
||||
override var name: String = "",
|
||||
override var author: String = "",
|
||||
override var kind: String? = null,
|
||||
var coverUrl: String? = null,
|
||||
var intro: String? = null,
|
||||
override var wordCount: String? = null,
|
||||
var latestChapterTitle: String? = null,
|
||||
var tocUrl: String = "", // 目录页Url (toc=table of Contents)
|
||||
var time: Long = System.currentTimeMillis(),
|
||||
var variable: String? = null,
|
||||
var originOrder: Int = 0
|
||||
) : Parcelable, BaseBook, Comparable<SearchBook> {
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override var infoHtml: String? = null
|
||||
|
||||
@Ignore
|
||||
@IgnoredOnParcel
|
||||
override var tocHtml: String? = null
|
||||
|
||||
override fun equals(other: Any?) = other is SearchBook && other.bookUrl == bookUrl
|
||||
|
||||
override fun hashCode() = bookUrl.hashCode()
|
||||
|
||||
override fun compareTo(other: SearchBook): Int {
|
||||
return other.originOrder - this.originOrder
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
override val variableMap by lazy {
|
||||
GSON.fromJsonObject<HashMap<String, String>>(variable) ?: HashMap()
|
||||
}
|
||||
|
||||
override fun putVariable(key: String, value: String?) {
|
||||
if (value != null) {
|
||||
variableMap[key] = value
|
||||
} else {
|
||||
variableMap.remove(key)
|
||||
}
|
||||
variable = GSON.toJson(variableMap)
|
||||
}
|
||||
|
||||
@delegate:Transient
|
||||
@delegate:Ignore
|
||||
@IgnoredOnParcel
|
||||
val origins: LinkedHashSet<String> by lazy { linkedSetOf(origin) }
|
||||
|
||||
fun addOrigin(origin: String) {
|
||||
origins.add(origin)
|
||||
}
|
||||
|
||||
fun getDisplayLastChapterTitle(): String {
|
||||
latestChapterTitle?.let {
|
||||
if (it.isNotEmpty()) {
|
||||
return it
|
||||
}
|
||||
}
|
||||
return "无最新章节"
|
||||
}
|
||||
|
||||
fun trimIntro(context: Context): String {
|
||||
val trimIntro = intro?.trim()
|
||||
return if (trimIntro.isNullOrEmpty()) {
|
||||
context.getString(R.string.intro_show_null)
|
||||
} else {
|
||||
context.getString(R.string.intro_show, trimIntro)
|
||||
}
|
||||
}
|
||||
|
||||
fun toBook() = Book(
|
||||
name = name,
|
||||
author = author,
|
||||
kind = kind,
|
||||
bookUrl = bookUrl,
|
||||
origin = origin,
|
||||
originName = originName,
|
||||
type = type,
|
||||
wordCount = wordCount,
|
||||
latestChapterTitle = latestChapterTitle,
|
||||
coverUrl = coverUrl,
|
||||
intro = intro,
|
||||
tocUrl = tocUrl,
|
||||
originOrder = originOrder,
|
||||
variable = variable
|
||||
).apply {
|
||||
this.infoHtml = this@SearchBook.infoHtml
|
||||
this.tocUrl = this@SearchBook.tocUrl
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import android.os.Parcelable
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Index
|
||||
import androidx.room.PrimaryKey
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
@Entity(tableName = "search_keywords", indices = [(Index(value = ["word"], unique = true))])
|
||||
data class SearchKeyword(
|
||||
@PrimaryKey
|
||||
var word: String = "", // 搜索关键词
|
||||
var usage: Int = 1, // 使用次数
|
||||
var lastUseTime: Long = System.currentTimeMillis() // 最后一次使用时间
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.legado.app.data.entities
|
||||
|
||||
import androidx.room.Entity
|
||||
import androidx.room.Ignore
|
||||
import androidx.room.PrimaryKey
|
||||
|
||||
|
||||
@Entity(tableName = "txtTocRules")
|
||||
data class TxtTocRule(
|
||||
@PrimaryKey
|
||||
var id: Long = System.currentTimeMillis(),
|
||||
var name: String = "",
|
||||
var rule: String = "",
|
||||
var serialNumber: Int = -1,
|
||||
var enable: Boolean = true
|
||||
) {
|
||||
|
||||
@Ignore
|
||||
constructor() : this(name = "")
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class BookInfoRule(
|
||||
var init: String? = null,
|
||||
var name: String? = null,
|
||||
var author: String? = null,
|
||||
var intro: String? = null,
|
||||
var kind: String? = null,
|
||||
var lastChapter: String? = null,
|
||||
var updateTime: String? = null,
|
||||
var coverUrl: String? = null,
|
||||
var tocUrl: String? = null,
|
||||
var wordCount: String? = null,
|
||||
var canReName: String? = null
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
interface BookListRule {
|
||||
var bookList: String?
|
||||
var name: String?
|
||||
var author: String?
|
||||
var intro: String?
|
||||
var kind: String?
|
||||
var lastChapter: String?
|
||||
var updateTime: String?
|
||||
var bookUrl: String?
|
||||
var coverUrl: String?
|
||||
var wordCount: String?
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ContentRule(
|
||||
var content: String? = null,
|
||||
var nextContentUrl: String? = null,
|
||||
var webJs: String? = null,
|
||||
var sourceRegex: String? = null,
|
||||
var replaceRegex: String? = null, //替换规则
|
||||
var imageStyle: String? = null, //默认大小居中,FULL最大宽度
|
||||
var payAction: String? = null, //购买操作,url/js
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,39 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
data class ExploreKind(
|
||||
val title: String,
|
||||
val url: String? = null,
|
||||
val style: Style? = null
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val defaultStyle = Style()
|
||||
}
|
||||
|
||||
fun style(): Style {
|
||||
return style ?: defaultStyle
|
||||
}
|
||||
|
||||
data class Style(
|
||||
val layout_flexGrow: Float = 0F,
|
||||
val layout_flexShrink: Float = 1F,
|
||||
val layout_alignSelf: String = "auto",
|
||||
val layout_flexBasisPercent: Float = -1F,
|
||||
val layout_wrapBefore: Boolean = false,
|
||||
) {
|
||||
|
||||
fun alignSelf(): Int {
|
||||
return when (layout_alignSelf) {
|
||||
"auto" -> -1
|
||||
"flex_start" -> 0
|
||||
"flex_end" -> 1
|
||||
"center" -> 2
|
||||
"baseline" -> 3
|
||||
"stretch" -> 4
|
||||
else -> -1
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class ExploreRule(
|
||||
override var bookList: String? = null,
|
||||
override var name: String? = null,
|
||||
override var author: String? = null,
|
||||
override var intro: String? = null,
|
||||
override var kind: String? = null,
|
||||
override var lastChapter: String? = null,
|
||||
override var updateTime: String? = null,
|
||||
override var bookUrl: String? = null,
|
||||
override var coverUrl: String? = null,
|
||||
override var wordCount: String? = null
|
||||
) : BookListRule, Parcelable
|
||||
@@ -0,0 +1,11 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class RowUi(
|
||||
var name: String,
|
||||
var type: String?,
|
||||
var action: String?
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,20 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
|
||||
@Parcelize
|
||||
data class SearchRule(
|
||||
var checkKeyWord: String? = null, // 校验关键字
|
||||
override var bookList: String? = null,
|
||||
override var name: String? = null,
|
||||
override var author: String? = null,
|
||||
override var intro: String? = null,
|
||||
override var kind: String? = null,
|
||||
override var lastChapter: String? = null,
|
||||
override var updateTime: String? = null,
|
||||
override var bookUrl: String? = null,
|
||||
override var coverUrl: String? = null,
|
||||
override var wordCount: String? = null
|
||||
) : BookListRule, Parcelable
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.legado.app.data.entities.rule
|
||||
|
||||
import android.os.Parcelable
|
||||
import kotlinx.parcelize.Parcelize
|
||||
|
||||
@Parcelize
|
||||
data class TocRule(
|
||||
var chapterList: String? = null,
|
||||
var chapterName: String? = null,
|
||||
var chapterUrl: String? = null,
|
||||
var isVip: String? = null,
|
||||
var isPay: String? = null,
|
||||
var updateTime: String? = null,
|
||||
var nextTocUrl: String? = null
|
||||
) : Parcelable
|
||||
@@ -0,0 +1,279 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.PreferKey
|
||||
import io.legado.app.utils.*
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Suppress("MemberVisibilityCanBePrivate")
|
||||
object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
|
||||
val isGooglePlay = appCtx.channel == "google"
|
||||
val isCronet = appCtx.getPrefBoolean("Cronet")
|
||||
var userAgent: String = getPrefUserAgent()
|
||||
var isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
||||
var clickActionTL = appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
||||
var clickActionTC = appCtx.getPrefInt(PreferKey.clickActionTC, 2)
|
||||
var clickActionTR = appCtx.getPrefInt(PreferKey.clickActionTR, 1)
|
||||
var clickActionML = appCtx.getPrefInt(PreferKey.clickActionML, 2)
|
||||
var clickActionMC = appCtx.getPrefInt(PreferKey.clickActionMC, 0)
|
||||
var clickActionMR = appCtx.getPrefInt(PreferKey.clickActionMR, 1)
|
||||
var clickActionBL = appCtx.getPrefInt(PreferKey.clickActionBL, 2)
|
||||
var clickActionBC = appCtx.getPrefInt(PreferKey.clickActionBC, 1)
|
||||
var clickActionBR = appCtx.getPrefInt(PreferKey.clickActionBR, 1)
|
||||
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) {
|
||||
when (key) {
|
||||
PreferKey.themeMode -> isEInkMode = appCtx.getPrefString(PreferKey.themeMode) == "3"
|
||||
PreferKey.clickActionTL -> clickActionTL =
|
||||
appCtx.getPrefInt(PreferKey.clickActionTL, 2)
|
||||
PreferKey.clickActionTC -> clickActionTC =
|
||||
appCtx.getPrefInt(PreferKey.clickActionTC, 2)
|
||||
PreferKey.clickActionTR -> clickActionTR =
|
||||
appCtx.getPrefInt(PreferKey.clickActionTR, 2)
|
||||
PreferKey.clickActionML -> clickActionML =
|
||||
appCtx.getPrefInt(PreferKey.clickActionML, 2)
|
||||
PreferKey.clickActionMC -> clickActionMC =
|
||||
appCtx.getPrefInt(PreferKey.clickActionMC, 2)
|
||||
PreferKey.clickActionMR -> clickActionMR =
|
||||
appCtx.getPrefInt(PreferKey.clickActionMR, 2)
|
||||
PreferKey.clickActionBL -> clickActionBL =
|
||||
appCtx.getPrefInt(PreferKey.clickActionBL, 2)
|
||||
PreferKey.clickActionBC -> clickActionBC =
|
||||
appCtx.getPrefInt(PreferKey.clickActionBC, 2)
|
||||
PreferKey.clickActionBR -> clickActionBR =
|
||||
appCtx.getPrefInt(PreferKey.clickActionBR, 2)
|
||||
PreferKey.readBodyToLh -> ReadBookConfig.readBodyToLh =
|
||||
appCtx.getPrefBoolean(PreferKey.readBodyToLh, true)
|
||||
PreferKey.useZhLayout -> ReadBookConfig.useZhLayout =
|
||||
appCtx.getPrefBoolean(PreferKey.useZhLayout)
|
||||
PreferKey.userAgent -> userAgent = getPrefUserAgent()
|
||||
}
|
||||
}
|
||||
|
||||
fun isNightTheme(context: Context): Boolean {
|
||||
return when (context.getPrefString(PreferKey.themeMode, "0")) {
|
||||
"1" -> false
|
||||
"2" -> true
|
||||
"3" -> false
|
||||
else -> sysConfiguration.isNightMode
|
||||
}
|
||||
}
|
||||
|
||||
var isNightTheme: Boolean
|
||||
get() = isNightTheme(appCtx)
|
||||
set(value) {
|
||||
if (isNightTheme != value) {
|
||||
if (value) {
|
||||
appCtx.putPrefString(PreferKey.themeMode, "2")
|
||||
} else {
|
||||
appCtx.putPrefString(PreferKey.themeMode, "1")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var showUnread: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showUnread, true)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.showUnread, value)
|
||||
}
|
||||
|
||||
var readBrightness: Int
|
||||
get() = if (isNightTheme) {
|
||||
appCtx.getPrefInt(PreferKey.nightBrightness, 100)
|
||||
} else {
|
||||
appCtx.getPrefInt(PreferKey.brightness, 100)
|
||||
}
|
||||
set(value) {
|
||||
if (isNightTheme) {
|
||||
appCtx.putPrefInt(PreferKey.nightBrightness, value)
|
||||
} else {
|
||||
appCtx.putPrefInt(PreferKey.brightness, value)
|
||||
}
|
||||
}
|
||||
|
||||
val useDefaultCover: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.useDefaultCover, false)
|
||||
|
||||
val isTransparentStatusBar: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.transparentStatusBar, true)
|
||||
|
||||
val immNavigationBar: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.immNavigationBar, true)
|
||||
|
||||
val screenOrientation: String?
|
||||
get() = appCtx.getPrefString(PreferKey.screenOrientation)
|
||||
|
||||
var bookGroupStyle: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.bookGroupStyle, 0)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.bookGroupStyle, value)
|
||||
}
|
||||
|
||||
var bookExportFileName: String?
|
||||
get() = appCtx.getPrefString(PreferKey.bookExportFileName)
|
||||
set(value) {
|
||||
appCtx.putPrefString(PreferKey.bookExportFileName, value)
|
||||
}
|
||||
|
||||
var bookImportFileName: String?
|
||||
get() = appCtx.getPrefString(PreferKey.bookImportFileName)
|
||||
set(value) {
|
||||
appCtx.putPrefString(PreferKey.bookImportFileName, value)
|
||||
}
|
||||
|
||||
var backupPath: String?
|
||||
get() = appCtx.getPrefString(PreferKey.backupPath)
|
||||
set(value) {
|
||||
if (value.isNullOrEmpty()) {
|
||||
appCtx.removePref(PreferKey.backupPath)
|
||||
} else {
|
||||
appCtx.putPrefString(PreferKey.backupPath, value)
|
||||
}
|
||||
}
|
||||
|
||||
var defaultBookTreeUri: String?
|
||||
get() = appCtx.getPrefString(PreferKey.defaultBookTreeUri)
|
||||
set(value) {
|
||||
if (value.isNullOrEmpty()) {
|
||||
appCtx.removePref(PreferKey.defaultBookTreeUri)
|
||||
} else {
|
||||
appCtx.putPrefString(PreferKey.defaultBookTreeUri, value)
|
||||
}
|
||||
}
|
||||
|
||||
val showDiscovery: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showDiscovery, true)
|
||||
|
||||
val showRSS: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.showRss, true)
|
||||
|
||||
val autoRefreshBook: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
|
||||
|
||||
var threadCount: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.threadCount, value)
|
||||
}
|
||||
|
||||
var importBookPath: String?
|
||||
get() = appCtx.getPrefString("importBookPath")
|
||||
set(value) {
|
||||
if (value == null) {
|
||||
appCtx.removePref("importBookPath")
|
||||
} else {
|
||||
appCtx.putPrefString("importBookPath", value)
|
||||
}
|
||||
}
|
||||
|
||||
var ttsSpeechRate: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.ttsSpeechRate, 5)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.ttsSpeechRate, value)
|
||||
}
|
||||
|
||||
var chineseConverterType: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.chineseConverterType)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.chineseConverterType, value)
|
||||
}
|
||||
|
||||
var systemTypefaces: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.systemTypefaces)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.systemTypefaces, value)
|
||||
}
|
||||
|
||||
var elevation: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.barElevation, AppConst.sysElevation)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.barElevation, value)
|
||||
}
|
||||
|
||||
var readUrlInBrowser: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.readUrlOpenInBrowser)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.readUrlOpenInBrowser, value)
|
||||
}
|
||||
|
||||
var exportCharset: String
|
||||
get() {
|
||||
val c = appCtx.getPrefString(PreferKey.exportCharset)
|
||||
if (c.isNullOrBlank()) {
|
||||
return "UTF-8"
|
||||
}
|
||||
return c
|
||||
}
|
||||
set(value) {
|
||||
appCtx.putPrefString(PreferKey.exportCharset, value)
|
||||
}
|
||||
|
||||
var exportUseReplace: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.exportUseReplace, true)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.exportUseReplace, value)
|
||||
}
|
||||
|
||||
var exportToWebDav: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.exportToWebDav)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.exportToWebDav, value)
|
||||
}
|
||||
var exportNoChapterName: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.exportNoChapterName)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.exportNoChapterName, value)
|
||||
}
|
||||
var exportType: Int
|
||||
get() = appCtx.getPrefInt(PreferKey.exportType)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.exportType, value)
|
||||
}
|
||||
|
||||
var changeSourceCheckAuthor: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.changeSourceCheckAuthor)
|
||||
set(value) {
|
||||
appCtx.putPrefBoolean(PreferKey.changeSourceCheckAuthor, value)
|
||||
}
|
||||
|
||||
var ttsEngine: String?
|
||||
get() = appCtx.getPrefString(PreferKey.ttsEngine)
|
||||
set(value) {
|
||||
appCtx.putPrefString(PreferKey.ttsEngine, value)
|
||||
}
|
||||
|
||||
val autoChangeSource: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.autoChangeSource, true)
|
||||
|
||||
val changeSourceLoadInfo get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadInfo)
|
||||
|
||||
val changeSourceLoadToc get() = appCtx.getPrefBoolean(PreferKey.changeSourceLoadToc)
|
||||
|
||||
val importKeepName get() = appCtx.getPrefBoolean(PreferKey.importKeepName)
|
||||
|
||||
val syncBookProgress get() = appCtx.getPrefBoolean(PreferKey.syncBookProgress, true)
|
||||
|
||||
var preDownloadNum
|
||||
get() = appCtx.getPrefInt(PreferKey.preDownloadNum, 10)
|
||||
set(value) {
|
||||
appCtx.putPrefInt(PreferKey.preDownloadNum, value)
|
||||
}
|
||||
|
||||
val mediaButtonOnExit get() = appCtx.getPrefBoolean("mediaButtonOnExit", true)
|
||||
|
||||
val replaceEnableDefault get() = appCtx.getPrefBoolean(PreferKey.replaceEnableDefault, true)
|
||||
|
||||
val doublePageHorizontal: Boolean
|
||||
get() = appCtx.getPrefBoolean(PreferKey.doublePageHorizontal, true)
|
||||
|
||||
private fun getPrefUserAgent(): String {
|
||||
val ua = appCtx.getPrefString(PreferKey.userAgent)
|
||||
if (ua.isNullOrBlank()) {
|
||||
return "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36"
|
||||
}
|
||||
return ua
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.help.http.newCallStrResponse
|
||||
import io.legado.app.help.http.okHttpClient
|
||||
import io.legado.app.model.NoStackTraceException
|
||||
import io.legado.app.utils.jsonPath
|
||||
import io.legado.app.utils.readString
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import splitties.init.appCtx
|
||||
|
||||
object AppUpdate {
|
||||
|
||||
fun checkFromGitHub(
|
||||
scope: CoroutineScope,
|
||||
showErrorMsg: Boolean = true,
|
||||
callback: (newVersion: String, updateBody: String, url: String, fileName: String) -> Unit
|
||||
) {
|
||||
Coroutine.async(scope) {
|
||||
val lastReleaseUrl = "https://api.github.com/repos/gedoor/legado/releases/latest"
|
||||
val body = okHttpClient.newCallStrResponse {
|
||||
url(lastReleaseUrl)
|
||||
}.body
|
||||
if (body.isNullOrBlank()) {
|
||||
throw NoStackTraceException("获取新版本出错")
|
||||
}
|
||||
val rootDoc = jsonPath.parse(body)
|
||||
val tagName = rootDoc.readString("$.tag_name")
|
||||
?: throw NoStackTraceException("获取新版本出错")
|
||||
if (tagName > AppConst.appInfo.versionName) {
|
||||
val updateBody = rootDoc.readString("$.body")
|
||||
?: throw NoStackTraceException("获取新版本出错")
|
||||
val downloadUrl = rootDoc.readString("$.assets[0].browser_download_url")
|
||||
?: throw NoStackTraceException("获取新版本出错")
|
||||
val fileName = rootDoc.readString("$.assets[0].name")
|
||||
?: throw NoStackTraceException("获取新版本出错")
|
||||
return@async arrayOf(tagName, updateBody, downloadUrl, fileName)
|
||||
} else {
|
||||
throw NoStackTraceException("已是最新版本")
|
||||
}
|
||||
}.timeout(10000)
|
||||
.onSuccess {
|
||||
callback.invoke(it[0], it[1], it[2], it[3])
|
||||
}.onError {
|
||||
if (showErrorMsg) {
|
||||
appCtx.toastOnUi("检测更新\n${it.localizedMessage}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.annotation.TargetApi
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.os.Build
|
||||
import android.renderscript.Allocation
|
||||
import android.renderscript.Element
|
||||
import android.renderscript.RenderScript
|
||||
import android.renderscript.ScriptIntrinsicBlur
|
||||
import com.bumptech.glide.load.engine.bitmap_recycle.BitmapPool
|
||||
import com.bumptech.glide.load.resource.bitmap.CenterCrop
|
||||
import java.security.MessageDigest
|
||||
import kotlin.math.min
|
||||
import kotlin.math.roundToInt
|
||||
|
||||
|
||||
/**
|
||||
* 模糊
|
||||
* @radius: 0..25
|
||||
*/
|
||||
class BlurTransformation(context: Context, private val radius: Int) : CenterCrop() {
|
||||
private val rs: RenderScript = RenderScript.create(context)
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.JELLY_BEAN_MR1)
|
||||
override fun transform(
|
||||
pool: BitmapPool,
|
||||
toTransform: Bitmap,
|
||||
outWidth: Int,
|
||||
outHeight: Int
|
||||
): Bitmap {
|
||||
val transform = super.transform(pool, toTransform, outWidth, outHeight)
|
||||
//图片缩小1/2
|
||||
val width = (min(outWidth, transform.width) / 2f).roundToInt()
|
||||
val height = (min(outHeight, transform.height) / 2f).roundToInt()
|
||||
val blurredBitmap = Bitmap.createScaledBitmap(transform, width, height, false)
|
||||
// Allocate memory for Renderscript to work with
|
||||
//分配用于渲染脚本的内存
|
||||
val input = Allocation.createFromBitmap(
|
||||
rs,
|
||||
blurredBitmap,
|
||||
Allocation.MipmapControl.MIPMAP_FULL,
|
||||
Allocation.USAGE_SHARED
|
||||
)
|
||||
val output = Allocation.createTyped(rs, input.type)
|
||||
|
||||
// Load up an instance of the specific script that we want to use.
|
||||
//加载我们想要使用的特定脚本的实例。
|
||||
val script = ScriptIntrinsicBlur.create(rs, Element.U8_4(rs))
|
||||
script.setInput(input)
|
||||
|
||||
// Set the blur radius
|
||||
//设置模糊半径0..25
|
||||
script.setRadius(radius.toFloat())
|
||||
|
||||
// Start the ScriptIntrinsicBlur
|
||||
//启动 ScriptIntrinsicBlur,
|
||||
script.forEach(output)
|
||||
|
||||
// Copy the output to the blurred bitmap
|
||||
//将输出复制到模糊的位图
|
||||
output.copyTo(blurredBitmap)
|
||||
|
||||
return blurredBitmap
|
||||
}
|
||||
|
||||
override fun updateDiskCacheKey(messageDigest: MessageDigest) {
|
||||
messageDigest.update("blur transformation".toByteArray())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.constant.AppPattern
|
||||
import io.legado.app.constant.EventBus
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.BookSource
|
||||
import io.legado.app.help.coroutine.Coroutine
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.localBook.LocalBook
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Deferred
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.delay
|
||||
import org.apache.commons.text.similarity.JaccardSimilarity
|
||||
import splitties.init.appCtx
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import java.util.concurrent.CopyOnWriteArraySet
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.abs
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("unused")
|
||||
object BookHelp {
|
||||
val downloadDir: File = appCtx.externalFiles
|
||||
const val cacheFolderName = "book_cache"
|
||||
private const val cacheImageFolderName = "images"
|
||||
private val downloadImages = CopyOnWriteArraySet<String>()
|
||||
|
||||
fun clearCache() {
|
||||
FileUtils.deleteFile(
|
||||
FileUtils.getPath(downloadDir, cacheFolderName)
|
||||
)
|
||||
}
|
||||
|
||||
fun clearCache(book: Book) {
|
||||
val filePath = FileUtils.getPath(downloadDir, cacheFolderName, book.getFolderName())
|
||||
FileUtils.deleteFile(filePath)
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除已删除书的缓存
|
||||
*/
|
||||
fun clearRemovedCache() {
|
||||
Coroutine.async {
|
||||
val bookFolderNames = arrayListOf<String>()
|
||||
appDb.bookDao.all.forEach {
|
||||
bookFolderNames.add(it.getFolderName())
|
||||
}
|
||||
val file = downloadDir.getFile(cacheFolderName)
|
||||
file.listFiles()?.forEach { bookFile ->
|
||||
if (!bookFolderNames.contains(bookFile.name)) {
|
||||
FileUtils.deleteFile(bookFile.absolutePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveContent(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
content: String
|
||||
) {
|
||||
saveText(book, bookChapter, content)
|
||||
saveImages(scope, bookSource, book, bookChapter, content)
|
||||
postEvent(EventBus.SAVE_CONTENT, bookChapter)
|
||||
}
|
||||
|
||||
private fun saveText(
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
content: String
|
||||
) {
|
||||
if (content.isEmpty()) return
|
||||
//保存文本
|
||||
FileUtils.createFileIfNotExist(
|
||||
downloadDir,
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
bookChapter.getFileName(),
|
||||
).writeText(content)
|
||||
}
|
||||
|
||||
private suspend fun saveImages(
|
||||
scope: CoroutineScope,
|
||||
bookSource: BookSource,
|
||||
book: Book,
|
||||
bookChapter: BookChapter,
|
||||
content: String
|
||||
) {
|
||||
val awaitList = arrayListOf<Deferred<Unit>>()
|
||||
content.split("\n").forEach {
|
||||
val matcher = AppPattern.imgPattern.matcher(it)
|
||||
if (matcher.find()) {
|
||||
matcher.group(1)?.let { src ->
|
||||
val mSrc = NetworkUtils.getAbsoluteURL(bookChapter.url, src)
|
||||
awaitList.add(scope.async {
|
||||
saveImage(bookSource, book, mSrc)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
awaitList.forEach {
|
||||
it.await()
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun saveImage(bookSource: BookSource?, book: Book, src: String) {
|
||||
while (downloadImages.contains(src)) {
|
||||
delay(100)
|
||||
}
|
||||
if (getImage(book, src).exists()) {
|
||||
return
|
||||
}
|
||||
downloadImages.add(src)
|
||||
val analyzeUrl = AnalyzeUrl(src, source = bookSource)
|
||||
try {
|
||||
analyzeUrl.getByteArrayAwait().let {
|
||||
FileUtils.createFileIfNotExist(
|
||||
downloadDir,
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
cacheImageFolderName,
|
||||
"${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}"
|
||||
).writeBytes(it)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
} finally {
|
||||
downloadImages.remove(src)
|
||||
}
|
||||
}
|
||||
|
||||
fun getImage(book: Book, src: String): File {
|
||||
return downloadDir.getFile(
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
cacheImageFolderName,
|
||||
"${MD5Utils.md5Encode16(src)}${getImageSuffix(src)}"
|
||||
)
|
||||
}
|
||||
|
||||
fun getImageSuffix(src: String): String {
|
||||
var suffix = src.substringAfterLast(".").substringBefore(",")
|
||||
if (suffix.length > 5) {
|
||||
suffix = ".jpg"
|
||||
}
|
||||
return suffix
|
||||
}
|
||||
|
||||
fun getChapterFiles(book: Book): List<String> {
|
||||
val fileNameList = arrayListOf<String>()
|
||||
if (book.isLocalTxt()) {
|
||||
return fileNameList
|
||||
}
|
||||
FileUtils.createFolderIfNotExist(
|
||||
downloadDir,
|
||||
subDirs = arrayOf(cacheFolderName, book.getFolderName())
|
||||
).list()?.let {
|
||||
fileNameList.addAll(it)
|
||||
}
|
||||
return fileNameList
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测该章节是否下载
|
||||
*/
|
||||
fun hasContent(book: Book, bookChapter: BookChapter): Boolean {
|
||||
return if (book.isLocalTxt()) {
|
||||
true
|
||||
} else {
|
||||
downloadDir.exists(
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
bookChapter.getFileName()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检测图片是否下载
|
||||
*/
|
||||
fun hasImageContent(book: Book, bookChapter: BookChapter): Boolean {
|
||||
if (!hasContent(book, bookChapter)) {
|
||||
return false
|
||||
}
|
||||
getContent(book, bookChapter)?.let {
|
||||
val matcher = AppPattern.imgPattern.matcher(it)
|
||||
while (matcher.find()) {
|
||||
matcher.group(1)?.let { src ->
|
||||
val image = getImage(book, src)
|
||||
if (!image.exists()) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* 读取章节内容
|
||||
*/
|
||||
fun getContent(book: Book, bookChapter: BookChapter): String? {
|
||||
if (book.isLocalTxt() || book.isUmd()) {
|
||||
return LocalBook.getContent(book, bookChapter)
|
||||
} else if (book.isEpub() && !hasContent(book, bookChapter)) {
|
||||
val string = LocalBook.getContent(book, bookChapter)
|
||||
string?.let {
|
||||
saveText(book, bookChapter, it)
|
||||
}
|
||||
return string
|
||||
} else {
|
||||
val file = downloadDir.getFile(
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
bookChapter.getFileName()
|
||||
)
|
||||
if (file.exists()) {
|
||||
return file.readText()
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 反转章节内容
|
||||
*/
|
||||
fun reverseContent(book: Book, bookChapter: BookChapter) {
|
||||
if (!book.isLocalBook()) {
|
||||
val file = downloadDir.getFile(
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
bookChapter.getFileName()
|
||||
)
|
||||
if (file.exists()) {
|
||||
val text = file.readText()
|
||||
val stringBuilder = StringBuilder()
|
||||
text.toStringArray().forEach {
|
||||
stringBuilder.insert(0, it)
|
||||
}
|
||||
file.writeText(stringBuilder.toString())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除章节内容
|
||||
*/
|
||||
fun delContent(book: Book, bookChapter: BookChapter) {
|
||||
if (book.isLocalTxt()) {
|
||||
return
|
||||
} else {
|
||||
FileUtils.createFileIfNotExist(
|
||||
downloadDir,
|
||||
cacheFolderName,
|
||||
book.getFolderName(),
|
||||
bookChapter.getFileName()
|
||||
).delete()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化书名
|
||||
*/
|
||||
fun formatBookName(name: String): String {
|
||||
return name
|
||||
.replace(AppPattern.nameRegex, "")
|
||||
.trim { it <= ' ' }
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化作者
|
||||
*/
|
||||
fun formatBookAuthor(author: String): String {
|
||||
return author
|
||||
.replace(AppPattern.authorRegex, "")
|
||||
.trim { it <= ' ' }
|
||||
}
|
||||
|
||||
private val jaccardSimilarity by lazy {
|
||||
JaccardSimilarity()
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据目录名获取当前章节
|
||||
*/
|
||||
fun getDurChapter(
|
||||
oldDurChapterIndex: Int,
|
||||
oldChapterListSize: Int,
|
||||
oldDurChapterName: String?,
|
||||
newChapterList: List<BookChapter>
|
||||
): Int {
|
||||
if (oldChapterListSize == 0) return oldDurChapterIndex
|
||||
if (newChapterList.isEmpty()) return oldDurChapterIndex
|
||||
val oldChapterNum = getChapterNum(oldDurChapterName)
|
||||
val oldName = getPureChapterName(oldDurChapterName)
|
||||
val newChapterSize = newChapterList.size
|
||||
val min = max(
|
||||
0,
|
||||
min(
|
||||
oldDurChapterIndex,
|
||||
oldDurChapterIndex - oldChapterListSize + newChapterSize
|
||||
) - 10
|
||||
)
|
||||
val max = min(
|
||||
newChapterSize - 1,
|
||||
max(
|
||||
oldDurChapterIndex,
|
||||
oldDurChapterIndex - oldChapterListSize + newChapterSize
|
||||
) + 10
|
||||
)
|
||||
var nameSim = 0.0
|
||||
var newIndex = 0
|
||||
var newNum = 0
|
||||
if (oldName.isNotEmpty()) {
|
||||
for (i in min..max) {
|
||||
val newName = getPureChapterName(newChapterList[i].title)
|
||||
val temp = jaccardSimilarity.apply(oldName, newName)
|
||||
if (temp > nameSim) {
|
||||
nameSim = temp
|
||||
newIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
if (nameSim < 0.96 && oldChapterNum > 0) {
|
||||
for (i in min..max) {
|
||||
val temp = getChapterNum(newChapterList[i].title)
|
||||
if (temp == oldChapterNum) {
|
||||
newNum = temp
|
||||
newIndex = i
|
||||
break
|
||||
} else if (abs(temp - oldChapterNum) < abs(newNum - oldChapterNum)) {
|
||||
newNum = temp
|
||||
newIndex = i
|
||||
}
|
||||
}
|
||||
}
|
||||
return if (nameSim > 0.96 || abs(newNum - oldChapterNum) < 1) {
|
||||
newIndex
|
||||
} else {
|
||||
min(max(0, newChapterList.size - 1), oldDurChapterIndex)
|
||||
}
|
||||
}
|
||||
|
||||
private val chapterNamePattern1 by lazy {
|
||||
Pattern.compile(".*?第([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话]")
|
||||
}
|
||||
|
||||
private val chapterNamePattern2 by lazy {
|
||||
Pattern.compile("^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*([\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、]|\\.[^\\d])")
|
||||
}
|
||||
|
||||
private val regexA by lazy {
|
||||
return@lazy "\\s".toRegex()
|
||||
}
|
||||
|
||||
private fun getChapterNum(chapterName: String?): Int {
|
||||
chapterName ?: return -1
|
||||
val chapterName1 = StringUtils.fullToHalf(chapterName).replace(regexA, "")
|
||||
return StringUtils.stringToInt(
|
||||
(
|
||||
chapterNamePattern1.matcher(chapterName1).takeIf { it.find() }
|
||||
?: chapterNamePattern2.matcher(chapterName1).takeIf { it.find() }
|
||||
)?.group(1)
|
||||
?: "-1"
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("SpellCheckingInspection")
|
||||
private val regexOther by lazy {
|
||||
// 所有非字母数字中日韩文字 CJK区+扩展A-F区
|
||||
@Suppress("RegExpDuplicateCharacterInClass")
|
||||
return@lazy "[^\\w\\u4E00-\\u9FEF〇\\u3400-\\u4DBF\\u20000-\\u2A6DF\\u2A700-\\u2EBEF]".toRegex()
|
||||
}
|
||||
|
||||
private val regexB by lazy {
|
||||
//章节序号,排除处于结尾的状况,避免将章节名替换为空字串
|
||||
return@lazy "^.*?第(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)[章节篇回集话](?!$)|^(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+[,:、])*(?:[\\d零〇一二两三四五六七八九十百千万壹贰叁肆伍陆柒捌玖拾佰仟]+)(?:[,:、](?!$)|\\.(?=[^\\d]))".toRegex()
|
||||
}
|
||||
|
||||
private val regexC by lazy {
|
||||
//前后附加内容,整个章节名都在括号中时只剔除首尾括号,避免将章节名替换为空字串
|
||||
return@lazy "(?!^)(?:[〖【《〔\\[{(][^〖【《〔\\[{()〕》】〗\\]}]+)?[)〕》】〗\\]}]$|^[〖【《〔\\[{(](?:[^〖【《〔\\[{()〕》】〗\\]}]+[〕》】〗\\]})])?(?!$)".toRegex()
|
||||
}
|
||||
|
||||
private fun getPureChapterName(chapterName: String?): String {
|
||||
return if (chapterName == null) "" else StringUtils.fullToHalf(chapterName)
|
||||
.replace(regexA, "")
|
||||
.replace(regexB, "")
|
||||
.replace(regexC, "")
|
||||
.replace(regexOther, "")
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Cache
|
||||
import io.legado.app.model.analyzeRule.QueryTTF
|
||||
import io.legado.app.utils.ACache
|
||||
import splitties.init.appCtx
|
||||
|
||||
@Suppress("unused")
|
||||
object CacheManager {
|
||||
|
||||
private val queryTTFMap = hashMapOf<String, Pair<Long, QueryTTF>>()
|
||||
|
||||
/**
|
||||
* saveTime 单位为秒
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun put(key: String, value: Any, saveTime: Int = 0) {
|
||||
val deadline =
|
||||
if (saveTime == 0) 0 else System.currentTimeMillis() + saveTime * 1000
|
||||
when (value) {
|
||||
is QueryTTF -> queryTTFMap[key] = Pair(deadline, value)
|
||||
is ByteArray -> ACache.get(appCtx).put(key, value, saveTime)
|
||||
else -> {
|
||||
val cache = Cache(key, value.toString(), deadline)
|
||||
appDb.cacheDao.insert(cache)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun get(key: String): String? {
|
||||
return appDb.cacheDao.get(key, System.currentTimeMillis())
|
||||
}
|
||||
|
||||
fun getInt(key: String): Int? {
|
||||
return get(key)?.toIntOrNull()
|
||||
}
|
||||
|
||||
fun getLong(key: String): Long? {
|
||||
return get(key)?.toLongOrNull()
|
||||
}
|
||||
|
||||
fun getDouble(key: String): Double? {
|
||||
return get(key)?.toDoubleOrNull()
|
||||
}
|
||||
|
||||
fun getFloat(key: String): Float? {
|
||||
return get(key)?.toFloatOrNull()
|
||||
}
|
||||
|
||||
fun getByteArray(key: String): ByteArray? {
|
||||
return ACache.get(appCtx).getAsBinary(key)
|
||||
}
|
||||
|
||||
fun getQueryTTF(key: String): QueryTTF? {
|
||||
val cache = queryTTFMap[key] ?: return null
|
||||
if (cache.first == 0L || cache.first > System.currentTimeMillis()) {
|
||||
return cache.second
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun delete(key: String) {
|
||||
appDb.cacheDao.delete(key)
|
||||
ACache.get(appCtx).remove(key)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,621 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import java.util.*
|
||||
import java.util.regex.Pattern
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
@Suppress("SameParameterValue", "RegExpRedundantEscape")
|
||||
object ContentHelp {
|
||||
|
||||
/**
|
||||
* 段落重排算法入口。把整篇内容输入,连接错误的分段,再把每个段落调用其他方法重新切分
|
||||
*
|
||||
* @param content 正文
|
||||
* @param chapterName 标题
|
||||
* @return
|
||||
*/
|
||||
fun reSegment(content: String, chapterName: String): String {
|
||||
var content1 = content
|
||||
val dict = makeDict(content1)
|
||||
var p = content1
|
||||
.replace(""".toRegex(), "“")
|
||||
.replace("[::]['\"‘”“]+".toRegex(), ":“")
|
||||
.replace("[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
|
||||
.split("\n(\\s*)".toRegex()).toTypedArray()
|
||||
|
||||
//初始化StringBuffer的长度,在原content的长度基础上做冗余
|
||||
var buffer = StringBuffer((content1.length * 1.15).toInt())
|
||||
// 章节的文本格式为章节标题-空行-首段,所以处理段落时需要略过第一行文本。
|
||||
buffer.append(" ")
|
||||
if (chapterName.trim { it <= ' ' } != p[0].trim { it <= ' ' }) {
|
||||
// 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
|
||||
buffer.append(p[0].replace("[\u3000\\s]+".toRegex(), ""))
|
||||
}
|
||||
|
||||
//如果原文存在分段错误,需要把段落重新黏合
|
||||
for (i in 1 until p.size) {
|
||||
if (match(MARK_SENTENCES_END, buffer[buffer.length - 1])) buffer.append("\n")
|
||||
// 段落开头以外的地方不应该有空格
|
||||
// 去除段落内空格。unicode 3000 象形字间隔(中日韩符号和标点),不包含在\s内
|
||||
buffer.append(p[i].replace("[\u3000\\s]".toRegex(), ""))
|
||||
}
|
||||
// 预分段预处理
|
||||
// ”“处理为”\n“。
|
||||
// ”。“处理为”。\n“。不考虑“?” “!”的情况。
|
||||
// ”。xxx处理为 ”。\n xxx
|
||||
p = buffer.toString()
|
||||
.replace("[\"”“]+[\\s]*[\"”“]+".toRegex(), "”\n“")
|
||||
.replace("[\"”“]+(?。!?!~)[\"”“]+".toRegex(), "”$1\n“")
|
||||
.replace("[\"”“]+(?。!?!~)([^\"”“])".toRegex(), "”$1\n$2")
|
||||
.replace(
|
||||
"([问说喊唱叫骂道着答])[\\.。]".toRegex(),
|
||||
"$1。\n"
|
||||
)
|
||||
.split("\n".toRegex()).toTypedArray()
|
||||
buffer = StringBuffer((content1.length * 1.15).toInt())
|
||||
for (s in p) {
|
||||
buffer.append("\n")
|
||||
buffer.append(findNewLines(s, dict))
|
||||
}
|
||||
buffer = reduceLength(buffer)
|
||||
content1 = (buffer.toString() // 处理章节头部空格和换行
|
||||
.replaceFirst("^\\s+".toRegex(), "")
|
||||
.replace("\\s*[\"”“]+[\\s]*[\"”“][\\s\"”“]*".toRegex(), "”\n“")
|
||||
.replace("[::][”“\"\\s]+".toRegex(), ":“")
|
||||
.replace("\n[\"“”]([^\n\"“”]+)([,:,:][\"”“])([^\n\"“”]+)".toRegex(), "\n$1:“$3")
|
||||
.replace("\n(\\s*)".toRegex(), "\n"))
|
||||
return content1
|
||||
}
|
||||
|
||||
/**
|
||||
* 强制切分,减少段落内的句子
|
||||
* 如果连续2对引号的段落没有提示语,进入对话模式。最后一对引号后强制切分段落
|
||||
* 如果引号内的内容长于5句,可能引号状态有误,随机分段
|
||||
* 如果引号外的内容长于3句,随机分段
|
||||
*
|
||||
* @param str
|
||||
* @return
|
||||
*/
|
||||
private fun reduceLength(str: StringBuffer): StringBuffer {
|
||||
val p = str.toString().split("\n".toRegex()).toTypedArray()
|
||||
val l = p.size
|
||||
val b = BooleanArray(l)
|
||||
for (i in 0 until l) {
|
||||
b[i] = p[i].matches(PARAGRAPH_DIAGLOG)
|
||||
}
|
||||
var dialogue = 0
|
||||
for (i in 0 until l) {
|
||||
if (b[i]) {
|
||||
if (dialogue < 0) dialogue = 1 else if (dialogue < 2) dialogue++
|
||||
} else {
|
||||
if (dialogue > 1) {
|
||||
p[i] = splitQuote(p[i])
|
||||
dialogue--
|
||||
} else if (dialogue > 0 && i < l - 2) {
|
||||
if (b[i + 1]) p[i] = splitQuote(p[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
val string = StringBuffer()
|
||||
for (i in 0 until l) {
|
||||
string.append('\n')
|
||||
string.append(p[i])
|
||||
//System.out.print(" "+b[i]);
|
||||
}
|
||||
//System.out.println(" " + str);
|
||||
return string
|
||||
}
|
||||
|
||||
// 强制切分进入对话模式后,未构成 “xxx” 形式的段落
|
||||
private fun splitQuote(str: String): String {
|
||||
val length = str.length
|
||||
if (length < 3) return str
|
||||
if (match(MARK_QUOTATION, str[0])) {
|
||||
val i = seekIndex(str, MARK_QUOTATION, 1, length - 2, true) + 1
|
||||
if (i > 1) if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
|
||||
return "${str.substring(0, i)}\n${str.substring(i)}"
|
||||
}
|
||||
} else if (match(MARK_QUOTATION, str[length - 1])) {
|
||||
val i = length - 1 - seekIndex(str, MARK_QUOTATION, 1, length - 2, false)
|
||||
if (i > 1) {
|
||||
if (!match(MARK_QUOTATION_BEFORE, str[i - 1])) {
|
||||
return "${str.substring(0, i)}\n${str.substring(i)}"
|
||||
}
|
||||
}
|
||||
}
|
||||
return str
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算随机插入换行符的位置。
|
||||
* @param str 字符串
|
||||
* @param offset 传回的结果需要叠加的偏移量
|
||||
* @param min 最低几个句子,随机插入换行
|
||||
* @param gain 倍率。每个句子插入换行的数学期望 = 1 / gain , gain越大越不容易插入换行
|
||||
* @return
|
||||
*/
|
||||
private fun forceSplit(
|
||||
str: String,
|
||||
offset: Int,
|
||||
min: Int,
|
||||
gain: Int,
|
||||
tigger: Int
|
||||
): ArrayList<Int> {
|
||||
val result = ArrayList<Int>()
|
||||
val arrayEnd = seekIndexs(str, MARK_SENTENCES_END_P, 0, str.length - 2, true)
|
||||
val arrayMid = seekIndexs(str, MARK_SENTENCES_MID, 0, str.length - 2, true)
|
||||
if (arrayEnd.size < tigger && arrayMid.size < tigger * 3) return result
|
||||
var j = 0
|
||||
var i = min
|
||||
while (i < arrayEnd.size) {
|
||||
var k = 0
|
||||
while (j < arrayMid.size) {
|
||||
if (arrayMid[j] < arrayEnd[i]) k++
|
||||
j++
|
||||
}
|
||||
if (Math.random() * gain < 0.8 + k / 2.5) {
|
||||
result.add(arrayEnd[i] + offset)
|
||||
i = max(i + min, i)
|
||||
}
|
||||
i++
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
// 对内容重新划分段落.输入参数str已经使用换行符预分割
|
||||
private fun findNewLines(str: String, dict: List<String>): String {
|
||||
val string = StringBuffer(str)
|
||||
// 标记string中每个引号的位置.特别的,用引号进行列举时视为只有一对引号。 如:“锅”、“碗”视为“锅、碗”,从而避免误断句。
|
||||
val arrayQuote: MutableList<Int> = ArrayList()
|
||||
// 标记插入换行符的位置,int为插入位置(str的char下标)
|
||||
var insN = ArrayList<Int>()
|
||||
|
||||
//mod[i]标记str的每一段处于引号内还是引号外。范围: str.substring( array_quote.get(i), array_quote.get(i+1) )的状态。
|
||||
//长度:array_quote.size(),但是初始化时未预估占用的长度,用空间换时间
|
||||
//0未知,正数引号内,负数引号外。
|
||||
//如果相邻的两个标记都为+1,那么需要增加1个引号。
|
||||
//引号内不进行断句
|
||||
val mod = IntArray(str.length)
|
||||
var waitClose = false
|
||||
for (i in str.indices) {
|
||||
val c = str[i]
|
||||
if (match(MARK_QUOTATION, c)) {
|
||||
val size = arrayQuote.size
|
||||
|
||||
// 把“xxx”、“yy”合并为“xxx_yy”进行处理
|
||||
if (size > 0) {
|
||||
val quotePre = arrayQuote[size - 1]
|
||||
if (i - quotePre == 2) {
|
||||
var remove = false
|
||||
if (waitClose) {
|
||||
if (match(",,、/", str[i - 1])) {
|
||||
// 考虑出现“和”这种特殊情况
|
||||
remove = true
|
||||
}
|
||||
} else if (match(",,、/和与或", str[i - 1])) {
|
||||
remove = true
|
||||
}
|
||||
if (remove) {
|
||||
string.setCharAt(i, '“')
|
||||
string.setCharAt(i - 2, '”')
|
||||
arrayQuote.removeAt(size - 1)
|
||||
mod[size - 1] = 1
|
||||
mod[size] = -1
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
arrayQuote.add(i)
|
||||
|
||||
// 为xxx:“xxx”做标记
|
||||
if (i > 1) {
|
||||
// 当前发言的正引号的前一个字符
|
||||
val charB1 = str[i - 1]
|
||||
// 上次发言的正引号的前一个字符
|
||||
var charB2 = 0.toChar()
|
||||
if (match(MARK_QUOTATION_BEFORE, charB1)) {
|
||||
// 如果不是第一处引号,寻找上一处断句,进行分段
|
||||
if (arrayQuote.size > 1) {
|
||||
val lastQuote = arrayQuote[arrayQuote.size - 2]
|
||||
var p = 0
|
||||
if (charB1 == ',' || charB1 == ',') {
|
||||
if (arrayQuote.size > 2) {
|
||||
p = arrayQuote[arrayQuote.size - 3]
|
||||
if (p > 0) {
|
||||
charB2 = str[p - 1]
|
||||
}
|
||||
}
|
||||
}
|
||||
//if(char_b2=='.' || char_b2=='。')
|
||||
if (match(MARK_SENTENCES_END_P, charB2)) {
|
||||
insN.add(p - 1)
|
||||
} else if (!match("的", charB2)) {
|
||||
val lastEnd = seekLast(str, MARK_SENTENCES_END, i, lastQuote)
|
||||
if (lastEnd > 0) insN.add(lastEnd) else insN.add(lastQuote)
|
||||
}
|
||||
}
|
||||
waitClose = true
|
||||
mod[size] = 1
|
||||
if (size > 0) {
|
||||
mod[size - 1] = -1
|
||||
if (size > 1) {
|
||||
mod[size - 2] = 1
|
||||
}
|
||||
}
|
||||
} else if (waitClose) {
|
||||
run {
|
||||
waitClose = false
|
||||
insN.add(i)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
val size = arrayQuote.size
|
||||
|
||||
|
||||
//标记循环状态,此位置前的引号是否已经配对
|
||||
var opend = false
|
||||
if (size > 0) {
|
||||
//第1次遍历array_quote,令其元素的值不为0
|
||||
for (i in 0 until size) {
|
||||
if (mod[i] > 0) {
|
||||
opend = true
|
||||
} else if (mod[i] < 0) {
|
||||
//连续2个反引号表明存在冲突,强制把前一个设为正引号
|
||||
if (!opend) {
|
||||
if (i > 0) mod[i] = 3
|
||||
}
|
||||
opend = false
|
||||
} else {
|
||||
opend = !opend
|
||||
if (opend) mod[i] = 2 else mod[i] = -2
|
||||
}
|
||||
}
|
||||
// 修正,断尾必须封闭引号
|
||||
if (opend) {
|
||||
if (arrayQuote[size - 1] - string.length > -3) {
|
||||
//if((match(MARK_QUOTATION,string.charAt(string.length()-1)) || match(MARK_QUOTATION,string.charAt(string.length()-2)))){
|
||||
if (size > 1) mod[size - 2] = 4
|
||||
// 0<=i<size,故无需判断size>=1
|
||||
mod[size - 1] = -4
|
||||
} else if (!match(MARK_SENTENCES_SAY, string[string.length - 2])) string.append(
|
||||
"”"
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
//第2次循环,mod[i]由负变正时,前1字符如果是句末,需要插入换行
|
||||
var loop2Mod1 = -1 //上一个引号跟随内容的状态
|
||||
var loop2Mod2: Int //当前引号跟随内容的状态
|
||||
var i = 0
|
||||
var j = arrayQuote[0] - 1 //当前引号前一字符的序号
|
||||
if (j < 0) {
|
||||
i = 1
|
||||
loop2Mod1 = 0
|
||||
}
|
||||
while (i < size) {
|
||||
j = arrayQuote[i] - 1
|
||||
loop2Mod2 = mod[i]
|
||||
if (loop2Mod1 < 0 && loop2Mod2 > 0) {
|
||||
if (match(MARK_SENTENCES_END, string[j])) insN.add(j)
|
||||
}
|
||||
loop2Mod1 = loop2Mod2
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
//第3次循环,匹配并插入换行。
|
||||
//"xxxx" xxxx。\n xxx“xxxx”
|
||||
//未实现
|
||||
|
||||
// 使用字典验证ins_n , 避免插入不必要的换行。
|
||||
// 由于目前没有插入、的列表,无法解决 “xx”、“xx”“xx” 被插入换行的问题
|
||||
val insN1 = ArrayList<Int>()
|
||||
for (i in insN) {
|
||||
if (match("\"'”“", string[i])) {
|
||||
val start: Int = seekLast(
|
||||
str,
|
||||
"\"'”“",
|
||||
i - 1,
|
||||
i - WORD_MAX_LENGTH
|
||||
)
|
||||
if (start > 0) {
|
||||
val word = str.substring(start + 1, i)
|
||||
if (dict.contains(word)) {
|
||||
//System.out.println("使用字典验证 跳过\tins_n=" + i + " word=" + word);
|
||||
//引号内如果是字典词条,后方不插入换行符(前方不需要优化)
|
||||
continue
|
||||
} else {
|
||||
//System.out.println("使用字典验证 插入\tins_n=" + i + " word=" + word);
|
||||
if (match("的地得", str[start])) {
|
||||
//xx的“xx”,后方不插入换行符(前方不需要优化)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
insN1.add(i)
|
||||
}
|
||||
insN = insN1
|
||||
|
||||
// 随机在句末插入换行符
|
||||
insN = ArrayList(HashSet(insN))
|
||||
insN.sort()
|
||||
run {
|
||||
var subs: String
|
||||
var j = 0
|
||||
var progress = 0
|
||||
var nextLine = -1
|
||||
if (insN.size > 0) nextLine = insN[j]
|
||||
var gain = 3
|
||||
var min = 0
|
||||
var trigger = 2
|
||||
for (i in arrayQuote.indices) {
|
||||
val qutoe = arrayQuote[i]
|
||||
if (qutoe > 0) {
|
||||
gain = 4
|
||||
min = 2
|
||||
trigger = 4
|
||||
} else {
|
||||
gain = 3
|
||||
min = 0
|
||||
trigger = 2
|
||||
}
|
||||
|
||||
// 把引号前的换行符与内容相间插入
|
||||
while (j < insN.size) {
|
||||
|
||||
// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
|
||||
if (nextLine >= qutoe) break
|
||||
nextLine = insN[j]
|
||||
if (progress < nextLine) {
|
||||
subs = string.substring(progress, nextLine)
|
||||
insN.addAll(forceSplit(subs, progress, min, gain, trigger))
|
||||
progress = nextLine + 1
|
||||
}
|
||||
j++
|
||||
}
|
||||
if (progress < qutoe) {
|
||||
subs = string.substring(progress, qutoe + 1)
|
||||
insN.addAll(forceSplit(subs, progress, min, gain, trigger))
|
||||
progress = qutoe + 1
|
||||
}
|
||||
}
|
||||
while (j < insN.size) {
|
||||
nextLine = insN[j]
|
||||
if (progress < nextLine) {
|
||||
subs = string.substring(progress, nextLine)
|
||||
insN.addAll(forceSplit(subs, progress, min, gain, trigger))
|
||||
progress = nextLine + 1
|
||||
}
|
||||
j++
|
||||
}
|
||||
if (progress < string.length) {
|
||||
subs = string.substring(progress, string.length)
|
||||
insN.addAll(forceSplit(subs, progress, min, gain, trigger))
|
||||
}
|
||||
}
|
||||
|
||||
// 根据段落状态修正引号方向、计算需要插入引号的位置
|
||||
// ins_quote跟随array_quote ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
|
||||
val insQuote = BooleanArray(size)
|
||||
opend = false
|
||||
for (i in 0 until size) {
|
||||
val p = arrayQuote[i]
|
||||
if (mod[i] > 0) {
|
||||
string.setCharAt(p, '“')
|
||||
if (opend) insQuote[i] = true
|
||||
opend = true
|
||||
} else if (mod[i] < 0) {
|
||||
string.setCharAt(p, '”')
|
||||
opend = false
|
||||
} else {
|
||||
opend = !opend
|
||||
if (opend) string.setCharAt(p, '“') else string.setCharAt(p, '”')
|
||||
}
|
||||
}
|
||||
insN = ArrayList(HashSet(insN))
|
||||
insN.sort()
|
||||
|
||||
// 完成字符串拼接(从string复制、插入引号和换行
|
||||
// ins_quote 在引号前插入一个引号。 ins_quote[i]!=0,则array_quote.get(i)的引号前需要前插入'”'
|
||||
// ins_n 插入换行。数组的值表示插入换行符的位置
|
||||
val buffer = StringBuffer((str.length * 1.15).toInt())
|
||||
var j = 0
|
||||
var progress = 0
|
||||
var nextLine = -1
|
||||
if (insN.size > 0) nextLine = insN[j]
|
||||
for (i in arrayQuote.indices) {
|
||||
val qutoe = arrayQuote[i]
|
||||
|
||||
// 把引号前的换行符与内容相间插入
|
||||
while (j < insN.size) {
|
||||
|
||||
// 如果下一个换行符在当前引号前,那么需要此次处理.如果紧挨当前引号,需要考虑插入引号的情况
|
||||
if (nextLine >= qutoe) break
|
||||
nextLine = insN[j]
|
||||
buffer.append(string, progress, nextLine + 1)
|
||||
buffer.append('\n')
|
||||
progress = nextLine + 1
|
||||
j++
|
||||
}
|
||||
if (progress < qutoe) {
|
||||
buffer.append(string, progress, qutoe + 1)
|
||||
progress = qutoe + 1
|
||||
}
|
||||
if (insQuote[i] && buffer.length > 2) {
|
||||
if (buffer[buffer.length - 1] == '\n') buffer.append('“') else buffer.insert(
|
||||
buffer.length - 1,
|
||||
"”\n"
|
||||
)
|
||||
}
|
||||
}
|
||||
while (j < insN.size) {
|
||||
nextLine = insN[j]
|
||||
if (progress <= nextLine) {
|
||||
buffer.append(string, progress, nextLine + 1)
|
||||
buffer.append('\n')
|
||||
progress = nextLine + 1
|
||||
}
|
||||
j++
|
||||
}
|
||||
if (progress < string.length) {
|
||||
buffer.append(string, progress, string.length)
|
||||
}
|
||||
return buffer.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 从字符串提取引号包围,且不止出现一次的内容为字典
|
||||
*
|
||||
* @param str
|
||||
* @return 词条列表
|
||||
*/
|
||||
private fun makeDict(str: String): List<String> {
|
||||
|
||||
// 引号中间不包含任何标点
|
||||
val patten = Pattern.compile(
|
||||
"""
|
||||
(?<=["'”“])([^
|
||||
\p{P}]{1,${WORD_MAX_LENGTH}})(?=["'”“])
|
||||
""".trimIndent()
|
||||
)
|
||||
//Pattern patten = Pattern.compile("(?<=[\"'”“])([^\n\"'”“]{1,16})(?=[\"'”“])");
|
||||
val matcher = patten.matcher(str)
|
||||
val cache: MutableList<String> = ArrayList()
|
||||
val dict: MutableList<String> = ArrayList()
|
||||
while (matcher.find()) {
|
||||
val word = matcher.group()
|
||||
if (cache.contains(word)) {
|
||||
if (!dict.contains(word)) dict.add(word)
|
||||
} else cache.add(word)
|
||||
}
|
||||
return dict
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算匹配到字典的每个字符的位置
|
||||
*
|
||||
* @param str 待匹配的字符串
|
||||
* @param key 字典
|
||||
* @param from 从字符串的第几个字符开始匹配
|
||||
* @param to 匹配到第几个字符结束
|
||||
* @param inOrder 是否按照从前向后的顺序匹配
|
||||
* @return 返回距离构成的ArrayList<Integer>
|
||||
</Integer> */
|
||||
private fun seekIndexs(
|
||||
str: String,
|
||||
key: String,
|
||||
from: Int,
|
||||
to: Int,
|
||||
inOrder: Boolean
|
||||
): ArrayList<Int> {
|
||||
val list = ArrayList<Int>()
|
||||
if (str.length - from < 1) return list
|
||||
var i = 0
|
||||
if (from > i) i = from
|
||||
var t = str.length
|
||||
if (to > 0) t = min(t, to)
|
||||
var c: Char
|
||||
while (i < t) {
|
||||
c = if (inOrder) str[i] else str[str.length - i - 1]
|
||||
if (key.indexOf(c) != -1) {
|
||||
list.add(i)
|
||||
}
|
||||
i++
|
||||
}
|
||||
return list
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串最后出现与字典中字符匹配的位置
|
||||
*
|
||||
* @param str 数据字符串
|
||||
* @param key 字典字符串
|
||||
* @param from 从哪个字符开始匹配,默认最末位
|
||||
* @param to 匹配到哪个字符(不包含此字符)默认0
|
||||
* @return 位置(正向计算)
|
||||
*/
|
||||
private fun seekLast(str: String, key: String, from: Int, to: Int): Int {
|
||||
if (str.length - from < 1) return -1
|
||||
var i = str.lastIndex
|
||||
if (from < i && i > 0) i = from
|
||||
var t = 0
|
||||
if (to > 0) t = to
|
||||
var c: Char
|
||||
while (i > t) {
|
||||
c = str[i]
|
||||
if (key.indexOf(c) != -1) {
|
||||
return i
|
||||
}
|
||||
i--
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算字符串与字典中字符的最短距离
|
||||
*
|
||||
* @param str 数据字符串
|
||||
* @param key 字典字符串
|
||||
* @param from 从哪个字符开始匹配,默认0
|
||||
* @param to 匹配到哪个字符(不包含此字符)默认匹配到最末位
|
||||
* @param inOrder 是否从正向开始匹配
|
||||
* @return 返回最短距离, 注意不是str的char的下标
|
||||
*/
|
||||
private fun seekIndex(str: String, key: String, from: Int, to: Int, inOrder: Boolean): Int {
|
||||
if (str.length - from < 1) return -1
|
||||
var i = 0
|
||||
if (from > i) i = from
|
||||
var t = str.length
|
||||
if (to > 0) t = min(t, to)
|
||||
var c: Char
|
||||
while (i < t) {
|
||||
c = if (inOrder) str[i] else str[str.length - i - 1]
|
||||
if (key.indexOf(c) != -1) {
|
||||
return i
|
||||
}
|
||||
i++
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
/* 搜寻引号并进行分段。处理了一、二、五三类常见情况
|
||||
参照百科词条[引号#应用示例](https://baike.baidu.com/item/%E5%BC%95%E5%8F%B7/998963?#5)对引号内容进行矫正并分句。
|
||||
一、完整引用说话内容,在反引号内侧有断句标点。例如:
|
||||
1) 丫姑折断几枝扔下来,边叫我的小名儿边说:“先喂饱你!”
|
||||
2)“哎呀,真是美极了!”皇帝说,“我十分满意!”
|
||||
3)“怕什么!海的美就在这里!”我说道。
|
||||
二、部分引用,在反引号外侧有断句标点:
|
||||
4)适当地改善自己的生活,岂但“你管得着吗”,而且是顺乎天理,合乎人情的。
|
||||
5)现代画家徐悲鸿笔下的马,正如有的评论家所说的那样,“形神兼备,充满生机”。
|
||||
6)唐朝的张嘉贞说它“制造奇特,人不知其所为”。
|
||||
三、一段接着一段地直接引用时,中间段落只在段首用起引号,该段段尾却不用引回号。但是正统文学不在考虑范围内。
|
||||
四、引号里面又要用引号时,外面一层用双引号,里面一层用单引号。暂时不需要考虑
|
||||
五、反语和强调,周围没有断句符号。
|
||||
*/
|
||||
|
||||
// 句子结尾的标点。因为引号可能存在误判,不包含引号。
|
||||
private const val MARK_SENTENCES_END = "?。!?!~"
|
||||
private const val MARK_SENTENCES_END_P = ".?。!?!~"
|
||||
|
||||
// 句中标点,由于某些网站常把“,”写为".",故英文句点按照句中标点判断
|
||||
private const val MARK_SENTENCES_MID = ".,、,—…"
|
||||
private const val MARK_SENTENCES_SAY = "问说喊唱叫骂道着答"
|
||||
|
||||
// XXX说:“”的冒号
|
||||
private const val MARK_QUOTATION_BEFORE = ",:,:"
|
||||
|
||||
// 引号
|
||||
private const val MARK_QUOTATION = "\"“”"
|
||||
private val PARAGRAPH_DIAGLOG = "^[\"”“][^\"”“]+[\"”“]$".toRegex()
|
||||
|
||||
// 限制字典的长度
|
||||
private const val WORD_MAX_LENGTH = 16
|
||||
|
||||
private fun match(rule: String, chr: Char): Boolean {
|
||||
return rule.indexOf(chr) != -1
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import com.github.liuyueyi.quick.transfer.ChineseUtils
|
||||
import io.legado.app.constant.AppLog
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.Book
|
||||
import io.legado.app.data.entities.BookChapter
|
||||
import io.legado.app.data.entities.ReplaceRule
|
||||
import io.legado.app.utils.toastOnUi
|
||||
import splitties.init.appCtx
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.regex.Pattern
|
||||
|
||||
class ContentProcessor private constructor(
|
||||
private val bookName: String,
|
||||
private val bookOrigin: String
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private val processors = hashMapOf<String, WeakReference<ContentProcessor>>()
|
||||
|
||||
fun get(bookName: String, bookOrigin: String): ContentProcessor {
|
||||
val processorWr = processors[bookName + bookOrigin]
|
||||
var processor: ContentProcessor? = processorWr?.get()
|
||||
if (processor == null) {
|
||||
processor = ContentProcessor(bookName, bookOrigin)
|
||||
processors[bookName + bookOrigin] = WeakReference(processor)
|
||||
}
|
||||
return processor
|
||||
}
|
||||
|
||||
fun upReplaceRules() {
|
||||
processors.forEach {
|
||||
it.value.get()?.upReplaceRules()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private val replaceRules = arrayListOf<ReplaceRule>()
|
||||
|
||||
init {
|
||||
upReplaceRules()
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun upReplaceRules() {
|
||||
replaceRules.clear()
|
||||
replaceRules.addAll(appDb.replaceRuleDao.findEnabledByScope(bookName, bookOrigin))
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun getReplaceRules(): Array<ReplaceRule> {
|
||||
return replaceRules.toTypedArray()
|
||||
}
|
||||
|
||||
fun getContent(
|
||||
book: Book,
|
||||
chapter: BookChapter, //已经经过简繁转换
|
||||
content: String,
|
||||
includeTitle: Boolean = true,
|
||||
useReplace: Boolean = true,
|
||||
chineseConvert: Boolean = true,
|
||||
reSegment: Boolean = true
|
||||
): List<String> {
|
||||
var mContent = content
|
||||
//去除重复标题
|
||||
try {
|
||||
val name = Pattern.quote(book.name)
|
||||
val title = Pattern.quote(chapter.title)
|
||||
val titleRegex = "^(\\s|\\p{P}|${name})*${title}(\\s|\\p{P})+".toRegex()
|
||||
mContent = mContent.replace(titleRegex, "")
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("去除重复标题出错\n${e.localizedMessage}", e)
|
||||
}
|
||||
if (reSegment && book.getReSegment()) {
|
||||
//重新分段
|
||||
mContent = ContentHelp.reSegment(mContent, chapter.title)
|
||||
}
|
||||
if (includeTitle) {
|
||||
//重新添加标题
|
||||
mContent = chapter.getDisplayTitle() + "\n" + mContent
|
||||
}
|
||||
if (useReplace && book.getUseReplaceRule()) {
|
||||
//替换
|
||||
getReplaceRules().forEach { item ->
|
||||
if (item.pattern.isNotEmpty()) {
|
||||
try {
|
||||
mContent = if (item.isRegex) {
|
||||
mContent.replace(item.pattern.toRegex(), item.replacement)
|
||||
} else {
|
||||
mContent.replace(item.pattern, item.replacement)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
AppLog.put("${item.name}替换出错\n${e.localizedMessage}")
|
||||
appCtx.toastOnUi("${item.name}替换出错")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (chineseConvert) {
|
||||
//简繁转换
|
||||
try {
|
||||
when (AppConfig.chineseConverterType) {
|
||||
1 -> mContent = ChineseUtils.t2s(mContent)
|
||||
2 -> mContent = ChineseUtils.s2t(mContent)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
appCtx.toastOnUi("简繁转换出错")
|
||||
}
|
||||
}
|
||||
val contents = arrayListOf<String>()
|
||||
mContent.split("\n").forEach { str ->
|
||||
val paragraph = str.trim {
|
||||
it.code <= 0x20 || it == ' '
|
||||
}
|
||||
if (paragraph.isNotEmpty()) {
|
||||
if (contents.isEmpty() && includeTitle) {
|
||||
contents.add(paragraph)
|
||||
} else {
|
||||
contents.add("${ReadBookConfig.paragraphIndent}$paragraph")
|
||||
}
|
||||
}
|
||||
}
|
||||
return contents
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.model.ReadAloud
|
||||
import io.legado.app.utils.FileUtils
|
||||
import io.legado.app.utils.getFile
|
||||
import io.legado.app.utils.longToastOnUi
|
||||
import io.legado.app.utils.msg
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
/**
|
||||
* 异常管理类
|
||||
*/
|
||||
@Suppress("DEPRECATION")
|
||||
class CrashHandler(val context: Context) : Thread.UncaughtExceptionHandler {
|
||||
|
||||
/**
|
||||
* 系统默认UncaughtExceptionHandler
|
||||
*/
|
||||
private var mDefaultHandler = Thread.getDefaultUncaughtExceptionHandler()
|
||||
|
||||
/**
|
||||
* 存储异常和参数信息
|
||||
*/
|
||||
private val paramsMap = HashMap<String, String>()
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private val format = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss")
|
||||
|
||||
init {
|
||||
//设置该CrashHandler为系统默认的
|
||||
Thread.setDefaultUncaughtExceptionHandler(this)
|
||||
}
|
||||
|
||||
/**
|
||||
* uncaughtException 回调函数
|
||||
*/
|
||||
override fun uncaughtException(thread: Thread, ex: Throwable) {
|
||||
ReadAloud.stop(context)
|
||||
handleException(ex)
|
||||
mDefaultHandler?.uncaughtException(thread, ex)
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理该异常
|
||||
*/
|
||||
private fun handleException(ex: Throwable?) {
|
||||
if (ex == null) return
|
||||
//收集设备参数信息
|
||||
collectDeviceInfo()
|
||||
//保存日志文件
|
||||
saveCrashInfo2File(ex)
|
||||
context.longToastOnUi(ex.msg)
|
||||
Thread.sleep(3000)
|
||||
}
|
||||
|
||||
/**
|
||||
* 收集设备参数信息
|
||||
*/
|
||||
private fun collectDeviceInfo() {
|
||||
kotlin.runCatching {
|
||||
//获取系统信息
|
||||
paramsMap["MANUFACTURER"] = Build.MANUFACTURER
|
||||
paramsMap["BRAND"] = Build.BRAND
|
||||
//获取app版本信息
|
||||
AppConst.appInfo.let {
|
||||
paramsMap["versionName"] = it.versionName
|
||||
paramsMap["versionCode"] = it.versionCode.toString()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存错误信息到文件中
|
||||
*/
|
||||
private fun saveCrashInfo2File(ex: Throwable) {
|
||||
val sb = StringBuilder()
|
||||
for ((key, value) in paramsMap) {
|
||||
sb.append(key).append("=").append(value).append("\n")
|
||||
}
|
||||
|
||||
val writer = StringWriter()
|
||||
val printWriter = PrintWriter(writer)
|
||||
ex.printStackTrace(printWriter)
|
||||
var cause: Throwable? = ex.cause
|
||||
while (cause != null) {
|
||||
cause.printStackTrace(printWriter)
|
||||
cause = cause.cause
|
||||
}
|
||||
printWriter.close()
|
||||
val result = writer.toString()
|
||||
sb.append(result)
|
||||
val timestamp = System.currentTimeMillis()
|
||||
val time = format.format(Date())
|
||||
val fileName = "crash-$time-$timestamp.log"
|
||||
context.externalCacheDir?.let { rootFile ->
|
||||
rootFile.getFile("crash").listFiles()?.forEach {
|
||||
if (it.lastModified() < System.currentTimeMillis() - TimeUnit.DAYS.toMillis(7)) {
|
||||
it.delete()
|
||||
}
|
||||
}
|
||||
FileUtils.createFileIfNotExist(rootFile, "crash", fileName)
|
||||
.writeText(sb.toString())
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.data.appDb
|
||||
import io.legado.app.data.entities.HttpTTS
|
||||
import io.legado.app.data.entities.RssSource
|
||||
import io.legado.app.data.entities.TxtTocRule
|
||||
import io.legado.app.utils.GSON
|
||||
import io.legado.app.utils.fromJsonArray
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
object DefaultData {
|
||||
|
||||
const val httpTtsFileName = "httpTTS.json"
|
||||
const val txtTocRuleFileName = "txtTocRule.json"
|
||||
|
||||
val httpTTS: List<HttpTTS> by lazy {
|
||||
val json =
|
||||
String(
|
||||
appCtx.assets.open("defaultData${File.separator}$httpTtsFileName")
|
||||
.readBytes()
|
||||
)
|
||||
HttpTTS.fromJsonArray(json)
|
||||
}
|
||||
|
||||
val readConfigs: List<ReadBookConfig.Config> by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}${ReadBookConfig.configFileName}")
|
||||
.readBytes()
|
||||
)
|
||||
GSON.fromJsonArray(json)!!
|
||||
}
|
||||
|
||||
val txtTocRules: List<TxtTocRule> by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}$txtTocRuleFileName")
|
||||
.readBytes()
|
||||
)
|
||||
GSON.fromJsonArray(json)!!
|
||||
}
|
||||
|
||||
val themeConfigs: List<ThemeConfig.Config> by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}${ThemeConfig.configFileName}")
|
||||
.readBytes()
|
||||
)
|
||||
GSON.fromJsonArray(json)!!
|
||||
}
|
||||
|
||||
val rssSources: List<RssSource> by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}rssSources.json")
|
||||
.readBytes()
|
||||
)
|
||||
RssSource.fromJsonArray(json)
|
||||
}
|
||||
|
||||
fun importDefaultHttpTTS() {
|
||||
appDb.httpTTSDao.deleteDefault()
|
||||
appDb.httpTTSDao.insert(*httpTTS.toTypedArray())
|
||||
}
|
||||
|
||||
fun importDefaultTocRules() {
|
||||
appDb.txtTocRuleDao.deleteDefault()
|
||||
appDb.txtTocRuleDao.insert(*txtTocRules.toTypedArray())
|
||||
}
|
||||
|
||||
fun importDefaultRssSources() {
|
||||
appDb.rssSourceDao.insert(*rssSources.toTypedArray())
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import io.legado.app.model.NoStackTraceException
|
||||
import io.legado.app.model.analyzeRule.AnalyzeRule
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.analyzeRule.RuleData
|
||||
import io.legado.app.utils.jsonPath
|
||||
import io.legado.app.utils.readString
|
||||
import splitties.init.appCtx
|
||||
import java.io.File
|
||||
|
||||
object DirectLinkUpload {
|
||||
|
||||
private const val uploadUrlKey = "directLinkUploadUrl"
|
||||
private const val downloadUrlRuleKey = "directLinkDownloadUrlRule"
|
||||
private const val summaryKey = "directSummary"
|
||||
|
||||
suspend fun upLoad(fileName: String, file: Any, contentType: String): String {
|
||||
val url = getUploadUrl()
|
||||
if (url.isNullOrBlank()) {
|
||||
throw NoStackTraceException("上传url未配置")
|
||||
}
|
||||
val downloadUrlRule = getDownloadUrlRule()
|
||||
if (downloadUrlRule.isNullOrBlank()) {
|
||||
throw NoStackTraceException("下载地址规则未配置")
|
||||
}
|
||||
val analyzeUrl = AnalyzeUrl(url)
|
||||
val res = analyzeUrl.upload(fileName, file, contentType)
|
||||
val analyzeRule = AnalyzeRule(RuleData()).setContent(res.body, res.url)
|
||||
val downloadUrl = analyzeRule.getString(downloadUrlRule)
|
||||
if (downloadUrl.isBlank()) {
|
||||
throw NoStackTraceException("上传失败,${res.body}")
|
||||
}
|
||||
return downloadUrl
|
||||
}
|
||||
|
||||
private val ruleDoc by lazy {
|
||||
val json = String(
|
||||
appCtx.assets.open("defaultData${File.separator}directLinkUpload.json")
|
||||
.readBytes()
|
||||
)
|
||||
jsonPath.parse(json)
|
||||
}
|
||||
|
||||
fun getUploadUrl(): String? {
|
||||
return CacheManager.get(uploadUrlKey)
|
||||
?: ruleDoc.readString("$.UploadUrl")
|
||||
}
|
||||
|
||||
fun putUploadUrl(url: String) {
|
||||
CacheManager.put(uploadUrlKey, url)
|
||||
}
|
||||
|
||||
fun getDownloadUrlRule(): String? {
|
||||
return CacheManager.get(downloadUrlRuleKey)
|
||||
?: ruleDoc.readString("$.DownloadUrlRule")
|
||||
}
|
||||
|
||||
fun putDownloadUrlRule(rule: String) {
|
||||
CacheManager.put(downloadUrlRuleKey, rule)
|
||||
}
|
||||
|
||||
fun getSummary(): String? {
|
||||
return CacheManager.get(summaryKey)
|
||||
?: ruleDoc.readString("summary")
|
||||
}
|
||||
|
||||
fun putSummary(summary: String?) {
|
||||
if (summary != null) {
|
||||
CacheManager.put(summaryKey, summary)
|
||||
} else {
|
||||
CacheManager.delete(summaryKey)
|
||||
}
|
||||
}
|
||||
|
||||
fun delete() {
|
||||
CacheManager.delete(uploadUrlKey)
|
||||
CacheManager.delete(downloadUrlRuleKey)
|
||||
CacheManager.delete(summaryKey)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.text.TextUtils
|
||||
|
||||
@Suppress("unused")
|
||||
class EventMessage {
|
||||
|
||||
var what: Int? = null
|
||||
var tag: String? = null
|
||||
var obj: Any? = null
|
||||
|
||||
fun isFrom(tag: String): Boolean {
|
||||
return TextUtils.equals(this.tag, tag)
|
||||
}
|
||||
|
||||
fun maybeFrom(vararg tags: String): Boolean {
|
||||
return listOf(*tags).contains(tag)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
||||
fun obtain(tag: String): EventMessage {
|
||||
val message = EventMessage()
|
||||
message.tag = tag
|
||||
return message
|
||||
}
|
||||
|
||||
fun obtain(what: Int): EventMessage {
|
||||
val message = EventMessage()
|
||||
message.what = what
|
||||
return message
|
||||
}
|
||||
|
||||
fun obtain(what: Int, obj: Any): EventMessage {
|
||||
val message = EventMessage()
|
||||
message.what = what
|
||||
message.obj = obj
|
||||
return message
|
||||
}
|
||||
|
||||
fun obtain(tag: String, obj: Any): EventMessage {
|
||||
val message = EventMessage()
|
||||
message.tag = tag
|
||||
message.obj = obj
|
||||
return message
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package io.legado.app.help
|
||||
|
||||
object IntentData {
|
||||
|
||||
private val bigData: MutableMap<String, Any> = mutableMapOf()
|
||||
|
||||
@Synchronized
|
||||
fun put(key: String, data: Any?) {
|
||||
data?.let {
|
||||
bigData[key] = data
|
||||
}
|
||||
}
|
||||
|
||||
@Synchronized
|
||||
fun put(data: Any?): String {
|
||||
val key = System.currentTimeMillis().toString()
|
||||
data?.let {
|
||||
bigData[key] = data
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
@Synchronized
|
||||
fun <T> get(key: String?): T? {
|
||||
if (key == null) return null
|
||||
val data = bigData[key]
|
||||
bigData.remove(key)
|
||||
return data as? T
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import io.legado.app.R
|
||||
import io.legado.app.utils.toastOnUi
|
||||
|
||||
@Suppress("unused")
|
||||
object IntentHelp {
|
||||
|
||||
|
||||
fun toTTSSetting(context: Context) {
|
||||
//跳转到文字转语音设置界面
|
||||
kotlin.runCatching {
|
||||
val intent = Intent()
|
||||
intent.action = "com.android.settings.TTS_SETTINGS"
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}.onFailure {
|
||||
context.toastOnUi(R.string.tip_cannot_jump_setting_page)
|
||||
}
|
||||
}
|
||||
|
||||
fun toInstallUnknown(context: Context) {
|
||||
kotlin.runCatching {
|
||||
val intent = Intent()
|
||||
intent.action = "android.settings.MANAGE_UNKNOWN_APP_SOURCES"
|
||||
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
context.startActivity(intent)
|
||||
}.onFailure {
|
||||
context.toastOnUi("无法打开设置")
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,666 @@
|
||||
package io.legado.app.help
|
||||
|
||||
import android.net.Uri
|
||||
import android.util.Base64
|
||||
import androidx.annotation.Keep
|
||||
import io.legado.app.BuildConfig
|
||||
import io.legado.app.constant.AppConst
|
||||
import io.legado.app.constant.AppConst.dateFormat
|
||||
import io.legado.app.data.entities.BaseSource
|
||||
import io.legado.app.help.http.*
|
||||
import io.legado.app.model.Debug
|
||||
import io.legado.app.model.analyzeRule.AnalyzeUrl
|
||||
import io.legado.app.model.analyzeRule.QueryTTF
|
||||
import io.legado.app.utils.*
|
||||
import kotlinx.coroutines.Dispatchers.IO
|
||||
import kotlinx.coroutines.async
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.jsoup.Connection
|
||||
import org.jsoup.Jsoup
|
||||
import splitties.init.appCtx
|
||||
import timber.log.Timber
|
||||
import java.io.ByteArrayInputStream
|
||||
import java.io.ByteArrayOutputStream
|
||||
import java.io.File
|
||||
import java.net.URLEncoder
|
||||
import java.nio.charset.Charset
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
import java.util.zip.ZipEntry
|
||||
import java.util.zip.ZipInputStream
|
||||
|
||||
/**
|
||||
* js扩展类, 在js中通过java变量调用
|
||||
* 所有对于文件的读写删操作都是相对路径,只能操作阅读缓存内的文件
|
||||
* /android/data/{package}/cache/...
|
||||
*/
|
||||
@Keep
|
||||
@Suppress("unused")
|
||||
interface JsExtensions {
|
||||
|
||||
fun getSource(): BaseSource?
|
||||
|
||||
/**
|
||||
* 访问网络,返回String
|
||||
*/
|
||||
fun ajax(urlStr: String): String? {
|
||||
return runBlocking {
|
||||
kotlin.runCatching {
|
||||
val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())
|
||||
analyzeUrl.getStrResponseAwait().body
|
||||
}.onFailure {
|
||||
log("ajax(${urlStr}) error\n${it.stackTraceToString()}")
|
||||
Timber.e(it)
|
||||
}.getOrElse {
|
||||
it.msg
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 并发访问网络
|
||||
*/
|
||||
fun ajaxAll(urlList: Array<String>): Array<StrResponse?> {
|
||||
return runBlocking {
|
||||
val asyncArray = Array(urlList.size) {
|
||||
async(IO) {
|
||||
val url = urlList[it]
|
||||
val analyzeUrl = AnalyzeUrl(url, source = getSource())
|
||||
analyzeUrl.getStrResponseAwait()
|
||||
}
|
||||
}
|
||||
val resArray = Array<StrResponse?>(urlList.size) {
|
||||
asyncArray[it].await()
|
||||
}
|
||||
resArray
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 访问网络,返回Response<String>
|
||||
*/
|
||||
fun connect(urlStr: String): StrResponse {
|
||||
return runBlocking {
|
||||
val analyzeUrl = AnalyzeUrl(urlStr, source = getSource())
|
||||
kotlin.runCatching {
|
||||
analyzeUrl.getStrResponseAwait()
|
||||
}.onFailure {
|
||||
log("connect(${urlStr}) error\n${it.stackTraceToString()}")
|
||||
Timber.e(it)
|
||||
}.getOrElse {
|
||||
StrResponse(analyzeUrl.url, it.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun connect(urlStr: String, header: String?): StrResponse {
|
||||
return runBlocking {
|
||||
val headerMap = GSON.fromJsonObject<Map<String, String>>(header)
|
||||
val analyzeUrl = AnalyzeUrl(urlStr, headerMapF = headerMap, source = getSource())
|
||||
kotlin.runCatching {
|
||||
analyzeUrl.getStrResponseAwait()
|
||||
}.onFailure {
|
||||
log("ajax($urlStr,$header) error\n${it.stackTraceToString()}")
|
||||
Timber.e(it)
|
||||
}.getOrElse {
|
||||
StrResponse(analyzeUrl.url, it.localizedMessage)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用webView访问网络
|
||||
* @param html 直接用webView载入的html, 如果html为空直接访问url
|
||||
* @param url html内如果有相对路径的资源不传入url访问不了
|
||||
* @param js 用来取返回值的js语句, 没有就返回整个源代码
|
||||
* @return 返回js获取的内容
|
||||
*/
|
||||
fun webView(html: String?, url: String?, js: String?): String? {
|
||||
return runBlocking {
|
||||
BackstageWebView(
|
||||
url = url,
|
||||
html = html,
|
||||
javaScript = js
|
||||
).getStrResponse().body
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 实现16进制字符串转文件
|
||||
* @param content 需要转成文件的16进制字符串
|
||||
* @param url 通过url里的参数来判断文件类型
|
||||
* @return 相对路径
|
||||
*/
|
||||
fun downloadFile(content: String, url: String): String {
|
||||
val type = AnalyzeUrl(url, source = getSource()).type ?: return ""
|
||||
val zipPath = FileUtils.getPath(
|
||||
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
|
||||
"${MD5Utils.md5Encode16(url)}.${type}"
|
||||
)
|
||||
FileUtils.deleteFile(zipPath)
|
||||
val zipFile = FileUtils.createFileIfNotExist(zipPath)
|
||||
StringUtils.hexStringToByte(content).let {
|
||||
if (it.isNotEmpty()) {
|
||||
zipFile.writeBytes(it)
|
||||
}
|
||||
}
|
||||
return zipPath.substring(FileUtils.getCachePath().length)
|
||||
}
|
||||
|
||||
/**
|
||||
* js实现重定向拦截,网络访问get
|
||||
*/
|
||||
fun get(urlStr: String, headers: Map<String, String>): Connection.Response {
|
||||
return Jsoup.connect(urlStr)
|
||||
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
||||
.ignoreContentType(true)
|
||||
.followRedirects(false)
|
||||
.headers(headers)
|
||||
.method(Connection.Method.GET)
|
||||
.execute()
|
||||
}
|
||||
|
||||
/**
|
||||
* 网络访问post
|
||||
*/
|
||||
fun post(urlStr: String, body: String, headers: Map<String, String>): Connection.Response {
|
||||
return Jsoup.connect(urlStr)
|
||||
.sslSocketFactory(SSLHelper.unsafeSSLSocketFactory)
|
||||
.ignoreContentType(true)
|
||||
.followRedirects(false)
|
||||
.requestBody(body)
|
||||
.headers(headers)
|
||||
.method(Connection.Method.POST)
|
||||
.execute()
|
||||
}
|
||||
|
||||
/**
|
||||
*js实现读取cookie
|
||||
*/
|
||||
fun getCookie(tag: String, key: String? = null): String {
|
||||
val cookie = CookieStore.getCookie(tag)
|
||||
val cookieMap = CookieStore.cookieToMap(cookie)
|
||||
return if (key != null) {
|
||||
cookieMap[key] ?: ""
|
||||
} else {
|
||||
cookie
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* js实现解码,不能删
|
||||
*/
|
||||
fun base64Decode(str: String): String {
|
||||
return EncoderUtils.base64Decode(str, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun base64Decode(str: String, flags: Int): String {
|
||||
return EncoderUtils.base64Decode(str, flags)
|
||||
}
|
||||
|
||||
fun base64DecodeToByteArray(str: String?): ByteArray? {
|
||||
if (str.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return Base64.decode(str, Base64.DEFAULT)
|
||||
}
|
||||
|
||||
fun base64DecodeToByteArray(str: String?, flags: Int): ByteArray? {
|
||||
if (str.isNullOrBlank()) {
|
||||
return null
|
||||
}
|
||||
return Base64.decode(str, flags)
|
||||
}
|
||||
|
||||
fun base64Encode(str: String): String? {
|
||||
return EncoderUtils.base64Encode(str, Base64.NO_WRAP)
|
||||
}
|
||||
|
||||
fun base64Encode(str: String, flags: Int): String? {
|
||||
return EncoderUtils.base64Encode(str, flags)
|
||||
}
|
||||
|
||||
fun md5Encode(str: String): String {
|
||||
return MD5Utils.md5Encode(str)
|
||||
}
|
||||
|
||||
fun md5Encode16(str: String): String {
|
||||
return MD5Utils.md5Encode16(str)
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
fun timeFormatUTC(time: Long, format: String, sh: Int): String? {
|
||||
val utc = SimpleTimeZone(sh, "UTC")
|
||||
return SimpleDateFormat(format, Locale.getDefault()).run {
|
||||
timeZone = utc
|
||||
format(Date(time))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 时间格式化
|
||||
*/
|
||||
fun timeFormat(time: Long): String {
|
||||
return dateFormat.format(Date(time))
|
||||
}
|
||||
|
||||
/**
|
||||
* utf8编码转gbk编码
|
||||
*/
|
||||
fun utf8ToGbk(str: String): String {
|
||||
val utf8 = String(str.toByteArray(charset("UTF-8")))
|
||||
val unicode = String(utf8.toByteArray(), charset("UTF-8"))
|
||||
return String(unicode.toByteArray(charset("GBK")))
|
||||
}
|
||||
|
||||
fun encodeURI(str: String): String {
|
||||
return try {
|
||||
URLEncoder.encode(str, "UTF-8")
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun encodeURI(str: String, enc: String): String {
|
||||
return try {
|
||||
URLEncoder.encode(str, enc)
|
||||
} catch (e: Exception) {
|
||||
""
|
||||
}
|
||||
}
|
||||
|
||||
fun htmlFormat(str: String): String {
|
||||
return HtmlFormatter.formatKeepImg(str)
|
||||
}
|
||||
|
||||
//****************文件操作******************//
|
||||
|
||||
/**
|
||||
* 获取本地文件
|
||||
* @param path 相对路径
|
||||
* @return File
|
||||
*/
|
||||
fun getFile(path: String): File {
|
||||
val cachePath = appCtx.externalCache.absolutePath
|
||||
val aPath = if (path.startsWith(File.separator)) {
|
||||
cachePath + path
|
||||
} else {
|
||||
cachePath + File.separator + path
|
||||
}
|
||||
return File(aPath)
|
||||
}
|
||||
|
||||
fun readFile(path: String): ByteArray? {
|
||||
val file = getFile(path)
|
||||
if (file.exists()) {
|
||||
return file.readBytes()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun readTxtFile(path: String): String {
|
||||
val file = getFile(path)
|
||||
if (file.exists()) {
|
||||
val charsetName = EncodingDetect.getEncode(file)
|
||||
return String(file.readBytes(), charset(charsetName))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
fun readTxtFile(path: String, charsetName: String): String {
|
||||
val file = getFile(path)
|
||||
if (file.exists()) {
|
||||
return String(file.readBytes(), charset(charsetName))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除本地文件
|
||||
*/
|
||||
fun deleteFile(path: String) {
|
||||
val file = getFile(path)
|
||||
FileUtils.delete(file, true)
|
||||
}
|
||||
|
||||
/**
|
||||
* js实现压缩文件解压
|
||||
* @param zipPath 相对路径
|
||||
* @return 相对路径
|
||||
*/
|
||||
fun unzipFile(zipPath: String): String {
|
||||
if (zipPath.isEmpty()) return ""
|
||||
val unzipPath = FileUtils.getPath(
|
||||
FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
|
||||
FileUtils.getNameExcludeExtension(zipPath)
|
||||
)
|
||||
FileUtils.deleteFile(unzipPath)
|
||||
val zipFile = getFile(zipPath)
|
||||
val unzipFolder = FileUtils.createFolderIfNotExist(unzipPath)
|
||||
ZipUtils.unzipFile(zipFile, unzipFolder)
|
||||
FileUtils.deleteFile(zipFile.absolutePath)
|
||||
return unzipPath.substring(FileUtils.getCachePath().length)
|
||||
}
|
||||
|
||||
/**
|
||||
* js实现文件夹内所有文件读取
|
||||
*/
|
||||
fun getTxtInFolder(unzipPath: String): String {
|
||||
if (unzipPath.isEmpty()) return ""
|
||||
val unzipFolder = getFile(unzipPath)
|
||||
val contents = StringBuilder()
|
||||
unzipFolder.listFiles().let {
|
||||
if (it != null) {
|
||||
for (f in it) {
|
||||
val charsetName = EncodingDetect.getEncode(f)
|
||||
contents.append(String(f.readBytes(), charset(charsetName)))
|
||||
.append("\n")
|
||||
}
|
||||
contents.deleteCharAt(contents.length - 1)
|
||||
}
|
||||
}
|
||||
FileUtils.deleteFile(unzipFolder.absolutePath)
|
||||
return contents.toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络zip文件里面的数据
|
||||
* @param url zip文件的链接或十六进制字符串
|
||||
* @param path 所需获取文件在zip内的路径
|
||||
* @return zip指定文件的数据
|
||||
*/
|
||||
fun getZipStringContent(url: String, path: String): String {
|
||||
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
||||
val charsetName = EncodingDetect.getEncode(byteArray)
|
||||
return String(byteArray, Charset.forName(charsetName))
|
||||
}
|
||||
|
||||
fun getZipStringContent(url: String, path: String, charsetName: String): String {
|
||||
val byteArray = getZipByteArrayContent(url, path) ?: return ""
|
||||
return String(byteArray, Charset.forName(charsetName))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取网络zip文件里面的数据
|
||||
* @param url zip文件的链接或十六进制字符串
|
||||
* @param path 所需获取文件在zip内的路径
|
||||
* @return zip指定文件的数据
|
||||
*/
|
||||
fun getZipByteArrayContent(url: String, path: String): ByteArray? {
|
||||
val bytes = if (url.startsWith("http://") || url.startsWith("https://")) {
|
||||
runBlocking {
|
||||
return@runBlocking okHttpClient.newCallResponseBody { url(url) }.bytes()
|
||||
}
|
||||
} else {
|
||||
StringUtils.hexStringToByte(url)
|
||||
}
|
||||
val bos = ByteArrayOutputStream()
|
||||
val zis = ZipInputStream(ByteArrayInputStream(bytes))
|
||||
var entry: ZipEntry? = zis.nextEntry
|
||||
while (entry != null) {
|
||||
if (entry.name.equals(path)) {
|
||||
zis.use { it.copyTo(bos) }
|
||||
return bos.toByteArray()
|
||||
}
|
||||
entry = zis.nextEntry
|
||||
}
|
||||
log("getZipContent 未发现内容")
|
||||
return null
|
||||
}
|
||||
|
||||
//******************文件操作************************//
|
||||
|
||||
/**
|
||||
* 解析字体,返回字体解析类
|
||||
*/
|
||||
fun queryBase64TTF(base64: String?): QueryTTF? {
|
||||
base64DecodeToByteArray(base64)?.let {
|
||||
return QueryTTF(it)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* 返回字体解析类
|
||||
* @param str 支持url,本地文件,base64,自动判断,自动缓存
|
||||
*/
|
||||
fun queryTTF(str: String?): QueryTTF? {
|
||||
str ?: return null
|
||||
val key = md5Encode16(str)
|
||||
var qTTF = CacheManager.getQueryTTF(key)
|
||||
if (qTTF != null) return qTTF
|
||||
val font: ByteArray? = when {
|
||||
str.isAbsUrl() -> runBlocking {
|
||||
var x = CacheManager.getByteArray(key)
|
||||
if (x == null) {
|
||||
x = okHttpClient.newCallResponseBody { url(str) }.bytes()
|
||||
x.let {
|
||||
CacheManager.put(key, it)
|
||||
}
|
||||
}
|
||||
return@runBlocking x
|
||||
}
|
||||
str.isContentScheme() -> Uri.parse(str).readBytes(appCtx)
|
||||
str.startsWith("/storage") -> File(str).readBytes()
|
||||
else -> base64DecodeToByteArray(str)
|
||||
}
|
||||
font ?: return null
|
||||
qTTF = QueryTTF(font)
|
||||
CacheManager.put(key, qTTF)
|
||||
return qTTF
|
||||
}
|
||||
|
||||
/**
|
||||
* @param text 包含错误字体的内容
|
||||
* @param font1 错误的字体
|
||||
* @param font2 正确的字体
|
||||
*/
|
||||
fun replaceFont(
|
||||
text: String,
|
||||
font1: QueryTTF?,
|
||||
font2: QueryTTF?
|
||||
): String {
|
||||
if (font1 == null || font2 == null) return text
|
||||
val contentArray = text.toCharArray()
|
||||
contentArray.forEachIndexed { index, s ->
|
||||
val oldCode = s.code
|
||||
if (font1.inLimit(s)) {
|
||||
val glyf = font1.getGlyfByCode(oldCode)
|
||||
val code = font2.getCodeByGlyf(glyf)
|
||||
if (code != 0) {
|
||||
contentArray[index] = code.toChar()
|
||||
}
|
||||
}
|
||||
}
|
||||
return contentArray.joinToString("")
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出调试日志
|
||||
*/
|
||||
fun log(msg: String): String {
|
||||
getSource()?.let {
|
||||
Debug.log(it.getKey(), msg)
|
||||
} ?: Debug.log(msg)
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.d(msg)
|
||||
}
|
||||
return msg
|
||||
}
|
||||
|
||||
/**
|
||||
* 输出对象类型
|
||||
*/
|
||||
fun logType(any: Any?) {
|
||||
if (any == null) {
|
||||
log("null")
|
||||
} else {
|
||||
log(any.javaClass.name)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成UUID
|
||||
*/
|
||||
fun randomUUID(): String {
|
||||
return UUID.randomUUID().toString()
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 解码为 ByteArray
|
||||
* @param str 传入的AES加密的数据
|
||||
* @param key AES 解密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
fun aesDecodeToByteArray(
|
||||
str: String, key: String, transformation: String, iv: String
|
||||
): ByteArray? {
|
||||
return try {
|
||||
EncoderUtils.decryptAES(
|
||||
data = str.encodeToByteArray(),
|
||||
key = key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
log(e.localizedMessage ?: "aesDecodeToByteArrayERROR")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* AES 解码为 String
|
||||
* @param str 传入的AES加密的数据
|
||||
* @param key AES 解密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
|
||||
fun aesDecodeToString(
|
||||
str: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return aesDecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 已经base64的AES 解码为 ByteArray
|
||||
* @param str 传入的AES Base64加密的数据
|
||||
* @param key AES 解密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
|
||||
fun aesBase64DecodeToByteArray(
|
||||
str: String, key: String, transformation: String, iv: String
|
||||
): ByteArray? {
|
||||
return try {
|
||||
EncoderUtils.decryptBase64AES(
|
||||
str.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
log(e.localizedMessage ?: "aesDecodeToByteArrayERROR")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 已经base64的AES 解码为 String
|
||||
* @param str 传入的AES Base64加密的数据
|
||||
* @param key AES 解密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
|
||||
fun aesBase64DecodeToString(
|
||||
str: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return aesBase64DecodeToByteArray(str, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密aes为ByteArray
|
||||
* @param data 传入的原始数据
|
||||
* @param key AES加密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
fun aesEncodeToByteArray(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): ByteArray? {
|
||||
return try {
|
||||
EncoderUtils.encryptAES(
|
||||
data.encodeToByteArray(),
|
||||
key = key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
log(e.localizedMessage ?: "aesEncodeToByteArrayERROR")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密aes为String
|
||||
* @param data 传入的原始数据
|
||||
* @param key AES加密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
fun aesEncodeToString(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return aesEncodeToByteArray(data, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密aes后Base64化的ByteArray
|
||||
* @param data 传入的原始数据
|
||||
* @param key AES加密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
fun aesEncodeToBase64ByteArray(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): ByteArray? {
|
||||
return try {
|
||||
EncoderUtils.encryptAES2Base64(
|
||||
data.encodeToByteArray(),
|
||||
key.encodeToByteArray(),
|
||||
transformation,
|
||||
iv.encodeToByteArray()
|
||||
)
|
||||
} catch (e: Exception) {
|
||||
Timber.e(e)
|
||||
log(e.localizedMessage ?: "aesEncodeToBase64ByteArrayERROR")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密aes后Base64化的String
|
||||
* @param data 传入的原始数据
|
||||
* @param key AES加密的key
|
||||
* @param transformation AES加密的方式
|
||||
* @param iv ECB模式的偏移向量
|
||||
*/
|
||||
fun aesEncodeToBase64String(
|
||||
data: String, key: String, transformation: String, iv: String
|
||||
): String? {
|
||||
return aesEncodeToBase64ByteArray(data, key, transformation, iv)?.let { String(it) }
|
||||
}
|
||||
|
||||
fun android(): String {
|
||||
return AppConst.androidId
|
||||
}
|
||||
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user