This commit is contained in:
HapeLee committed 2025-06-04 13:33:51 +08:00
1 parent b86d90021c
commit 6c0757c91b
972 files changed
+6885 -7321

No files matched your search

+255
View File
@@ -0,0 +1,255 @@
package io.legato.kazusa
import android.app.Application
import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.ActivityInfo
import android.content.pm.ApplicationInfo
import android.content.res.Configuration
import android.os.Build
import com.github.liuyueyi.quick.transfer.constants.TransType
import com.google.android.material.color.DynamicColors
import com.jeremyliao.liveeventbus.LiveEventBus
import com.jeremyliao.liveeventbus.logger.DefaultLogger
import com.script.rhino.ReadOnlyJavaObject
import com.script.rhino.RhinoScriptEngine
import com.script.rhino.RhinoWrapFactory
import io.legato.kazusa.base.AppContextWrapper
import io.legato.kazusa.constant.AppConst.channelIdDownload
import io.legato.kazusa.constant.AppConst.channelIdReadAloud
import io.legato.kazusa.constant.AppConst.channelIdWeb
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookChapter
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.HttpTTS
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.data.entities.rule.BookInfoRule
import io.legato.kazusa.data.entities.rule.ContentRule
import io.legato.kazusa.data.entities.rule.ExploreRule
import io.legato.kazusa.data.entities.rule.SearchRule
import io.legato.kazusa.help.AppFreezeMonitor
import io.legato.kazusa.help.AppWebDav
import io.legato.kazusa.help.CrashHandler
import io.legato.kazusa.help.DefaultData
import io.legato.kazusa.help.LifecycleHelp
import io.legato.kazusa.help.RuleBigDataHelp
import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.ThemeConfig.applyDayNight
import io.legato.kazusa.help.config.ThemeConfig.applyDayNightInit
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.help.http.Cronet
import io.legato.kazusa.help.http.ObsoleteUrlFactory
import io.legato.kazusa.help.http.okHttpClient
import io.legato.kazusa.help.rhino.NativeBaseSource
import io.legato.kazusa.help.source.SourceHelp
import io.legato.kazusa.help.storage.Backup
import io.legato.kazusa.model.BookCover
import io.legato.kazusa.utils.ChineseUtils
import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.defaultSharedPreferences
import io.legato.kazusa.utils.getPrefBoolean
import io.legato.kazusa.utils.isDebuggable
import kotlinx.coroutines.launch
import org.chromium.base.ThreadUtils
import splitties.init.appCtx
import splitties.systemservices.notificationManager
import java.net.URL
import java.util.concurrent.TimeUnit
import java.util.logging.Level
class App : Application() {
private lateinit var oldConfig: Configuration
override fun onCreate() {
super.onCreate()
DynamicColors.applyToActivitiesIfAvailable(this)
CrashHandler(this)
if (isDebuggable) {
ThreadUtils.setThreadAssertsDisabledForTesting(true)
}
oldConfig = Configuration(resources.configuration)
applyDayNightInit(this)
registerActivityLifecycleCallbacks(LifecycleHelp)
defaultSharedPreferences.registerOnSharedPreferenceChangeListener(AppConfig)
Coroutine.async {
LogUtils.init(this@App)
LogUtils.d("App", "onCreate")
LogUtils.logDeviceInfo()
//预下载Cronet so
Cronet.preDownload()
createNotificationChannels()
LiveEventBus.config()
.lifecycleObserverAlwaysActive(true)
.autoClear(false)
.enableLogger(BuildConfig.DEBUG || AppConfig.recordLog)
.setLogger(EventLogger())
DefaultData.upVersion()
AppFreezeMonitor.init(this@App)
URL.setURLStreamHandlerFactory(ObsoleteUrlFactory(okHttpClient))
launch { installGmsTlsProvider(appCtx) }
initRhino()
//初始化封面
BookCover.toString()
//清除过期数据
appDb.cacheDao.clearDeadline(System.currentTimeMillis())
if (getPrefBoolean(PreferKey.autoClearExpired, true)) {
val clearTime = System.currentTimeMillis() - TimeUnit.DAYS.toMillis(1)
appDb.searchBookDao.clearExpired(clearTime)
}
RuleBigDataHelp.clearInvalid()
BookHelp.clearInvalidCache()
Backup.clearCache()
//初始化简繁转换引擎
when (AppConfig.chineseConverterType) {
1 -> {
ChineseUtils.fixT2sDict()
ChineseUtils.preLoad(true, TransType.TRADITIONAL_TO_SIMPLE)
}
2 -> ChineseUtils.preLoad(true, TransType.SIMPLE_TO_TRADITIONAL)
}
//调整排序序号
SourceHelp.adjustSortNumber()
//同步阅读记录
if (AppConfig.syncBookProgress) {
AppWebDav.downloadAllBookProgress()
}
}
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(AppContextWrapper.wrap(base))
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
val diff = newConfig.diff(oldConfig)
if ((diff and ActivityInfo.CONFIG_UI_MODE) != 0) {
applyDayNight(this)
}
oldConfig = Configuration(newConfig)
}
/**
* 尝试在安装了GMS的设备上(GMS或者MicroG)使用GMS内置的Conscrypt
* 作为首选JCE提供程序,而使Okhttp在低版本Android上
* 能够启用TLSv1.3
* https://f-droid.org/zh_Hans/2020/05/29/android-updates-and-tls-connections.html
* https://developer.android.google.cn/reference/javax/net/ssl/SSLSocket
*
* @param context
* @return
*/
private fun installGmsTlsProvider(context: Context) {
try {
val gmsPackageName = "com.google.android.gms"
val appInfo = packageManager.getApplicationInfo(gmsPackageName, 0)
if ((appInfo.flags and ApplicationInfo.FLAG_SYSTEM) == 0) {
return
}
val gms = context.createPackageContext(
gmsPackageName,
CONTEXT_INCLUDE_CODE or CONTEXT_IGNORE_SECURITY
)
gms.classLoader
.loadClass("com.google.android.gms.common.security.ProviderInstallerImpl")
.getMethod("insertProvider", Context::class.java)
.invoke(null, gms)
} catch (e: java.lang.Exception) {
e.printStackTrace()
}
}
/**
* 创建通知ID
*/
private fun createNotificationChannels() {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val downloadChannel = NotificationChannel(
channelIdDownload,
getString(R.string.action_download),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(false)
enableVibration(false)
setSound(null, null)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val readAloudChannel = NotificationChannel(
channelIdReadAloud,
getString(R.string.read_aloud),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(false)
enableVibration(false)
setSound(null, null)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
val webChannel = NotificationChannel(
channelIdWeb,
getString(R.string.web_service),
NotificationManager.IMPORTANCE_DEFAULT
).apply {
enableLights(false)
enableVibration(false)
setSound(null, null)
lockscreenVisibility = Notification.VISIBILITY_PUBLIC
}
//向notification manager 提交channel
notificationManager.createNotificationChannels(
listOf(
downloadChannel,
readAloudChannel,
webChannel
)
)
}
private fun initRhino() {
RhinoScriptEngine
RhinoWrapFactory.register(BookSource::class.java, NativeBaseSource.factory)
RhinoWrapFactory.register(RssSource::class.java, NativeBaseSource.factory)
RhinoWrapFactory.register(HttpTTS::class.java, NativeBaseSource.factory)
RhinoWrapFactory.register(ExploreRule::class.java, ReadOnlyJavaObject.factory)
RhinoWrapFactory.register(SearchRule::class.java, ReadOnlyJavaObject.factory)
RhinoWrapFactory.register(BookInfoRule::class.java, ReadOnlyJavaObject.factory)
RhinoWrapFactory.register(ContentRule::class.java, ReadOnlyJavaObject.factory)
RhinoWrapFactory.register(BookChapter::class.java, ReadOnlyJavaObject.factory)
RhinoWrapFactory.register(Book.ReadConfig::class.java, ReadOnlyJavaObject.factory)
}
class EventLogger : DefaultLogger() {
override fun log(level: Level, msg: String) {
super.log(level, msg)
LogUtils.d(TAG, msg)
}
override fun log(level: Level, msg: String, th: Throwable?) {
super.log(level, msg, th)
LogUtils.d(TAG, "$msg\n${th?.stackTraceToString()}")
}
companion object {
private const val TAG = "[LiveEventBus]"
}
}
companion object {
init {
if (BuildConfig.DEBUG) {
System.setProperty("kotlinx.coroutines.debug", "on")
}
}
}
}
@@ -0,0 +1,15 @@
# 文件结构介绍
* api 提供的接口
* base 基类
* constant 常量
* data 数据
* exception 错误类型
* help 帮助
* lib 库
* model 解析
* receiver 广播侦听
* service 服务
* ui 界面
* utils 辅助类
* web web服务
@@ -0,0 +1,164 @@
/*
* Copyright (C) 2020 w568w
*/
package io.legato.kazusa.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.legato.kazusa.api.controller.BookController
import io.legato.kazusa.api.controller.BookSourceController
import io.legato.kazusa.api.controller.RssSourceController
import kotlinx.coroutines.runBlocking
/**
* 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,
SaveBookProgress
}
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(): Boolean {
context?.let { context ->
ShortCuts.buildShortCuts(context)
}
return false
}
override fun delete(
uri: Uri,
selection: String?,
selectionArgs: Array<String>?
): Int {
if (sMatcher.match(uri) < 0) return -1
when (RequestCode.entries[sMatcher.match(uri)]) {
RequestCode.DeleteBookSources -> BookSourceController.deleteSources(selection)
RequestCode.DeleteRssSources -> BookSourceController.deleteSources(selection)
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.entries[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
runBlocking {
when (RequestCode.entries[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))
}
RequestCode.SaveBookProgress -> values?.let {
BookController.saveBookProgress(values.getAsString(postBodyKey))
}
else -> throw IllegalStateException(
"Unexpected value: " + RequestCode.entries[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.entries[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.entries[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,29 @@
package io.legato.kazusa.api
import androidx.annotation.Keep
@Keep
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,63 @@
package io.legato.kazusa.api
import android.content.Context
import android.content.Intent
import androidx.core.content.pm.ShortcutInfoCompat
import androidx.core.content.pm.ShortcutManagerCompat
import androidx.core.graphics.drawable.IconCompat
import io.legato.kazusa.R
import io.legato.kazusa.receiver.SharedReceiverActivity
import io.legato.kazusa.ui.book.read.ReadBookActivity
import io.legato.kazusa.ui.main.MainActivity
object ShortCuts {
private inline fun <reified T> buildIntent(context: Context): Intent {
val intent = Intent(context, T::class.java)
intent.action = Intent.ACTION_VIEW
return intent
}
private fun buildBookShelfShortCutInfo(context: Context): ShortcutInfoCompat {
val bookShelfIntent = buildIntent<MainActivity>(context)
return ShortcutInfoCompat.Builder(context, "bookshelf")
.setShortLabel(context.getString(R.string.bookshelf))
.setLongLabel(context.getString(R.string.bookshelf))
.setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book))
.setIntent(bookShelfIntent)
.build()
}
private fun buildReadBookShortCutInfo(context: Context): ShortcutInfoCompat {
val bookShelfIntent = buildIntent<MainActivity>(context)
val readBookIntent = buildIntent<ReadBookActivity>(context)
return ShortcutInfoCompat.Builder(context, "lastRead")
.setShortLabel(context.getString(R.string.last_read))
.setLongLabel(context.getString(R.string.last_read))
.setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book))
.setIntents(arrayOf(bookShelfIntent, readBookIntent))
.build()
}
private fun buildReadAloudShortCutInfo(context: Context): ShortcutInfoCompat {
val readAloudIntent = buildIntent<SharedReceiverActivity>(context)
readAloudIntent.putExtra("action", "readAloud")
return ShortcutInfoCompat.Builder(context, "readAloud")
.setShortLabel(context.getString(R.string.read_aloud))
.setLongLabel(context.getString(R.string.read_aloud))
.setIcon(IconCompat.createWithResource(context, R.drawable.icon_read_book))
.setIntent(readAloudIntent)
.build()
}
fun buildShortCuts(context: Context) {
ShortcutManagerCompat.setDynamicShortcuts(
context, listOf(
buildReadBookShortCutInfo(context),
buildReadAloudShortCutInfo(context),
buildBookShelfShortCutInfo(context)
)
)
}
}
@@ -0,0 +1,325 @@
package io.legato.kazusa.api.controller
import android.graphics.Bitmap
import android.graphics.drawable.Drawable
import androidx.core.graphics.drawable.toBitmap
import com.bumptech.glide.Glide
import io.legato.kazusa.api.ReturnData
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookProgress
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.help.AppWebDav
import io.legato.kazusa.help.CacheManager
import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.help.book.ContentProcessor
import io.legato.kazusa.help.book.isLocal
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.glide.ImageLoader
import io.legato.kazusa.model.BookCover
import io.legato.kazusa.model.ImageProvider
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.model.localBook.LocalBook
import io.legato.kazusa.model.webBook.WebBook
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.cnCompare
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.printOnDebug
import io.legato.kazusa.utils.stackTraceStr
import kotlinx.coroutines.delay
import kotlinx.coroutines.runBlocking
import splitties.init.appCtx
import java.io.File
import java.util.WeakHashMap
import java.util.concurrent.TimeUnit
object BookController {
private lateinit var book: Book
private var bookSource: BookSource? = null
private var bookUrl: String = ""
private val defaultCoverCache by lazy { WeakHashMap<Drawable, Bitmap>() }
/**
* 书架所有书籍
*/
val bookshelf: ReturnData
get() {
val books = appDb.bookDao.all
val returnData = ReturnData()
return if (books.isEmpty()) {
returnData.setErrorMsg("还没有添加小说")
} else {
val data = when (AppConfig.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)
.override(84, 112)
.centerCrop()
.submit()
return try {
returnData.setData(ftBitmap.get(3, TimeUnit.SECONDS))
} catch (e: Exception) {
try {
val defaultBitmap = defaultCoverCache.getOrPut(BookCover.defaultDrawable) {
Glide.with(appCtx)
.asBitmap()
.load(BookCover.defaultDrawable.toBitmap())
.override(84, 112)
.centerCrop()
.submit()
.get()
}
returnData.setData(defaultBitmap)
} catch (e: Exception) {
returnData.setErrorMsg(e.localizedMessage ?: "getCover error")
}
}
}
/**
* 获取正文图片
*/
fun getImg(parameters: Map<String, List<String>>): ReturnData {
val returnData = ReturnData()
val bookUrl = parameters["url"]?.firstOrNull()
?: return returnData.setErrorMsg("bookUrl为空")
val src = parameters["path"]?.firstOrNull()
?: return returnData.setErrorMsg("图片链接为空")
val width = parameters["width"]?.firstOrNull()?.toInt() ?: 640
if (this.bookUrl != bookUrl) {
this.book = appDb.bookDao.getBook(bookUrl)
?: return returnData.setErrorMsg("bookUrl不对")
this.bookSource = appDb.bookSourceDao.getBookSource(book.origin)
}
this.bookUrl = bookUrl
val bitmap = runBlocking {
ImageProvider.cacheImage(book, src, bookSource)
ImageProvider.getImage(book, src, width)
}
return returnData.setData(bitmap)
}
/**
* 更新目录
*/
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("未在数据库找到对应书籍,请先添加")
if (book.isLocal) {
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(bookSource, book)
}
WebBook.getChapterListAwait(bookSource, book).getOrThrow()
}
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 = runBlocking {
var chapter = appDb.bookChapterDao.getChapter(bookUrl, index)
var wait = 0
while (chapter == null && wait < 30) {
delay(1000)
chapter = appDb.bookChapterDao.getChapter(bookUrl, index)
wait++
}
chapter
}
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)
content = runBlocking {
contentProcessor.getContent(book, chapter, content, includeTitle = false)
.toString()
}
return returnData.setData(content)
}
val bookSource = appDb.bookSourceDao.getBookSource(book.origin)
?: return returnData.setErrorMsg("未找到书源")
try {
content = runBlocking {
WebBook.getContentAwait(bookSource, book, chapter).let {
val contentProcessor = ContentProcessor.get(book.name, book.origin)
contentProcessor.getContent(book, chapter, it, includeTitle = false)
.toString()
}
}
returnData.setData(content)
} catch (e: Exception) {
returnData.setErrorMsg(e.stackTraceStr)
}
return returnData
}
/**
* 保存书籍
*/
suspend fun saveBook(postData: String?): ReturnData {
val returnData = ReturnData()
GSON.fromJsonObject<Book>(postData).getOrNull()?.let { book ->
AppWebDav.uploadBookProgress(book)
book.save()
return returnData.setData("")
}
return returnData.setErrorMsg("格式不对")
}
/**
* 删除书籍
*/
fun deleteBook(postData: String?): ReturnData {
val returnData = ReturnData()
GSON.fromJsonObject<Book>(postData).getOrNull()?.let { book ->
book.delete()
return returnData.setData("")
}
return returnData.setErrorMsg("格式不对")
}
/**
* 保存进度
*/
suspend fun saveBookProgress(postData: String?): ReturnData {
val returnData = ReturnData()
GSON.fromJsonObject<BookProgress>(postData)
.onFailure { it.printOnDebug() }
.getOrNull()?.let { bookProgress ->
appDb.bookDao.getBook(bookProgress.name, bookProgress.author)?.let { book ->
book.durChapterIndex = bookProgress.durChapterIndex
book.durChapterPos = bookProgress.durChapterPos
book.durChapterTitle = bookProgress.durChapterTitle
book.durChapterTime = bookProgress.durChapterTime
AppWebDav.uploadBookProgress(bookProgress) {
book.syncTime = System.currentTimeMillis()
}
appDb.bookDao.update(book)
ReadBook.book?.let {
if (it.name == bookProgress.name &&
it.author == bookProgress.author
) {
ReadBook.webBookProgress = bookProgress
}
}
return returnData.setData("")
}
}
return returnData.setErrorMsg("格式不对")
}
/**
* 添加本地书籍
*/
fun addLocalBook(
parameters: Map<String, List<String>>,
files: Map<String, String>
): ReturnData {
val returnData = ReturnData()
val fileName = parameters["fileName"]?.firstOrNull()
?: return returnData.setErrorMsg("fileName 不能为空")
val fileData = files["fileData"]
?: return returnData.setErrorMsg("fileData 不能为空")
kotlin.runCatching {
val uri = LocalBook.saveBookFile(File(fileData).inputStream(), fileName)
LocalBook.importFile(uri)
}.onFailure {
return when (it) {
is SecurityException -> returnData.setErrorMsg("需重新设置书籍保存位置!")
else -> returnData.setErrorMsg("保存书籍错误\n${it.localizedMessage}")
}
}
return returnData.setData(true)
}
/**
* 保存web阅读界面配置
*/
fun saveWebReadConfig(postData: String?): ReturnData {
val returnData = ReturnData()
postData?.let {
CacheManager.put("webReadConfig", postData)
} ?: CacheManager.delete("webReadConfig")
return returnData.setData("")
}
/**
* 获取web阅读界面配置
*/
fun getWebReadConfig(): ReturnData {
val returnData = ReturnData()
val data = CacheManager.get("webReadConfig")
?: return returnData.setErrorMsg("没有配置")
return returnData.setData(data)
}
}
@@ -0,0 +1,80 @@
package io.legato.kazusa.api.controller
import android.text.TextUtils
import io.legato.kazusa.api.ReturnData
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.help.source.SourceHelp
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonArray
import io.legato.kazusa.utils.fromJsonObject
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("数据不能为空")
val bookSource = GSON.fromJsonObject<BookSource>(postData).getOrNull()
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("转换源失败")
}
return returnData
}
fun saveSources(postData: String?): ReturnData {
postData ?: return ReturnData().setErrorMsg("数据为空")
val okSources = arrayListOf<BookSource>()
val bookSources = GSON.fromJsonArray<BookSource>(postData).getOrNull()
if (bookSources.isNullOrEmpty()) {
return ReturnData().setErrorMsg("转换源失败")
}
bookSources.forEach { bookSource ->
if (bookSource.bookSourceName.isNotBlank()
&& bookSource.bookSourceUrl.isNotBlank()
) {
appDb.bookSourceDao.insert(bookSource)
okSources.add(bookSource)
}
}
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).getOrThrow().let {
SourceHelp.deleteBookSources(it)
}
}.onFailure {
return ReturnData().setErrorMsg(it.localizedMessage ?: "数据格式错误")
}
return ReturnData().setData("已执行"/*okSources*/)
}
}
@@ -0,0 +1,97 @@
package io.legato.kazusa.api.controller
import io.legato.kazusa.api.ReturnData
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.replace
import io.legato.kazusa.utils.stackTraceStr
object ReplaceRuleController {
val allRules: ReturnData
get() {
val rules = appDb.replaceRuleDao.all
val returnData = ReturnData()
returnData.setData(GSON.toJson(rules))
return returnData
}
fun saveRule(postData: String?): ReturnData {
val returnData = ReturnData()
postData ?: return returnData.setErrorMsg("数据不能为空")
val rule = GSON.fromJsonObject<ReplaceRule>(postData).getOrNull()
if (rule == null) {
returnData.setErrorMsg("格式不对")
} else {
if (rule.order == Int.MIN_VALUE) {
rule.order = appDb.replaceRuleDao.maxOrder + 1
}
appDb.replaceRuleDao.insert(rule)
}
return returnData
}
fun delete(postData: String?): ReturnData {
val returnData = ReturnData()
postData ?: return returnData.setErrorMsg("数据不能为空")
val rule = GSON.fromJsonObject<ReplaceRule>(postData).getOrNull()
if (rule == null) {
returnData.setErrorMsg("格式不对")
} else {
appDb.replaceRuleDao.delete(rule)
}
return returnData
}
/**
* 传入测试数据格式
* {
* rule: Replace,
* text: "xxx"
* }
*/
fun testRule(postData: String?): ReturnData {
val returnData = ReturnData()
postData ?: return returnData.setErrorMsg("数据不能为空")
val map = GSON.fromJsonObject<Map<String, *>>(postData).getOrNull()
if (map == null) {
returnData.setErrorMsg("格式不对")
} else {
val rule = map["rule"]?.let {
if (it is String) {
GSON.fromJsonObject<ReplaceRule>(it).getOrNull()
} else {
GSON.fromJsonObject<ReplaceRule>(GSON.toJson(it)).getOrNull()
}
}
if (rule == null) {
returnData.setErrorMsg("格式不对")
return returnData
}
if (rule.pattern.isEmpty()) {
returnData.setErrorMsg("替换规则不能为空")
}
val text = map["text"] as String
val content = try {
if (rule.isRegex) {
text.replace(
rule.pattern.toRegex(),
rule.replacement,
rule.getValidTimeoutMillisecond()
)
} else {
text.replace(rule.pattern, rule.replacement)
}
} catch (e: Exception) {
e.stackTraceStr
}
returnData.setData(content)
}
return returnData
}
}
@@ -0,0 +1,77 @@
package io.legato.kazusa.api.controller
import android.text.TextUtils
import io.legato.kazusa.api.ReturnData
import io.legato.kazusa.data.appDb
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.help.source.SourceHelp
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonArray
import io.legato.kazusa.utils.fromJsonObject
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("数据不能为空")
GSON.fromJsonObject<RssSource>(postData).onFailure {
returnData.setErrorMsg("转换源失败${it.localizedMessage}")
}.onSuccess { source ->
if (TextUtils.isEmpty(source.sourceName) || TextUtils.isEmpty(source.sourceUrl)) {
returnData.setErrorMsg("源名称和URL不能为空")
} else {
appDb.rssSourceDao.insert(source)
returnData.setData("")
}
}
return returnData
}
fun saveSources(postData: String?): ReturnData {
postData ?: return ReturnData().setErrorMsg("数据不能为空")
val okSources = arrayListOf<RssSource>()
val source = GSON.fromJsonArray<RssSource>(postData).getOrNull()
if (source.isNullOrEmpty()) {
return ReturnData().setErrorMsg("转换源失败")
}
for (rssSource in source) {
if (rssSource.sourceName.isBlank() || rssSource.sourceUrl.isBlank()) {
continue
}
appDb.rssSourceDao.insert(rssSource)
okSources.add(rssSource)
}
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("没有传递数据")
GSON.fromJsonArray<RssSource>(postData).onFailure {
return ReturnData().setErrorMsg("格式不对")
}.onSuccess {
SourceHelp.deleteRssSources(it)
}
return ReturnData().setData("已执行"/*okSources*/)
}
}
@@ -0,0 +1,99 @@
package io.legato.kazusa.base
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import android.os.Build
import android.os.LocaleList
import io.legato.kazusa.constant.PreferKey
import io.legato.kazusa.utils.getPrefInt
import io.legato.kazusa.utils.getPrefString
import io.legato.kazusa.utils.sysConfiguration
import java.util.*
@Suppress("unused")
object AppContextWrapper {
@SuppressLint("ObsoleteSdkInt")
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
}
/**
* 当前系统语言
*/
@SuppressLint("ObsoleteSdkInt")
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语言
*/
@SuppressLint("ObsoleteSdkInt")
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,208 @@
package io.legato.kazusa.base
import android.annotation.SuppressLint
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.MotionEvent
import android.view.View
import android.widget.FrameLayout
import androidx.activity.addCallback
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AppCompatActivity
import androidx.viewbinding.ViewBinding
import com.google.android.material.appbar.AppBarLayout
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.Theme
import io.legato.kazusa.help.config.ThemeConfig
//import io.legado.app.lib.theme.backgroundColor
//import io.legado.app.lib.theme.primaryColor
import io.legato.kazusa.utils.applyOpenTint
import io.legato.kazusa.utils.applyTint
import io.legato.kazusa.utils.disableAutoFill
import io.legato.kazusa.utils.hideSoftInput
import io.legato.kazusa.utils.toastOnUi
import io.legato.kazusa.utils.windowSize
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() {
protected abstract val binding: VB
val isInMultiWindow: Boolean
@SuppressLint("ObsoleteSdkInt")
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)
}
@SuppressLint("ObsoleteSdkInt")
override fun onCreate(savedInstanceState: Bundle?) {
window.decorView.disableAutoFill()
//initTheme()
super.onCreate(savedInstanceState)
//setupSystemBar()
setContentView(binding.root)
upBackgroundImage()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
findViewById<AppBarLayout>(R.id.title_bar)
//?.onMultiWindowModeChanged(isInMultiWindowMode, fullScreen)
}
onBackPressedDispatcher.addCallback(this) {
finish()
}
observeLiveBus()
onActivityCreated(savedInstanceState)
}
@RequiresApi(Build.VERSION_CODES.O)
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()
}
abstract fun onActivityCreated(savedInstanceState: Bundle?)
final override fun onCreateOptionsMenu(menu: Menu): Boolean {
val bool = onCompatCreateOptionsMenu(menu)
menu.applyTint(this, toolBarTheme)
return 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)
open 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)
}
}
}
open fun upBackgroundImage() {
if (imageBg) {
try {
ThemeConfig.getBgImage(this, windowManager.windowSize)?.let {
window.decorView.background = BitmapDrawable(resources, it)
}
} catch (e: OutOfMemoryError) {
toastOnUi("背景图片太大,内存溢出")
} catch (e: Exception) {
AppLog.put("加载背景出错\n${e.localizedMessage}", e)
}
}
}
// open 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 dispatchTouchEvent(ev: MotionEvent): Boolean {
return try {
super.dispatchTouchEvent(ev)
} catch (e: IllegalArgumentException) {
e.printStackTrace()
false
}
}
override fun finish() {
currentFocus?.hideSoftInput()
super.finish()
}
}
@@ -0,0 +1,66 @@
package io.legato.kazusa.base
import android.content.DialogInterface
import android.content.DialogInterface.OnDismissListener
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
abstract class BaseBottomSheetDialogFragment(
@LayoutRes layoutID: Int
) : BottomSheetDialogFragment(layoutID) {
private var onDismissListener: OnDismissListener? = null
fun setOnDismissListener(onDismissListener: OnDismissListener?) {
this.onDismissListener = onDismissListener
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.N) {
// setStyle(STYLE_NO_TITLE, 0)
// }
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onFragmentCreated(view, savedInstanceState)
observeLiveBus()
}
abstract fun onFragmentCreated(view: View, savedInstanceState: Bundle?)
override fun show(manager: FragmentManager, tag: String?) {
kotlin.runCatching {
manager.beginTransaction().remove(this).commit()
super.show(manager, tag)
}.onFailure {
AppLog.put("显示BottomSheetDialog失败 tag:$tag\n$it")
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismissListener?.onDismiss(dialog)
}
fun <T> execute(
scope: CoroutineScope = lifecycleScope,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
) = Coroutine.async(scope, context) { block() }
open fun observeLiveBus() {
}
}
@@ -0,0 +1,103 @@
package io.legato.kazusa.base
import android.content.DialogInterface
import android.content.DialogInterface.OnDismissListener
import android.os.Bundle
import android.view.View
import androidx.annotation.LayoutRes
import androidx.fragment.app.DialogFragment
import androidx.fragment.app.FragmentManager
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.lib.theme.ThemeStore
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlin.coroutines.CoroutineContext
abstract class BaseDialogFragment(
@LayoutRes layoutID: Int,
private val adaptationSoftKeyboard: Boolean = false
) : DialogFragment(layoutID) {
private var onDismissListener: OnDismissListener? = null
fun setOnDismissListener(onDismissListener: OnDismissListener?) {
this.onDismissListener = onDismissListener
}
override fun onStart() {
super.onStart()
// if (adaptationSoftKeyboard) {
// dialog?.window?.setBackgroundDrawableResource(R.color.transparent)
// } else if (AppConfig.isEInkMode) {
// dialog?.window?.let {
// it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
// val attr = it.attributes
// attr.dimAmount = 0.0f
// attr.windowAnimations = 0
// it.attributes = attr
// it.decorView.setBackgroundKeepPadding(R.color.transparent)
// }
// // 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行.
// lifecycle.addObserver(LifecycleEventObserver { _, event ->
// if (event == Lifecycle.Event.ON_START) {
// when (dialog?.window?.attributes?.gravity) {
// Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom)
// Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top)
// else -> {
// val padding = 2.dpToPx();
// view?.setPadding(padding, padding, padding, padding)
// view?.setBackgroundResource(R.drawable.bg_eink_border_dialog)
// }
// }
// }
// })
// }
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
if (adaptationSoftKeyboard) {
view.findViewById<View>(R.id.vw_bg)?.setOnClickListener(null)
view.setOnClickListener { dismiss() }
} else if (!AppConfig.isEInkMode) {
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)
}.onFailure {
AppLog.put("显示对话框失败 tag:$tag", it)
}
}
override fun onDismiss(dialog: DialogInterface) {
super.onDismiss(dialog)
onDismissListener?.onDismiss(dialog)
}
fun <T> execute(
scope: CoroutineScope = lifecycleScope,
context: CoroutineContext = Dispatchers.IO,
block: suspend CoroutineScope.() -> T
) = Coroutine.async(scope, context) { block() }
open fun observeLiveBus() {
}
}
@@ -0,0 +1,76 @@
package io.legato.kazusa.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.legato.kazusa.utils.applyTint
@Suppress("MemberVisibilityCanBePrivate")
abstract class BaseFragment(@LayoutRes layoutID: Int) : Fragment(layoutID) {
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()
observeLiveBus()
onFragmentCreated(view, savedInstanceState)
}
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)
// }
}
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,39 @@
package io.legato.kazusa.base
import androidx.lifecycle.LifecycleEventObserver
import com.google.android.material.bottomsheet.BottomSheetDialogFragment
import io.legato.kazusa.help.config.AppConfig
abstract class BasePrefDialogFragment(
) : BottomSheetDialogFragment() {
override fun onStart() {
super.onStart()
if (AppConfig.isEInkMode) {
// dialog?.window?.let {
// it.clearFlags(WindowManager.LayoutParams.FLAG_DIM_BEHIND)
// val attr = it.attributes
// attr.dimAmount = 0.0f
// attr.windowAnimations = 0
// it.attributes = attr
// it.setBackgroundDrawableResource(R.color.transparent)
// }
// 修改gravity的时机一般在子类的onStart方法中, 因此需要在onStart之后执行.
lifecycle.addObserver(LifecycleEventObserver { _, event ->
// if (event == Lifecycle.Event.ON_START) {
// when (dialog?.window?.attributes?.gravity) {
// Gravity.TOP -> view?.setBackgroundResource(R.drawable.bg_eink_border_bottom)
// Gravity.BOTTOM -> view?.setBackgroundResource(R.drawable.bg_eink_border_top)
// else -> {
// val padding = 2.dpToPx();
// view?.setPadding(padding, padding, padding, padding)
// view?.setBackgroundResource(R.drawable.bg_eink_border_dialog)
// }
// }
// }
})
}
}
}
@@ -0,0 +1,107 @@
package io.legato.kazusa.base
import android.content.Intent
import android.os.Build
import android.os.IBinder
import androidx.annotation.CallSuper
import androidx.lifecycle.LifecycleService
import androidx.lifecycle.lifecycleScope
import io.legato.kazusa.R
import io.legato.kazusa.help.LifecycleHelp
import io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.lib.permission.Permissions
import io.legato.kazusa.lib.permission.PermissionsCompat
import io.legato.kazusa.utils.LogUtils
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.isActive
import kotlinx.coroutines.sync.Semaphore
import kotlin.coroutines.CoroutineContext
abstract class BaseService : LifecycleService() {
private val simpleName = this::class.simpleName.toString()
private var isForeground = false
fun <T> execute(
scope: CoroutineScope = lifecycleScope,
context: CoroutineContext = Dispatchers.IO,
start: CoroutineStart = CoroutineStart.DEFAULT,
executeContext: CoroutineContext = Dispatchers.Main,
semaphore: Semaphore? = null,
block: suspend CoroutineScope.() -> T
) = Coroutine.async(scope, context, start, executeContext, semaphore, block)
@CallSuper
override fun onCreate() {
super.onCreate()
LifecycleHelp.onServiceCreate(this)
checkPermission()
}
@CallSuper
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
LogUtils.d(simpleName) {
"onStartCommand $intent ${intent?.toUri(0)}"
}
if (!isForeground) {
startForegroundNotification()
isForeground = true
}
return super.onStartCommand(intent, flags, startId)
}
@CallSuper
override fun onTaskRemoved(rootIntent: Intent?) {
LogUtils.d(simpleName, "onTaskRemoved")
super.onTaskRemoved(rootIntent)
stopSelf()
}
override fun onBind(intent: Intent): IBinder? {
super.onBind(intent)
return null
}
@CallSuper
override fun onDestroy() {
super.onDestroy()
LifecycleHelp.onServiceDestroy(this)
}
@CallSuper
override fun onTimeout(startId: Int) {
super.onTimeout(startId)
LogUtils.d(simpleName, "onTimeout startId:$startId")
stopSelf()
}
/**
* 开启前台服务并发送通知
*/
open fun startForegroundNotification() {
}
/**
* 检测通知权限和后台权限
*/
private fun checkPermission() {
PermissionsCompat.Builder()
.addPermissions(Permissions.POST_NOTIFICATIONS)
.rationale(R.string.notification_permission_rationale)
.onGranted {
if (lifecycleScope.isActive) {
startForegroundNotification()
}
}
.request()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PermissionsCompat.Builder()
.addPermissions(Permissions.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS)
.rationale(R.string.ignore_battery_permission_rationale)
.request()
}
}
}
@@ -0,0 +1,52 @@
package io.legato.kazusa.base
import android.app.Application
import android.content.Context
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.viewModelScope
import io.legato.kazusa.App
import io.legato.kazusa.help.coroutine.Coroutine
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Semaphore
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,
start: CoroutineStart = CoroutineStart.DEFAULT,
executeContext: CoroutineContext = Dispatchers.Main,
semaphore: Semaphore? = null,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(scope, context, start, executeContext, semaphore, block)
}
fun <T> executeLazy(
scope: CoroutineScope = viewModelScope,
context: CoroutineContext = Dispatchers.IO,
executeContext: CoroutineContext = Dispatchers.Main,
semaphore: Semaphore? = null,
block: suspend CoroutineScope.() -> T
): Coroutine<T> {
return Coroutine.async(
scope, context, CoroutineStart.LAZY, executeContext, semaphore, 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.legato.kazusa.base
import androidx.lifecycle.ViewModel
import androidx.viewbinding.ViewBinding
import io.legato.kazusa.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.legato.kazusa.base
import androidx.lifecycle.ViewModel
abstract class VMBaseFragment<VM : ViewModel>(layoutID: Int) : BaseFragment(layoutID) {
protected abstract val viewModel: VM
}
@@ -0,0 +1,223 @@
package io.legato.kazusa.base.adapter
import android.content.Context
import android.os.Parcelable
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()
if (keepScrollPosition) {
layoutManager?.onRestoreInstanceState(layoutState)
layoutState = null
}
}
}
}
private var itemClickListener: ((holder: ItemViewHolder, item: ITEM) -> Unit)? = null
private var itemLongClickListener: ((holder: ItemViewHolder, item: ITEM) -> Boolean)? = null
private var layoutManager: RecyclerView.LayoutManager? = null
private var layoutState: Parcelable? = null
var itemAnimation: ItemAnimation? = null
abstract val diffItemCallback: DiffUtil.ItemCallback<ITEM>
open val keepScrollPosition = false
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
}
fun setItems(items: List<ITEM>?) {
kotlin.runCatching {
if (keepScrollPosition) {
layoutState = layoutManager?.onSaveInstanceState()
}
asyncListDiffer.submitList(items?.toMutableList())
}
}
fun setItem(position: Int, item: ITEM) {
kotlin.runCatching {
asyncListDiffer.currentList[position] = item
notifyItemChanged(position)
}
}
fun updateItem(item: ITEM) {
kotlin.runCatching {
val index = asyncListDiffer.currentList.indexOf(item)
if (index >= 0) {
asyncListDiffer.currentList[index] = item
notifyItemChanged(index)
}
}
}
fun updateItem(position: Int, payload: Any) {
kotlin.runCatching {
val size = itemCount
if (position in 0 until size) {
notifyItemChanged(position, payload)
}
}
}
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 {
return ItemViewHolder(getViewBinding(parent))
}
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>
) {
registerListener(holder, (holder.binding as VB))
registerItemListener(holder)
getItem(holder.layoutPosition)?.let {
convert(holder, holder.binding, it, payloads)
}
}
private fun registerItemListener(holder: ItemViewHolder) {
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)
}
}
}
}
override fun onViewAttachedToWindow(holder: ItemViewHolder) {
super.onViewAttachedToWindow(holder)
addAnimation(holder)
}
override fun onAttachedToRecyclerView(recyclerView: RecyclerView) {
super.onAttachedToRecyclerView(recyclerView)
val manager = recyclerView.layoutManager
layoutManager = manager
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.legato.kazusa.base.adapter
import android.view.animation.Interpolator
import android.view.animation.LinearInterpolator
import io.legato.kazusa.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.legato.kazusa.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,486 @@
package io.legato.kazusa.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 io.legato.kazusa.help.coroutine.Coroutine
import io.legato.kazusa.utils.buildMainHandler
import io.legato.kazusa.utils.withTimeoutOrNullAsync
import kotlinx.coroutines.ensureActive
import splitties.views.onLongClick
import java.util.Collections
/**
* 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
private var diffJob: Coroutine<*>? = 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>,
skipDiff: Boolean = false
) {
kotlin.runCatching {
val oldItems = this.items.toList()
val itemsSize = items?.size ?: 0
val headerCount = getHeaderCount()
val footerCount = getFooterCount()
val callback = object : DiffUtil.Callback() {
override fun getOldListSize(): Int {
return itemCount
}
override fun getNewListSize(): Int {
return itemsSize + headerCount + footerCount
}
override fun areItemsTheSame(oldItemPosition: Int, newItemPosition: Int): Boolean {
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
?: return true
val newItem = items?.getOrNull(newItemPosition - headerCount)
?: return true
return itemCallback.areItemsTheSame(oldItem, newItem)
}
override fun areContentsTheSame(
oldItemPosition: Int,
newItemPosition: Int
): Boolean {
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
?: return true
val newItem = items?.getOrNull(newItemPosition - headerCount)
?: return true
return itemCallback.areContentsTheSame(oldItem, newItem)
}
override fun getChangePayload(oldItemPosition: Int, newItemPosition: Int): Any? {
val oldItem = oldItems.getOrNull(oldItemPosition - headerCount)
?: return null
val newItem = items?.getOrNull(newItemPosition - headerCount)
?: return null
return itemCallback.getChangePayload(oldItem, newItem)
}
}
diffJob?.cancel()
diffJob = Coroutine.async {
val diffResult = if (skipDiff) withTimeoutOrNullAsync(500L) {
DiffUtil.calculateDiff(callback, itemsSize < 2000)
} else {
DiffUtil.calculateDiff(callback, itemsSize < 2000)
}
ensureActive()
handler.post {
if (diffResult == null) {
setItems(items)
return@post
}
if (this@RecyclerAdapter.items.isNotEmpty()) {
this@RecyclerAdapter.items.clear()
}
if (items != null) {
this@RecyclerAdapter.items.addAll(items)
}
diffResult.dispatchUpdatesTo(this@RecyclerAdapter)
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(getActualPosition(position))
fun getItems(): List<ITEM> = items.toList()
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 -> getItemByLayoutPosition(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 -> {
ItemViewHolder(getViewBinding(parent))
}
}
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)) {
registerListener(holder, (holder.binding as VB))
registerItemListener(holder)
getItemByLayoutPosition(holder.layoutPosition)?.let { item ->
convert(holder, holder.binding, item, payloads)
}
}
}
private fun registerItemListener(holder: ItemViewHolder) {
if (itemClickListener != null) {
holder.itemView.setOnClickListener {
getItemByLayoutPosition(holder.layoutPosition)?.let {
itemClickListener?.invoke(holder, it)
}
}
}
if (itemLongClickListener != null) {
holder.itemView.onLongClick {
getItemByLayoutPosition(holder.layoutPosition)?.let {
itemLongClickListener?.invoke(holder, it)
}
}
}
}
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
const val TYPE_FOOTER_VIEW = Int.MAX_VALUE - 999
private val handler by lazy { buildMainHandler() }
}
}
@@ -0,0 +1,18 @@
package io.legato.kazusa.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.legato.kazusa.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.legato.kazusa.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.legato.kazusa.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.legato.kazusa.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.legato.kazusa.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,111 @@
package io.legato.kazusa.constant
import android.annotation.SuppressLint
import android.content.pm.PackageManager
import android.provider.Settings
import androidx.annotation.Keep
import cn.hutool.crypto.digest.DigestUtil
import io.legato.kazusa.BuildConfig
import io.legato.kazusa.help.update.AppVariant
import org.apache.commons.lang3.time.FastDateFormat
import splitties.init.appCtx
@Suppress("ConstPropertyName")
@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
const val DEFAULT_WEBDAV_ID = -1L
private const val OFFICIAL_SIGNATURE =
"8DACBF25EC667C9B1374DB1450C1A866C2AAA1173016E80BF6AD2F06FABDDC08"
private const val BETA_SIGNATURE =
"93A28468B0F69E8D14C8A99AB45841CEF902BBBA3761BBFEE02E67CBA801563E"
val timeFormat: FastDateFormat by lazy {
FastDateFormat.getInstance("HH:mm")
}
val dateFormat: FastDateFormat by lazy {
FastDateFormat.getInstance("yyyy/MM/dd HH:mm")
}
val fileNameFormat: FastDateFormat by lazy {
FastDateFormat.getInstance("yy-MM-dd-HH-mm-ss")
}
const val imagePathKey = "imagePath"
val menuViewNames = arrayOf(
"com.android.internal.view.menu.ListMenuItemView",
"androidx.appcompat.view.menu.ListMenuItemView"
)
@SuppressLint("PrivateResource")
val sysElevation = appCtx.resources
.getDimension(com.google.android.material.R.dimen.design_appbar_elevation)
.toInt()
val androidId: String by lazy {
Settings.System.getString(appCtx.contentResolver, Settings.Secure.ANDROID_ID) ?: "null"
}
val appInfo: AppInfo by lazy {
val appInfo = AppInfo()
@Suppress("DEPRECATION")
appCtx.packageManager.getPackageInfo(appCtx.packageName, PackageManager.GET_ACTIVITIES)
?.let {
appInfo.versionName = it.versionName!!
appInfo.appVariant = when {
it.packageName.contains("releaseA") -> AppVariant.BETA_RELEASEA
isBeta -> AppVariant.BETA_RELEASE
isOfficial -> AppVariant.OFFICIAL
else -> AppVariant.UNKNOWN
}
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
}
@Suppress("DEPRECATION")
private val sha256Signature: String by lazy {
val packageInfo =
appCtx.packageManager.getPackageInfo(appCtx.packageName, PackageManager.GET_SIGNATURES)
DigestUtil.sha256Hex(packageInfo.signatures!![0].toByteArray()).uppercase()
}
private val isOfficial = sha256Signature == OFFICIAL_SIGNATURE
private val isBeta = sha256Signature == BETA_SIGNATURE || BuildConfig.DEBUG
val charsets =
arrayListOf("UTF-8", "GB2312", "GB18030", "GBK", "Unicode", "UTF-16", "UTF-16LE", "ASCII")
@Keep
data class AppInfo(
var versionCode: Long = 0L,
var versionName: String = "",
var appVariant: AppVariant = AppVariant.UNKNOWN
)
/**
* 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,64 @@
package io.legato.kazusa.constant
import android.util.Log
import io.legato.kazusa.BuildConfig
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.utils.LogUtils
import io.legato.kazusa.utils.toastOnUi
import splitties.init.appCtx
object AppLog {
private val mLogs = arrayListOf<Triple<Long, String, Throwable?>>()
val logs get() = mLogs.toList()
@Synchronized
fun put(message: String?, throwable: Throwable? = null, toast: Boolean = false) {
message ?: return
if (toast) {
appCtx.toastOnUi(message)
}
if (mLogs.size > 100) {
mLogs.removeLastOrNull()
}
if (throwable == null) {
LogUtils.d("AppLog", message)
} else {
LogUtils.d("AppLog", "$message\n${throwable.stackTraceToString()}")
}
mLogs.add(0, Triple(System.currentTimeMillis(), message, throwable))
if (BuildConfig.DEBUG) {
val stackTrace = Thread.currentThread().stackTrace
Log.e(stackTrace[3].className, message, throwable)
}
}
@Synchronized
fun putNotSave(message: String?, throwable: Throwable? = null, toast: Boolean = false) {
message ?: return
if (toast) {
appCtx.toastOnUi(message)
}
if (mLogs.size > 100) {
mLogs.removeLastOrNull()
}
mLogs.add(0, Triple(System.currentTimeMillis(), message, throwable))
if (BuildConfig.DEBUG) {
val stackTrace = Thread.currentThread().stackTrace
Log.e(stackTrace[3].className, message, throwable)
}
}
@Synchronized
fun clear() {
mLogs.clear()
}
fun putDebug(message: String?, throwable: Throwable? = null) {
if (AppConfig.recordLog) {
put(message, throwable)
}
}
}
@@ -0,0 +1,57 @@
package io.legato.kazusa.constant
import java.util.regex.Pattern
@Suppress("RegExpRedundantEscape", "unused")
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=\"([^\"]*(?:\"[^>]+\\})?)\"[^>]*>")
//dataURL图片类型
val dataUriRegex = Regex("^data:.*?;base64,(.*)")
val nameRegex = Regex("\\s+作\\s*者.*|\\s+\\S+\\s+著")
val authorRegex = Regex("^\\s*作\\s*者[::\\s]+|\\s+著")
val fileNameRegex = Regex("[\\\\/:*?\"<>|.]")
val splitGroupRegex = Regex("[,;,;]")
val titleNumPattern: Pattern = Pattern.compile("(第)(.+?)(章)")
//书源调试信息中的各种符号
val debugMessageSymbolRegex = Regex("[⇒◇┌└≡]")
//本地书籍支持类型
val bookFileRegex = Regex(".*\\.(txt|epub|umd|pdf|mobi|azw3|azw)", RegexOption.IGNORE_CASE)
//压缩文件支持类型
val archiveFileRegex = Regex(".*\\.(zip|rar|7z)$", RegexOption.IGNORE_CASE)
/**
* 所有标点
*/
val bdRegex = Regex("(\\p{P})+")
/**
* 换行
*/
val rnRegex = Regex("[\\r\\n]")
/**
* 不发音段落判断
*/
val notReadAloudRegex = Regex("^(\\s|\\p{C}|\\p{P}|\\p{Z}|\\p{S})+$")
val xmlContentTypeRegex = "(application|text)/\\w*\\+?xml.*".toRegex()
val semicolonRegex = ";".toRegex()
val equalsRegex = "=".toRegex()
val spaceRegex = "\\s+".toRegex()
val regexCharRegex = "[{}()\\[\\].+*?^$\\\\|]".toRegex()
val LFRegex = "\n".toRegex()
}
@@ -0,0 +1,18 @@
package io.legato.kazusa.constant
import androidx.annotation.IntDef
@Suppress("ConstPropertyName")
object BookSourceType {
const val default = 0 // 0 文本
const val audio = 1 // 1 音频
const val image = 2 // 2 图片
const val file = 3 // 3 只提供下载服务的网站
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@IntDef(default, audio, image, file)
annotation class Type
}
@@ -0,0 +1,73 @@
package io.legato.kazusa.constant
import androidx.annotation.IntDef
/**
* 以二进制位来区分,可能一本书籍包含多个类型,每一位代表一个类型,数值为2的n次方
* 以二进制位来区分,数据库查询更高效, 数值>=8和老版本类型区分开
*/
@Suppress("ConstPropertyName")
object BookType {
/**
* 8 文本
*/
const val text = 0b1000
/**
* 16 更新失败
*/
const val updateError = 0b10000
/**
* 32 音频
*/
const val audio = 0b100000
/**
* 64 图片
*/
const val image = 0b1000000
/**
* 128 只提供下载服务的网站
*/
const val webFile = 0b10000000
/**
* 256 本地
*/
const val local = 0b100000000
/**
* 512 压缩包 表明书籍文件是从压缩包内解压来的
*/
const val archive = 0b1000000000
/**
* 1024 未正式加入到书架的临时阅读书籍
*/
const val notShelf = 0b100_0000_0000
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@IntDef(text, updateError, audio, image, webFile, local, archive, notShelf)
annotation class Type
/**
* 所有可以从书源转换的书籍类型
*/
const val allBookType = text or image or audio or webFile
const val allBookTypeLocal = text or image or audio or webFile or local
/**
* 本地书籍书源标志
*/
const val localTag = "loc_book"
/**
* 书源已webDav::开头的书籍,可以从webDav更新或重新下载
*/
const val webDavTag = "webDav::"
}
@@ -0,0 +1,35 @@
package io.legato.kazusa.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 AUDIO_DS = "audioDs"
const val READ_ALOUD_DS = "readAloudDs"
const val BATTERY_CHANGED = "batteryChanged"
const val TIME_CHANGED = "timeChanged"
const val UP_CONFIG = "upConfig"
const val AUDIO_SUB_TITLE = "audioSubTitle"
const val AUDIO_STATE = "audioState"
const val AUDIO_PROGRESS = "audioProgress"
const val AUDIO_BUFFER_PROGRESS = "audioBufferProgress"
const val AUDIO_SIZE = "audioSize"
const val AUDIO_SPEED = "audioSpeed"
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_DONE = "checkSourceDone"
const val TIP_COLOR = "tipColor"
const val SOURCE_CHANGED = "sourceChanged"
const val SEARCH_RESULT = "searchResult"
const val UPDATE_READ_ACTION_BAR = "updateReadActionBar"
const val UP_SEEK_BAR = "upSeekBar"
const val READ_ALOUD_PLAY = "readAloudPlay"
const val EXPORT_BOOK = "exportBook"
const val UP_MANGA_CONFIG = "upMangaConfig"
}
@@ -0,0 +1,25 @@
package io.legato.kazusa.constant
@Suppress("ConstPropertyName")
object IntentAction {
const val start = "start"
const val play = "play"
const val playNew = "playNew"
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 upTtsProgress = "upTtsProgress"
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"
const val stopPlay = "stopPlay"
}
@@ -0,0 +1,19 @@
package io.legato.kazusa.constant
/**
* 通知ID不能重复,统一规划通知ID
*/
@Suppress("ConstPropertyName")
object NotificationId {
const val ReadAloudService = 101
const val AudioPlayService = 102
const val CacheBookService = 103
const val ExportBookService = 104
const val WebService = 105
const val DownloadService = 106
const val CheckSourceService = 107
const val Download = 10000
const val ExportBook = 201
}
@@ -0,0 +1,23 @@
package io.legato.kazusa.constant
import androidx.annotation.IntDef
@Suppress("ConstPropertyName")
object PageAnim {
const val coverPageAnim = 0
const val slidePageAnim = 1
const val simulationPageAnim = 2
const val scrollPageAnim = 3
const val noAnim = 4
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@IntDef(coverPageAnim, slidePageAnim, simulationPageAnim, scrollPageAnim, noAnim)
annotation class Anim
}
@@ -0,0 +1,183 @@
package io.legato.kazusa.constant
@Suppress("ConstPropertyName")
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 loadCoverOnlyWifi = "loadCoverOnlyWifi"
const val coverShowName = "coverShowName"
const val coverShowAuthor = "coverShowAuthor"
const val coverShowNameN = "coverShowNameN"
const val coverShowAuthorN = "coverShowAuthorN"
const val remoteServerId = "remoteServerId"
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 ttsFollowSys = "ttsFollowSys"
const val ttsSpeechRate = "ttsSpeechRate"
const val prevKeys = "prevKeyCodes"
const val nextKeys = "nextKeyCodes"
const val showDiscovery = "showDiscovery"
const val enableReview = "enableReview"
const val showRss = "showRss"
const val bookshelfLayout = "bookshelfLayout"
const val bookshelfSort = "bookshelfSort"
const val bookExportFileName = "bookExportFileName"
const val bookImportFileName = "bookImportFileName"
const val episodeExportFileName = "episodeExportFileName"
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 webDavDir = "webDavDir"
const val enableCustomExport = "enableCustomExport"
const val exportToWebDav = "webDavCacheBackup"
const val exportNoChapterName = "exportNoChapterName"
const val exportType = "exportType"
const val exportPictureFile = "exportPictureFile"
const val changeSourceCheckAuthor = "changeSourceCheckAuthor"
const val changeSourceLoadToc = "changeSourceLoadToc"
const val changeSourceLoadInfo = "changeSourceLoadInfo"
const val changeSourceLoadWordCount = "changeSourceLoadWordCount"
const val chineseConverterType = "chineseConverterType"
const val launcherIcon = "launcherIcon"
const val textSelectAble = "selectText"
const val shareLayout = "shareLayout"
const val comicStyleSelect = "comicStyleSelect"
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 importKeepGroup = "importKeepGroup"
const val screenOrientation = "screenOrientation"
const val syncBookProgress = "syncBookProgress"
const val syncBookProgressPlus = "syncBookProgressPlus"
const val cronet = "Cronet"
const val antiAlias = "antiAlias"
const val bitmapCacheSize = "bitmapCacheSize"
const val imageRetainNum = "imageRetainNum"
const val preDownloadNum = "preDownloadNum"
const val mangaPreDownloadNum = "mangaPreDownloadNum"
const val mangaAutoPageSpeed = "mangaAutoPageSpeed"
const val mangaFooterConfig = "mangaFooterConfig"
const val disableClickScroll = "disableClickScroll"
const val enableMangaHorizontalScroll = "enableMangaHorizontalScroll"
const val hideMangaTitle = "hideMangaTitle"
const val mangaColorFilter = "mangaColorFilter"
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 = "doubleHorizontalPage"
const val readUrlOpenInBrowser = "readUrlInBrowser"
const val defaultBookTreeUri = "defaultBookTreeUri"
const val checkSource = "checkSource"
const val uploadRule = "uploadRule"
const val tocUiUseReplace = "tocUiUseReplace"
const val tocCountWords = "tocCountWords"
const val enableReadRecord = "enableReadRecord"
const val localBookImportSort = "localBookImportSort"
const val customWelcome = "customWelcome"
const val welcomeImage = "welcomeImagePath"
const val welcomeImageDark = "welcomeImagePathDark"
const val welcomeShowText = "welcomeShowText"
const val welcomeShowTextDark = "welcomeShowTextDark"
const val welcomeShowIcon = "welcomeShowIcon"
const val welcomeShowIconDark = "welcomeShowIconDark"
const val pageTouchSlop = "pageTouchSlop"
const val showAddToShelfAlert = "showAddToShelfAlert"
const val ignoreAudioFocus = "ignoreAudioFocus"
const val parallelExportBook = "parallelExportBook"
const val progressBarBehavior = "progressBarBehavior"
const val sourceEditMaxLine = "sourceEditMaxLine"
const val ttsTimer = "ttsTimer"
const val noAnimScrollPage = "noAnimScrollPage"
const val webDavDeviceName = "webDavDeviceName"
const val webServiceWakeLock = "webServiceWakeLock"
const val audioPlayWakeLock = "audioPlayWakeLock"
const val readAloudWakeLock = "readAloudWakeLock"
const val showLastUpdateTime = "showLastUpdateTime"
const val showWaitUpCount = "showWaitUpCount"
const val clearWebViewData = "clearWebViewData"
const val onlyLatestBackup = "onlyLatestBackup"
const val brightnessVwPos = "brightnessVwPos"
const val shrinkDatabase = "shrinkDatabase"
const val batchChangeSourceDelay = "batchChangeSourceDelay"
const val openBookInfoByClickTitle = "openBookInfoByClickTitle"
const val defaultHomePage = "defaultHomePage"
const val showBookshelfFastScroller = "showBookshelfFastScroller"
const val importKeepEnable = "importKeepEnable"
const val previewImageByClick = "previewImageByClick"
const val keyPageOnLongPress = "keyPageOnLongPress"
const val volumeKeyPage = "volumeKeyPage"
const val volumeKeyPageOnPlay = "volumeKeyPageOnPlay"
const val mouseWheelPage = "mouseWheelPage"
const val recordHeapDump = "recordHeapDump"
const val optimizeRender = "optimizeRender"
const val updateToVariant = "updateToVariant"
const val streamReadAloudAudio = "streamReadAloudAudio"
const val pauseReadAloudWhilePhoneCalls = "pauseReadAloudWhilePhoneCalls"
const val readAloudByMediaButton = "readAloudByMediaButton"
const val showMangaUi = "showMangaUi"
const val disableMangaScale = "disableMangaScale"
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"
const val showReadTitleAddition = "showReadTitleAddition"
const val readBarStyleFollowPage = "readBarStyleFollowPage"
const val contentSelectSpeakMod = "contentReadAloudMod"
}
@@ -0,0 +1,16 @@
package io.legato.kazusa.constant
import androidx.annotation.IntDef
@Suppress("ConstPropertyName")
object SourceType {
const val book = 0
const val rss = 1
@Target(AnnotationTarget.VALUE_PARAMETER)
@Retention(AnnotationRetention.SOURCE)
@IntDef(book, rss)
annotation class Type
}
@@ -0,0 +1,7 @@
package io.legato.kazusa.constant
object Status {
const val STOP = 0
const val PLAY = 1
const val PAUSE = 3
}
@@ -0,0 +1,5 @@
package io.legato.kazusa.constant
enum class Theme {
Dark, Light, Auto, Transparent, EInk;
}
@@ -0,0 +1,230 @@
package io.legato.kazusa.data
import android.content.ContentValues
import android.database.sqlite.SQLiteDatabase
import androidx.room.AutoMigration
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legato.kazusa.data.dao.BookChapterDao
import io.legato.kazusa.data.dao.BookDao
import io.legato.kazusa.data.dao.BookGroupDao
import io.legato.kazusa.data.dao.BookSourceDao
import io.legato.kazusa.data.dao.BookmarkDao
import io.legato.kazusa.data.dao.CacheDao
import io.legato.kazusa.data.dao.CookieDao
import io.legato.kazusa.data.dao.DictRuleDao
import io.legato.kazusa.data.dao.HttpTTSDao
import io.legato.kazusa.data.dao.KeyboardAssistsDao
import io.legato.kazusa.data.dao.ReadRecordDao
import io.legato.kazusa.data.dao.ReplaceRuleDao
import io.legato.kazusa.data.dao.RssArticleDao
import io.legato.kazusa.data.dao.RssReadRecordDao
import io.legato.kazusa.data.dao.RssSourceDao
import io.legato.kazusa.data.dao.RssStarDao
import io.legato.kazusa.data.dao.RuleSubDao
import io.legato.kazusa.data.dao.SearchBookDao
import io.legato.kazusa.data.dao.SearchKeywordDao
import io.legato.kazusa.data.dao.ServerDao
import io.legato.kazusa.data.dao.TxtTocRuleDao
import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookChapter
import io.legato.kazusa.data.entities.BookGroup
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.BookSourcePart
import io.legato.kazusa.data.entities.Bookmark
import io.legato.kazusa.data.entities.Cache
import io.legato.kazusa.data.entities.Cookie
import io.legato.kazusa.data.entities.DictRule
import io.legato.kazusa.data.entities.HttpTTS
import io.legato.kazusa.data.entities.KeyboardAssist
import io.legato.kazusa.data.entities.ReadRecord
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.data.entities.RssArticle
import io.legato.kazusa.data.entities.RssReadRecord
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.data.entities.RssStar
import io.legato.kazusa.data.entities.RuleSub
import io.legato.kazusa.data.entities.SearchBook
import io.legato.kazusa.data.entities.SearchKeyword
import io.legato.kazusa.data.entities.Server
import io.legato.kazusa.data.entities.TxtTocRule
import io.legato.kazusa.help.DefaultData
import org.intellij.lang.annotations.Language
import splitties.init.appCtx
import java.util.Locale
val appDb by lazy {
Room.databaseBuilder(appCtx, AppDatabase::class.java, AppDatabase.DATABASE_NAME)
.fallbackToDestructiveMigrationFrom(false, 1, 2, 3, 4, 5, 6, 7, 8, 9)
.addMigrations(*DatabaseMigrations.migrations)
.allowMainThreadQueries()
.addCallback(AppDatabase.dbCallback)
.build()
}
@Database(
version = 75,
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, DictRule::class, KeyboardAssist::class, Server::class],
views = [BookSourcePart::class],
autoMigrations = [
AutoMigration(from = 43, to = 44),
AutoMigration(from = 44, to = 45),
AutoMigration(from = 45, to = 46),
AutoMigration(from = 46, to = 47),
AutoMigration(from = 47, to = 48),
AutoMigration(from = 48, to = 49),
AutoMigration(from = 49, to = 50),
AutoMigration(from = 50, to = 51),
AutoMigration(from = 51, to = 52),
AutoMigration(from = 52, to = 53),
AutoMigration(from = 53, to = 54),
AutoMigration(from = 54, to = 55, spec = DatabaseMigrations.Migration_54_55::class),
AutoMigration(from = 55, to = 56),
AutoMigration(from = 56, to = 57),
AutoMigration(from = 57, to = 58),
AutoMigration(from = 58, to = 59),
AutoMigration(from = 59, to = 60),
AutoMigration(from = 60, to = 61),
AutoMigration(from = 61, to = 62),
AutoMigration(from = 62, to = 63),
AutoMigration(from = 63, to = 64),
AutoMigration(from = 64, to = 65, spec = DatabaseMigrations.Migration_64_65::class),
AutoMigration(from = 65, to = 66),
AutoMigration(from = 66, to = 67),
AutoMigration(from = 67, to = 68),
AutoMigration(from = 68, to = 69),
AutoMigration(from = 69, to = 70),
AutoMigration(from = 70, to = 71),
AutoMigration(from = 71, to = 72),
AutoMigration(from = 72, to = 73),
AutoMigration(from = 73, to = 74),
AutoMigration(from = 74, to = 75),
]
)
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 rssReadRecordDao: RssReadRecordDao
abstract val cookieDao: CookieDao
abstract val txtTocRuleDao: TxtTocRuleDao
abstract val readRecordDao: ReadRecordDao
abstract val httpTTSDao: HttpTTSDao
abstract val cacheDao: CacheDao
abstract val ruleSubDao: RuleSubDao
abstract val dictRuleDao: DictRuleDao
abstract val keyboardAssistsDao: KeyboardAssistsDao
abstract val serverDao: ServerDao
companion object {
const val DATABASE_NAME = "legado.db"
const val BOOK_TABLE_NAME = "books"
const val BOOK_SOURCE_TABLE_NAME = "book_sources"
const val RSS_SOURCE_TABLE_NAME = "rssSources"
val dbCallback = object : Callback() {
override fun onCreate(db: SupportSQLiteDatabase) {
db.setLocale(Locale.CHINESE)
}
override fun onOpen(db: SupportSQLiteDatabase) {
@Language("sql")
val insertBookGroupAllSql = """
insert into book_groups(groupId, groupName, 'order', show)
select ${BookGroup.IdAll}, '全部', -10, 1
where not exists (select * from book_groups where groupId = ${BookGroup.IdAll})
""".trimIndent()
db.execSQL(insertBookGroupAllSql)
@Language("sql")
val insertBookGroupLocalSql = """
insert into book_groups(groupId, groupName, 'order', enableRefresh, show)
select ${BookGroup.IdLocal}, '本地', -9, 0, 1
where not exists (select * from book_groups where groupId = ${BookGroup.IdLocal})
""".trimIndent()
db.execSQL(insertBookGroupLocalSql)
@Language("sql")
val insertBookGroupMusicSql = """
insert into book_groups(groupId, groupName, 'order', show)
select ${BookGroup.IdAudio}, '音频', -8, 1
where not exists (select * from book_groups where groupId = ${BookGroup.IdAudio})
""".trimIndent()
db.execSQL(insertBookGroupMusicSql)
@Language("sql")
val insertBookGroupNetNoneGroupSql = """
insert into book_groups(groupId, groupName, 'order', show)
select ${BookGroup.IdNetNone}, '网络未分组', -7, 1
where not exists (select * from book_groups where groupId = ${BookGroup.IdNetNone})
""".trimIndent()
db.execSQL(insertBookGroupNetNoneGroupSql)
@Language("sql")
val insertBookGroupLocalNoneGroupSql = """
insert into book_groups(groupId, groupName, 'order', show)
select ${BookGroup.IdLocalNone}, '本地未分组', -6, 0
where not exists (select * from book_groups where groupId = ${BookGroup.IdLocalNone})
""".trimIndent()
db.execSQL(insertBookGroupLocalNoneGroupSql)
@Language("sql")
val insertBookGroupErrorSql = """
insert into book_groups(groupId, groupName, 'order', show)
select ${BookGroup.IdError}, '更新失败', -1, 1
where not exists (select * from book_groups where groupId = ${BookGroup.IdError})
""".trimIndent()
db.execSQL(insertBookGroupErrorSql)
@Language("sql")
val upBookSourceLoginUiSql =
"update book_sources set loginUi = null where loginUi = 'null'"
db.execSQL(upBookSourceLoginUiSql)
@Language("sql")
val upRssSourceLoginUiSql =
"update rssSources set loginUi = null where loginUi = 'null'"
db.execSQL(upRssSourceLoginUiSql)
@Language("sql")
val upHttpTtsLoginUiSql =
"update httpTTS set loginUi = null where loginUi = 'null'"
db.execSQL(upHttpTtsLoginUiSql)
@Language("sql")
val upHttpTtsConcurrentRateSql =
"update httpTTS set concurrentRate = '0' where concurrentRate is null"
db.execSQL(upHttpTtsConcurrentRateSql)
db.query("select * from keyboardAssists order by serialNo").use {
if (it.count == 0) {
DefaultData.keyboardAssists.forEach { keyboardAssist ->
val contentValues = ContentValues().apply {
put("type", keyboardAssist.type)
put("key", keyboardAssist.key)
put("value", keyboardAssist.value)
put("serialNo", keyboardAssist.serialNo)
}
db.insert(
"keyboardAssists",
SQLiteDatabase.CONFLICT_REPLACE,
contentValues
)
}
}
}
}
}
}
}
@@ -0,0 +1,374 @@
package io.legato.kazusa.data
import androidx.room.DeleteColumn
import androidx.room.migration.AutoMigrationSpec
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.BookSourceType
import io.legato.kazusa.constant.BookType
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, migration_42_43,
)
}
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")
}
}
private val migration_42_43 = object : Migration(42, 43) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE `chapters` ADD `isVolume` INTEGER NOT NULL DEFAULT 0")
}
}
@Suppress("ClassName")
class Migration_54_55 : AutoMigrationSpec {
override fun onPostMigrate(db: SupportSQLiteDatabase) {
db.execSQL(
"""
update books set type = ${BookType.audio}
where type = ${BookSourceType.audio}
""".trimIndent()
)
db.execSQL(
"""
update books set type = ${BookType.image}
where type = ${BookSourceType.image}
""".trimIndent()
)
db.execSQL(
"""
update books set type = ${BookType.webFile}
where type = ${BookSourceType.file}
""".trimIndent()
)
db.execSQL(
"""
update books set type = ${BookType.text}
where type = ${BookSourceType.default}
""".trimIndent()
)
db.execSQL(
"""
update books set type = type | ${BookType.local}
where origin like '${BookType.localTag}%' or origin like '${BookType.webDavTag}%'
""".trimIndent()
)
}
}
@Suppress("ClassName")
@DeleteColumn(
tableName = "book_sources",
columnName = "enabledReview"
)
class Migration_64_65 : AutoMigrationSpec
}
@@ -0,0 +1,18 @@
# 存储数据用
* 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,46 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legato.kazusa.data.entities.BookChapter
@Dao
interface BookChapterDao {
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and title like '%'||:key||'%' order by `index`")
fun search(bookUrl: String, key: String): List<BookChapter>
@Query("SELECT * FROM chapters where bookUrl = :bookUrl and `index` >= :start and `index` <= :end and title like '%'||:key||'%' order by `index`")
fun search(bookUrl: String, key: String, start: Int, end: Int): 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)
@Query("update chapters set wordCount = :wordCount where bookUrl = :bookUrl and url = :url")
fun upWordCount(bookUrl: String, url: String, wordCount: String)
}
@@ -0,0 +1,164 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legato.kazusa.constant.BookType
import io.legato.kazusa.data.entities.Book
import io.legato.kazusa.data.entities.BookGroup
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.help.book.isNotShelf
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
@Dao
interface BookDao {
fun flowByGroup(groupId: Long): Flow<List<Book>> {
return when (groupId) {
BookGroup.IdRoot -> flowRoot()
BookGroup.IdAll -> flowAll()
BookGroup.IdLocal -> flowLocal()
BookGroup.IdAudio -> flowAudio()
BookGroup.IdNetNone -> flowNetNoGroup()
BookGroup.IdLocalNone -> flowLocalNoGroup()
BookGroup.IdError -> flowUpdateError()
else -> flowByUserGroup(groupId)
}.map { list ->
list.filterNot { it.isNotShelf }
}
}
@Query(
"""
select * from books where type & ${BookType.text} > 0
and type & ${BookType.local} = 0
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
and (select show from book_groups where groupId = ${BookGroup.IdNetNone}) != 1
"""
)
fun flowRoot(): Flow<List<Book>>
@Query("SELECT * FROM books order by durChapterTime desc")
fun flowAll(): Flow<List<Book>>
@Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0")
fun flowAudio(): Flow<List<Book>>
@Query("SELECT * FROM books WHERE type & ${BookType.local} > 0")
fun flowLocal(): Flow<List<Book>>
@Query(
"""
select * from books where type & ${BookType.audio} = 0 and type & ${BookType.local} = 0
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
"""
)
fun flowNetNoGroup(): Flow<List<Book>>
@Query(
"""
select * from books where type & ${BookType.local} > 0
and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
"""
)
fun flowLocalNoGroup(): Flow<List<Book>>
@Query("SELECT * FROM books WHERE (`group` & :group) > 0")
fun flowByUserGroup(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 type & ${BookType.updateError} > 0 order by durChapterTime desc")
fun flowUpdateError(): 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 originName = :fileName")
fun getBookByFileName(fileName: String): 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?
@Query("""select distinct bs.* from books, book_sources bs
where origin == bookSourceUrl and origin not like '${BookType.localTag}%'
and origin not like '${BookType.webDavTag}%'""")
fun getAllUseBookSource(): List<BookSource>
@Query("SELECT * FROM books WHERE name = :name and origin = :origin")
fun getBookByOrigin(name: String, origin: String): Book?
@get:Query("select count(bookUrl) from books where (SELECT sum(groupId) FROM book_groups)")
val noGroupSize: Int
@get:Query("SELECT * FROM books where type & ${BookType.local} = 0")
val webBooks: List<Book>
@get:Query("SELECT * FROM books where type & ${BookType.local} = 0 and canUpdate = 1")
val hasUpdateBooks: List<Book>
@get:Query("SELECT * FROM books")
val all: List<Book>
@Query("SELECT * FROM books where type & :type > 0 and type & ${BookType.local} = 0")
fun getByTypeOnLine(type: Int): List<Book>
@get:Query("SELECT * FROM books where type & ${BookType.text} > 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 exists(select 1 from books where bookUrl = :bookUrl)")
fun has(bookUrl: String): Boolean
@Query("select exists(select 1 from books where name = :name and author = :author)")
fun has(name: String, author: String): Boolean
@Query(
"""select exists(select 1 from books where type & ${BookType.local} > 0
and (originName = :fileName or (origin != '${BookType.localTag}' and origin like '%' || :fileName)))"""
)
fun hasFile(fileName: 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)
@Query("update books set `group` = `group` - :group where `group` & :group > 0")
fun removeGroup(group: Long)
@Query("delete from books where type & ${BookType.notShelf} > 0")
fun deleteNotShelfBook()
}
@@ -0,0 +1,103 @@
package io.legato.kazusa.data.dao
import androidx.lifecycle.LiveData
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legato.kazusa.constant.BookType
import io.legato.kazusa.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(
"""
with const as (SELECT sum(groupId) sumGroupId FROM book_groups where groupId > 0)
SELECT book_groups.* FROM book_groups join const
where show > 0
and (
(groupId >= 0 and exists (select 1 from books where `group` & book_groups.groupId > 0))
or groupId = -1
or (groupId = -2 and exists (select 1 from books where type & ${BookType.local} > 0))
or (groupId = -3 and exists (select 1 from books where type & ${BookType.audio} > 0))
or (groupId = -11 and exists (select 1 from books where type & ${BookType.updateError} > 0))
or (groupId = -4
and exists (
select 1 from books
where type & ${BookType.audio} = 0
and type & ${BookType.local} = 0
and const.sumGroupId & `group` = 0
)
)
or (groupId = -5
and exists (
select 1 from books
where type & ${BookType.audio} = 0
and type & ${BookType.local} > 0
and const.sumGroupId & `group` = 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>
@get:Query("select count(*) < 64 from book_groups where groupId >= 0 or groupId == ${Long.MIN_VALUE}")
val canAddGroup: Boolean
@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,372 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Transaction
import androidx.room.Update
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.data.entities.BookSource
import io.legato.kazusa.data.entities.BookSourcePart
import io.legato.kazusa.utils.cnCompare
import io.legato.kazusa.utils.splitNotBlank
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@Dao
interface BookSourceDao {
@Query("select * from book_sources_part order by customOrder asc")
fun flowAll(): Flow<List<BookSourcePart>>
@Query(
"""select bp.*
from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl
where b.bookSourceName like '%' || :searchKey || '%'
or b.bookSourceGroup like '%' || :searchKey || '%'
or b.bookSourceUrl like '%' || :searchKey || '%'
or b.bookSourceComment like '%' || :searchKey || '%'
order by b.customOrder asc"""
)
fun flowSearch(searchKey: String): Flow<List<BookSourcePart>>
@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 search(searchKey: String): List<BookSource>
@Query(
"""select bp.*
from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl
where b.enabled = 1
and (b.bookSourceName like '%' || :searchKey || '%'
or b.bookSourceGroup like '%' || :searchKey || '%'
or b.bookSourceUrl like '%' || :searchKey || '%'
or b.bookSourceComment like '%' || :searchKey || '%')
order by b.customOrder asc"""
)
fun flowSearchEnabled(searchKey: String): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources_part
where bookSourceGroup = :searchKey
or bookSourceGroup like :searchKey || ',%'
or bookSourceGroup like '%,' || :searchKey
or bookSourceGroup like '%,' || :searchKey || ',%'
order by customOrder asc"""
)
fun flowGroupSearch(searchKey: String): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources
where bookSourceGroup = :searchKey
or bookSourceGroup like :searchKey || ',%'
or bookSourceGroup like '%,' || :searchKey
or bookSourceGroup like '%,' || :searchKey || ',%'
order by customOrder asc"""
)
fun groupSearch(searchKey: String): List<BookSource>
@Query("select * from book_sources_part where enabled = 1 order by customOrder asc")
fun flowEnabled(): Flow<List<BookSourcePart>>
@Query("select * from book_sources_part where enabled = 0 order by customOrder asc")
fun flowDisabled(): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources_part
where enabledExplore = 1 and hasExploreUrl = 1 order by customOrder asc"""
)
fun flowExplore(): Flow<List<BookSourcePart>>
@Query("select * from book_sources_part where hasLoginUrl = 1 order by customOrder asc")
fun flowLogin(): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources_part
where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'
order by customOrder asc"""
)
fun flowNoGroup(): Flow<List<BookSourcePart>>
@Query("select * from book_sources_part where enabledExplore = 1 order by customOrder asc")
fun flowEnabledExplore(): Flow<List<BookSourcePart>>
@Query("select * from book_sources_part where enabledExplore = 0 order by customOrder asc")
fun flowDisabledExplore(): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources_part
where enabledExplore = 1
and hasExploreUrl = 1
and (bookSourceGroup like '%' || :key || '%'
or bookSourceName like '%' || :key || '%')
order by customOrder asc"""
)
fun flowExplore(key: String): Flow<List<BookSourcePart>>
@Query(
"""select * from book_sources_part
where enabledExplore = 1
and hasExploreUrl = 1
and (bookSourceGroup = :key
or bookSourceGroup like :key || ',%'
or bookSourceGroup like '%,' || :key
or bookSourceGroup like '%,' || :key || ',%')
order by customOrder asc"""
)
fun flowGroupExplore(key: String): Flow<List<BookSourcePart>>
@Query("select distinct bookSourceGroup from book_sources where trim(bookSourceGroup) <> ''")
fun flowGroupsUnProcessed(): Flow<List<String>>
@Query(
"""select distinct bookSourceGroup from book_sources
where enabled = 1 and trim(bookSourceGroup) <> ''"""
)
fun flowEnabledGroupsUnProcessed(): Flow<List<String>>
@Query(
"""select distinct bookSourceGroup from book_sources
where enabledExplore = 1
and trim(exploreUrl) <> ''
and trim(bookSourceGroup) <> ''
order by customOrder"""
)
fun flowExploreGroupsUnProcessed(): Flow<List<String>>
@Query(
"""select * from book_sources
where bookSourceGroup like '%' || :group || '%' order by customOrder asc"""
)
fun getByGroup(group: String): List<BookSource>
@Query(
"""select * from book_sources
where enabled = 1
and (bookSourceGroup = :group
or bookSourceGroup like :group || ',%'
or bookSourceGroup like '%,' || :group
or bookSourceGroup like '%,' || :group || ',%')
order by customOrder asc"""
)
fun getEnabledByGroup(group: String): List<BookSource>
@Query(
"""select * from book_sources_part
where enabled = 1
and (bookSourceGroup = :group
or bookSourceGroup like :group || ',%'
or bookSourceGroup like '%,' || :group
or bookSourceGroup like '%,' || :group || ',%')
order by customOrder asc"""
)
fun getEnabledPartByGroup(group: String): List<BookSourcePart>
@Query(
"""select * from book_sources
where bookUrlPattern != 'NONE' and bookSourceType = :type order by customOrder asc"""
)
fun getEnabledByType(type: Int): List<BookSource>
@Query("select * from book_sources where enabled = 1 and bookSourceUrl = :baseUrl")
fun getBookSourceAddBook(baseUrl: String): BookSource?
@get:Query(
"""select bp.*
from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl
where b.enabled = 1
and trim(b.bookUrlPattern) <> ''
and trim(b.bookUrlPattern) <> 'NONE'
order by b.customOrder"""
)
val hasBookUrlPattern: List<BookSourcePart>
@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_part order by customOrder asc")
val allPart: List<BookSourcePart>
@get:Query("select * from book_sources where enabled = 1 order by customOrder")
val allEnabled: List<BookSource>
@get:Query("select * from book_sources_part where enabled = 1 order by customOrder asc")
val allEnabledPart: List<BookSourcePart>
@get:Query("select * from book_sources where enabled = 0 order by customOrder")
val allDisabled: List<BookSource>
@get:Query(
"""select * from book_sources
where bookSourceGroup is null or bookSourceGroup = '' or bookSourceGroup like '%未分组%'"""
)
val allNoGroup: List<BookSource>
@get:Query("select * from book_sources where enabledExplore = 1 order by customOrder")
val allEnabledExplore: List<BookSource>
@get:Query("select * from book_sources where enabledExplore = 0 order by customOrder")
val allDisabledExplore: List<BookSource>
@get:Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
val allLogin: List<BookSource>
@get:Query(
"""select bp.*
from book_sources b join book_sources_part bp on b.bookSourceUrl = bp.bookSourceUrl
where b.enabled = 1 and b.bookSourceType = 0 order by b.customOrder"""
)
val allTextEnabledPart: List<BookSourcePart>
@get:Query(
"""select distinct bookSourceGroup from book_sources
where trim(bookSourceGroup) <> ''"""
)
val allGroupsUnProcessed: List<String>
@get:Query(
"""select distinct bookSourceGroup from book_sources
where enabled = 1 and trim(bookSourceGroup) <> ''"""
)
val allEnabledGroupsUnProcessed: List<String>
@Query("select * from book_sources where bookSourceUrl = :key")
fun getBookSource(key: String): BookSource?
@Query("select * from book_sources_part where bookSourceUrl = :key")
fun getBookSourcePart(key: String): BookSourcePart?
@Query("select count(*) from book_sources")
fun allCount(): Int
@Query("SELECT EXISTS(select 1 from book_sources where bookSourceUrl = :key)")
fun has(key: String): Boolean
@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)
@Transaction
fun delete(bookSources: List<BookSourcePart>) {
for (bs in bookSources) {
delete(bs.bookSourceUrl)
}
}
@get:Query("select min(customOrder) from book_sources")
val minOrder: Int
@get:Query("select max(customOrder) from book_sources")
val maxOrder: Int
@get:Query(
"""select exists (select 1
from book_sources group by customOrder having count(customOrder) > 1)"""
)
val hasDuplicateOrder: Boolean
@Query("update book_sources set enabled = :enable where bookSourceUrl = :bookSourceUrl")
fun enable(bookSourceUrl: String, enable: Boolean)
@Transaction
fun enable(enable: Boolean, bookSources: List<BookSourcePart>) {
for (bs in bookSources) {
enable(bs.bookSourceUrl, enable)
}
}
@Query("update book_sources set enabledExplore = :enable where bookSourceUrl = :bookSourceUrl")
fun enableExplore(bookSourceUrl: String, enable: Boolean)
@Transaction
fun enableExplore(enable: Boolean, bookSources: List<BookSourcePart>) {
for (bs in bookSources) {
enableExplore(bs.bookSourceUrl, enable)
}
}
@Query(
"""update book_sources
set customOrder = :customOrder where bookSourceUrl = :bookSourceUrl"""
)
fun upOrder(bookSourceUrl: String, customOrder: Int)
@Transaction
fun upOrder(bookSources: List<BookSourcePart>) {
for (bs in bookSources) {
upOrder(bs.bookSourceUrl, bs.customOrder)
}
}
fun upOrder(bookSource: BookSourcePart) {
upOrder(bookSource.bookSourceUrl, bookSource.customOrder)
}
@Query(
"""update book_sources
set bookSourceGroup = :bookSourceGroup where bookSourceUrl = :bookSourceUrl"""
)
fun upGroup(bookSourceUrl: String, bookSourceGroup: String)
@Transaction
fun upGroup(bookSources: List<BookSourcePart>) {
for (bs in bookSources) {
bs.bookSourceGroup?.let { upGroup(bs.bookSourceUrl, it) }
}
}
private fun dealGroups(list: List<String>): List<String> {
val groups = linkedSetOf<String>()
list.forEach {
it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group ->
groups.add(group)
}
}
return groups.sortedWith { o1, o2 ->
o1.cnCompare(o2)
}
}
fun allGroups(): List<String> = dealGroups(allGroupsUnProcessed)
fun allEnabledGroups(): List<String> = dealGroups(allEnabledGroupsUnProcessed)
fun flowGroups(): Flow<List<String>> {
return flowGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
fun flowExploreGroups(): Flow<List<String>> {
return flowExploreGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
fun flowEnabledGroups(): Flow<List<String>> {
return flowEnabledGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
}
@@ -0,0 +1,60 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.Bookmark
import kotlinx.coroutines.flow.Flow
@Dao
interface BookmarkDao {
@get:Query(
"""
select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos
"""
)
val all: List<Bookmark>
@Query("select * from bookmarks order by bookName collate localized, bookAuthor collate localized, chapterIndex, chapterPos")
fun flowAll(): Flow<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>>
@Query(
"""select * from bookmarks
where bookName = :bookName and bookAuthor = :bookAuthor
order by chapterIndex"""
)
fun getByBook(bookName: String, bookAuthor: String): List<Bookmark>
@Query(
"""SELECT * FROM bookmarks
where bookName = :bookName and bookAuthor = :bookAuthor
and chapterName like '%'||:key||'%' or content like '%'||:key||'%'
order by chapterIndex"""
)
fun search(bookName: String, bookAuthor: String, key: String): 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,35 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import io.legato.kazusa.data.entities.Cache
@Dao
interface CacheDao {
@Query("select * from caches where `key` = :key")
fun get(key: String): Cache?
@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 `key` like 'v_' || :key || '_%'
or `key` = 'userInfo_' || :key
or `key` = 'loginHeader_' || :key
or `key` = 'sourceVariable_' || :key"""
)
fun deleteSourceVariables(key: String)
@Query("delete from caches where deadline > 0 and deadline < :now")
fun clearDeadline(now: Long)
}
@@ -0,0 +1,26 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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,32 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.DictRule
import kotlinx.coroutines.flow.Flow
@Dao
interface DictRuleDao {
@get:Query("select * from dictRules order by sortNumber")
val all: List<DictRule>
@get:Query("select * from dictRules where enabled = 1 order by sortNumber")
val enabled: List<DictRule>
@Query("select * from dictRules order by sortNumber")
fun flowAll(): Flow<List<DictRule>>
@Query("select * from dictRules where name = :name")
fun getByName(name: String): DictRule?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg dictRule: DictRule)
@Update
fun update(vararg dictRule: DictRule)
@Delete
fun delete(vararg dictRule: DictRule)
}
@@ -0,0 +1,36 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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,34 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.KeyboardAssist
import kotlinx.coroutines.flow.Flow
@Dao
interface KeyboardAssistsDao {
@get:Query("select * from keyboardAssists order by serialNo")
val all: List<KeyboardAssist>
@Query("select * from keyboardAssists where type = :type order by serialNo")
fun getByType(type: Int): List<KeyboardAssist>
@get:Query("select * from keyboardAssists order by serialNo")
val flowAll: Flow<List<KeyboardAssist>>
@Query("select * from keyboardAssists where type = :type order by serialNo")
fun flowByType(type: Int): Flow<List<KeyboardAssist>>
@get:Query("select max(serialNo) from keyboardAssists order by serialNo")
val maxSerialNo: Int
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg keyboardAssist: KeyboardAssist)
@Update
fun update(vararg keyboardAssist: KeyboardAssist)
@Delete
fun delete(vararg keyboardAssist: KeyboardAssist)
}
@@ -0,0 +1,55 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.ReadRecord
import io.legato.kazusa.data.entities.ReadRecordShow
@Dao
interface ReadRecordDao {
@get:Query("select * from readRecord")
val all: List<ReadRecord>
@get:Query(
"""
select bookName, sum(readTime) as readTime, max(lastRead) as lastRead
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 bookName, sum(readTime) as readTime, max(lastRead) as lastRead
from readRecord
where bookName like '%' || :searchKey || '%'
group by bookName
order by bookName collate localized"""
)
fun search(searchKey: String): List<ReadRecordShow>
@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,114 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.data.entities.ReplaceRule
import io.legato.kazusa.utils.cnCompare
import io.legato.kazusa.utils.splitNotBlank
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@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 flowGroupsUnProcessed(): Flow<List<String>>
@Query("select * from replace_rules where `group` is null or trim(`group`) = '' or trim(`group`) like '%未分组%'")
fun flowNoGroup(): Flow<List<ReplaceRule>>
@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 allGroupsUnProcessed: 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 scopeContent = 1
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '')
and (excludeScope is null or (excludeScope not LIKE '%' || :name || '%' and excludeScope not LIKE '%' || :origin || '%'))
order by sortOrder"""
)
fun findEnabledByContentScope(name: String, origin: String): List<ReplaceRule>
@Query(
"""SELECT * FROM replace_rules WHERE isEnabled = 1 and scopeTitle = 1
AND (scope LIKE '%' || :name || '%' or scope LIKE '%' || :origin || '%' or scope is null or scope = '')
and (excludeScope is null or (excludeScope not LIKE '%' || :name || '%' and excludeScope not LIKE '%' || :origin || '%'))
order by sortOrder"""
)
fun findEnabledByTitleScope(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)
private fun dealGroups(list: List<String>): List<String> {
val groups = linkedSetOf<String>()
list.forEach {
it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group ->
groups.add(group)
}
}
return groups.sortedWith { o1, o2 ->
o1.cnCompare(o2)
}
}
fun allGroups(): List<String> = dealGroups(allGroupsUnProcessed)
fun flowGroups(): Flow<List<String>> {
return flowGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
}
@@ -0,0 +1,40 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.RssArticle
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.`group`, 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)
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun append(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("update rssArticles set origin = :origin where origin = :oldOrigin")
fun updateOrigin(origin: String, oldOrigin: String)
@Query("delete from rssArticles where origin = :origin")
fun delete(origin: String)
}
@@ -0,0 +1,21 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.RssReadRecord
@Dao
interface RssReadRecordDao {
@Insert(onConflict = OnConflictStrategy.IGNORE)
fun insertRecord(vararg rssReadRecord: RssReadRecord)
@Query("select * from rssReadRecords order by readTime desc")
fun getRecords(): List<RssReadRecord>
@get:Query("select count(1) from rssReadRecords")
val countRecords: Int
@Query("delete from rssReadRecords")
fun deleteAllRecord()
}
@@ -0,0 +1,157 @@
package io.legato.kazusa.data.dao
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.data.entities.RssSource
import io.legato.kazusa.utils.cnCompare
import io.legato.kazusa.utils.splitNotBlank
import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
@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 order by customOrder")
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 || '%'
or sourceComment like '%' || :key || '%'
order by customOrder"""
)
fun flowSearch(key: String): Flow<List<RssSource>>
@Query(
"""SELECT * FROM rssSources
where (sourceGroup = :key
or sourceGroup like :key || ',%'
or sourceGroup like '%,' || :key
or 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 = 0 order by customOrder")
fun flowDisabled(): Flow<List<RssSource>>
@Query("select * from rssSources where loginUrl is not null and loginUrl != ''")
fun flowLogin(): Flow<List<RssSource>>
@Query("select * from rssSources where sourceGroup is null or sourceGroup = '' or sourceGroup like '%未分组%'")
fun flowNoGroup(): Flow<List<RssSource>>
@Query(
"""SELECT * FROM rssSources
where enabled = 1
and (sourceName like '%' || :searchKey || '%'
or sourceGroup like '%' || :searchKey || '%'
or sourceUrl like '%' || :searchKey || '%'
or sourceComment like '%' || :searchKey || '%')
order by customOrder"""
)
fun flowEnabled(searchKey: String): Flow<List<RssSource>>
@Query(
"""SELECT * FROM rssSources
where enabled = 1 and (sourceGroup = :searchKey
or sourceGroup like :searchKey || ',%'
or sourceGroup like '%,' || :searchKey
or sourceGroup like '%,' || :searchKey || ',%')
order by customOrder"""
)
fun flowEnabledByGroup(searchKey: String): Flow<List<RssSource>>
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
fun flowGroupsUnProcessed(): Flow<List<String>>
@Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> '' and enabled = 1")
fun flowEnabledGroupsUnProcessed(): Flow<List<String>>
@get:Query("select distinct sourceGroup from rssSources where trim(sourceGroup) <> ''")
val allGroupsUnProcessed: 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)
@Query("delete from rssSources where sourceGroup like 'legado'")
fun deleteDefault()
@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>
@Query("select exists(select 1 from rssSources where sourceUrl = :key)")
fun has(key: String): Boolean
@Query("update rssSources set enabled = :enable where sourceUrl = :sourceUrl")
fun enable(sourceUrl: String, enable: Boolean)
private fun dealGroups(list: List<String>): List<String> {
val groups = linkedSetOf<String>()
list.forEach {
it.splitNotBlank(AppPattern.splitGroupRegex).forEach { group ->
groups.add(group)
}
}
return groups.sortedWith { o1, o2 ->
o1.cnCompare(o2)
}
}
fun allGroups(): List<String> = dealGroups(allGroupsUnProcessed)
fun flowGroups(): Flow<List<String>> {
return flowGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
fun flowEnabledGroups(): Flow<List<String>> {
return flowEnabledGroupsUnProcessed().map { list ->
dealGroups(list)
}.flowOn(IO)
}
}
@@ -0,0 +1,45 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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 `group` from rssStars group by `group` order by `group`")
fun flowGroups(): Flow<List<String>>
@Query("select * from rssStars where `group` = :group order by starTime desc")
fun flowByGroup(group: String): Flow<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("update rssStars set origin = :origin where origin = :oldOrigin")
fun updateOrigin(origin: String, oldOrigin: String)
@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)
@Query("delete from rssStars where `group` = :group")
fun deleteByGroup(group: String)
@Query("delete from rssStars")
fun deleteAll()
}
@@ -0,0 +1,30 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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,73 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount
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 changeSourceByGroup(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, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount
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.bookSourceGroup like '%'||:sourceGroup||'%'
and (originName like '%'||:key||'%' or t1.latestChapterTitle like '%'||:key||'%')
and t2.enabled = 1
order by t2.customOrder"""
)
fun changeSourceSearch(
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, t1.chapterWordCountText, t1.respondTime, t1.chapterWordCount
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.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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,33 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.data.entities.Server
import kotlinx.coroutines.flow.Flow
@Dao
interface ServerDao {
@Query("select * from servers order by sortNumber")
fun observeAll(): Flow<List<Server>>
@get:Query("select * from servers order by sortNumber")
val all: List<Server>
@Query("select * from servers where id = :id")
fun get(id: Long): Server?
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(vararg server: Server)
@Update(onConflict = OnConflictStrategy.REPLACE)
fun update(vararg server: Server)
@Delete
fun delete(vararg server: Server)
@Query("delete from servers where id = :id")
fun delete(id: Long)
@Query("delete from servers where id < 0")
fun deleteDefault()
}
@@ -0,0 +1,45 @@
package io.legato.kazusa.data.dao
import androidx.room.*
import io.legato.kazusa.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 * from txtTocRules where enable != 1 order by serialNumber")
val disabled: List<TxtTocRule>
@get:Query("select count(*) from txtTocRules")
val count: Int
@Query("select * from txtTocRules where id = :id")
fun get(id: Long): 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,53 @@
package io.legato.kazusa.data.entities
import io.legato.kazusa.help.RuleBigDataHelp
import io.legato.kazusa.model.analyzeRule.RuleDataInterface
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.splitNotBlank
interface BaseBook : RuleDataInterface {
var name: String
var author: String
var bookUrl: String
var kind: String?
var wordCount: String?
var variable: String?
var infoHtml: String?
var tocHtml: String?
override fun putVariable(key: String, value: String?): Boolean {
if (super.putVariable(key, value)) {
variable = GSON.toJson(variableMap)
}
return true
}
fun putCustomVariable(value: String?) {
putVariable("custom", value)
}
fun getCustomVariable(): String {
return getVariable("custom")
}
override fun putBigVariable(key: String, value: String?) {
RuleBigDataHelp.putBookVariable(bookUrl, key, value)
}
override fun getBigVariable(key: String): String? {
return RuleBigDataHelp.getBookVariable(bookUrl, key)
}
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,29 @@
package io.legato.kazusa.data.entities
import io.legato.kazusa.help.RuleBigDataHelp
import io.legato.kazusa.model.analyzeRule.RuleDataInterface
import io.legato.kazusa.utils.GSON
interface BaseRssArticle : RuleDataInterface {
var origin: String
var link: String
var variable: String?
override fun putVariable(key: String, value: String?): Boolean {
if (super.putVariable(key, value)) {
variable = GSON.toJson(variableMap)
}
return true
}
override fun putBigVariable(key: String, value: String?) {
RuleBigDataHelp.putRssVariable(origin, link, key, value)
}
override fun getBigVariable(key: String): String? {
return RuleBigDataHelp.getRssVariable(origin, link, key)
}
}
@@ -0,0 +1,260 @@
package io.legato.kazusa.data.entities
import cn.hutool.crypto.symmetric.AES
import com.script.ScriptBindings
import com.script.buildScriptBindings
import com.script.rhino.RhinoScriptEngine
import io.legato.kazusa.constant.AppConst
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.data.entities.rule.RowUi
import io.legato.kazusa.help.CacheManager
import io.legato.kazusa.help.JsExtensions
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.crypto.SymmetricCryptoAndroid
import io.legato.kazusa.help.http.CookieStore
import io.legato.kazusa.help.source.getShareScope
import io.legato.kazusa.model.Debug
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.GSONStrict
import io.legato.kazusa.utils.fromJsonArray
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.has
import io.legato.kazusa.utils.printOnDebug
import org.intellij.lang.annotations.Language
/**
* 可在js里调用,source.xxx()
*/
@Suppress("unused")
interface BaseSource : JsExtensions {
/**
* 并发率
*/
var concurrentRate: String?
/**
* 登录地址
*/
var loginUrl: String?
/**
* 登录UI
*/
var loginUi: String?
/**
* 请求头
*/
var header: String?
/**
* 启用cookieJar
*/
var enabledCookieJar: Boolean?
/**
* js库
*/
var jsLib: String?
fun getTag(): String
fun getKey(): String
override fun getSource(): BaseSource? {
return this
}
fun loginUi(): List<RowUi>? {
return GSON.fromJsonArray<RowUi>(loginUi).onFailure {
it.printOnDebug()
}.getOrNull()
}
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
}
}
/**
* 调用login函数 实现登录请求
*/
fun login() {
val loginJs = getLoginJs()
if (!loginJs.isNullOrBlank()) {
@Language("js")
val js = """$loginJs
if(typeof login=='function'){
login.apply(this);
} else {
throw('Function login not implements!!!')
}
""".trimIndent()
evalJS(js)
}
}
/**
* 解析header规则
*/
fun getHeaderMap(hasLoginHeader: Boolean = false) = HashMap<String, String>().apply {
header?.let {
try {
val json = when {
it.startsWith("@js:", true) -> evalJS(it.substring(4)).toString()
it.startsWith("<js>", true) -> evalJS(
it.substring(4, it.lastIndexOf("<"))
).toString()
else -> it
}
GSONStrict.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
putAll(map)
} ?: GSON.fromJsonObject<Map<String, String>>(json).getOrNull()?.let { map ->
Debug.log("≡请求头规则 JSON 格式不规范,请改为规范格式")
putAll(map)
}
} catch (e: Exception) {
AppLog.put("执行请求头规则出错\n$e", e)
}
}
if (!has(AppConst.UA_NAME, true)) {
put(AppConst.UA_NAME, AppConfig.userAgent)
}
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<Map<String, String>>(cache).getOrNull()
}
/**
* 保存登录头部信息,map格式,访问时自动添加
*/
fun putLoginHeader(header: String) {
val headerMap = GSON.fromJsonObject<Map<String, String>>(header).getOrNull()
val cookie = headerMap?.get("Cookie") ?: headerMap?.get("cookie")
cookie?.let {
CookieStore.replaceCookie(getKey(), it)
}
CacheManager.put("loginHeader_${getKey()}", header)
}
fun removeLoginHeader() {
CacheManager.delete("loginHeader_${getKey()}")
CookieStore.removeCookie(getKey())
}
/**
* 获取用户信息,可以用来登录
* 用户信息采用aes加密存储
*/
fun getLoginInfo(): String? {
try {
val key = AppConst.androidId.encodeToByteArray(0, 16)
val cache = CacheManager.get("userInfo_${getKey()}") ?: return null
return AES(key).decryptStr(cache)
} catch (e: Exception) {
AppLog.put("获取登陆信息出错", e)
return null
}
}
fun getLoginInfoMap(): Map<String, String>? {
return GSON.fromJsonObject<Map<String, String>>(getLoginInfo()).getOrNull()
}
/**
* 保存用户信息,aes加密
*/
fun putLoginInfo(info: String): Boolean {
return try {
val key = (AppConst.androidId).encodeToByteArray(0, 16)
val encodeStr = SymmetricCryptoAndroid("AES", key).encryptBase64(info)
CacheManager.put("userInfo_${getKey()}", encodeStr)
true
} catch (e: Exception) {
AppLog.put("保存登陆信息出错", e)
false
}
}
fun removeLoginInfo() {
CacheManager.delete("userInfo_${getKey()}")
}
/**
* 设置自定义变量
* @param variable 变量内容
*/
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()}") ?: ""
}
/**
* 保存数据
*/
fun put(key: String, value: String): String {
CacheManager.put("v_${getKey()}_${key}", value)
return value
}
/**
* 获取保存的数据
*/
fun get(key: String): String {
return CacheManager.get("v_${getKey()}_${key}") ?: ""
}
/**
* 执行JS
*/
@Throws(Exception::class)
fun evalJS(jsStr: String, bindingsConfig: ScriptBindings.() -> Unit = {}): Any? {
val bindings = buildScriptBindings { bindings ->
bindings["java"] = this
bindings["source"] = this
bindings["baseUrl"] = getKey()
bindings["cookie"] = CookieStore
bindings["cache"] = CacheManager
bindings.apply(bindingsConfig)
}
val sharedScope = getShareScope()
val scope = if (sharedScope == null) {
RhinoScriptEngine.getRuntimeScope(bindings)
} else {
bindings.apply {
prototype = sharedScope
}
}
return RhinoScriptEngine.eval(jsStr, scope)
}
}
@@ -0,0 +1,416 @@
package io.legato.kazusa.data.entities
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import androidx.room.TypeConverter
import androidx.room.TypeConverters
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.BookType
import io.legato.kazusa.constant.PageAnim
import io.legato.kazusa.data.appDb
import io.legato.kazusa.help.book.BookHelp
import io.legato.kazusa.help.book.ContentProcessor
import io.legato.kazusa.help.book.getFolderNameNoCache
import io.legato.kazusa.help.book.isEpub
import io.legato.kazusa.help.book.isImage
import io.legato.kazusa.help.book.simulatedTotalChapterNum
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.help.config.ReadBookConfig
import io.legato.kazusa.model.ReadBook
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import java.nio.charset.Charset
import java.time.LocalDate
import kotlin.math.max
@Parcelize
@TypeConverters(Book.Converters::class)
@Entity(
tableName = "books",
indices = [Index(value = ["name", "author"], unique = true)]
)
data class Book(
// 详情页Url(本地书源存储完整文件路径)
@PrimaryKey
@ColumnInfo(defaultValue = "")
override var bookUrl: String = "",
// 目录页Url (toc=table of Contents)
@ColumnInfo(defaultValue = "")
var tocUrl: String = "",
// 书源URL(默认BookType.local)
@ColumnInfo(defaultValue = BookType.localTag)
var origin: String = BookType.localTag,
//书源名称 or 本地书籍文件名
@ColumnInfo(defaultValue = "")
var originName: String = "",
// 书籍名称(书源获取)
@ColumnInfo(defaultValue = "")
override var name: String = "",
// 作者名称(书源获取)
@ColumnInfo(defaultValue = "")
override var author: String = "",
// 分类信息(书源获取)
override var kind: String? = null,
// 分类信息(用户修改)
var customTag: String? = null,
// 封面Url(书源获取)
var coverUrl: String? = null,
// 封面Url(用户修改)
var customCoverUrl: String? = null,
// 简介内容(书源获取)
var intro: String? = null,
// 简介内容(用户修改)
var customIntro: String? = null,
// 自定义字符集名称(仅适用于本地书籍)
var charset: String? = null,
// 类型,详见BookType
@ColumnInfo(defaultValue = "0")
var type: Int = BookType.text,
// 自定义分组索引号
@ColumnInfo(defaultValue = "0")
var group: Long = 0,
// 最新章节标题
var latestChapterTitle: String? = null,
// 最新章节标题更新时间
@ColumnInfo(defaultValue = "0")
var latestChapterTime: Long = System.currentTimeMillis(),
// 最近一次更新书籍信息的时间
@ColumnInfo(defaultValue = "0")
var lastCheckTime: Long = System.currentTimeMillis(),
// 最近一次发现新章节的数量
@ColumnInfo(defaultValue = "0")
var lastCheckCount: Int = 0,
// 书籍目录总数
@ColumnInfo(defaultValue = "0")
var totalChapterNum: Int = 0,
// 当前章节名称
var durChapterTitle: String? = null,
// 当前章节索引
@ColumnInfo(defaultValue = "0")
var durChapterIndex: Int = 0,
// 当前阅读的进度(首行字符的索引位置)
@ColumnInfo(defaultValue = "0")
var durChapterPos: Int = 0,
// 最近一次阅读书籍的时间(打开正文的时间)
@ColumnInfo(defaultValue = "0")
var durChapterTime: Long = System.currentTimeMillis(),
//字数
override var wordCount: String? = null,
// 刷新书架时更新书籍信息
@ColumnInfo(defaultValue = "1")
var canUpdate: Boolean = true,
// 手动排序
@ColumnInfo(defaultValue = "0")
var order: Int = 0,
//书源排序
@ColumnInfo(defaultValue = "0")
var originOrder: Int = 0,
// 自定义书籍变量信息(用于书源规则检索书籍信息)
override var variable: String? = null,
//阅读设置
var readConfig: ReadConfig? = null,
//同步时间
@ColumnInfo(defaultValue = "0")
var syncTime: Long = 0L
) : Parcelable, BaseBook {
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: HashMap<String, String> by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
}
@Ignore
@IgnoredOnParcel
override var infoHtml: String? = null
@Ignore
@IgnoredOnParcel
override var tocHtml: String? = null
@Ignore
@IgnoredOnParcel
var downloadUrls: List<String>? = null
@Ignore
@IgnoredOnParcel
private var folderName: String? = null
@get:Ignore
@IgnoredOnParcel
val lastChapterIndex get() = totalChapterNum - 1
fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
fun getUnreadChapterNum() = max(simulatedTotalChapterNum() - durChapterIndex - 1, 0)
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")
}
@IgnoredOnParcel
val config: ReadConfig
get() {
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 {
val useReplaceRule = config.useReplaceRule
if (useReplaceRule != null) {
return useReplaceRule
}
//图片类书源 epub本地 默认关闭净化
if (isImage || isEpub) {
return false
}
return AppConfig.replaceEnableDefault
}
fun setReSegment(reSegment: Boolean) {
config.reSegment = reSegment
}
fun getReSegment(): Boolean {
return config.reSegment
}
fun setPageAnim(pageAnim: Int?) {
config.pageAnim = pageAnim
}
fun getPageAnim(): Int {
var pageAnim = config.pageAnim
?: if (type and BookType.image > 0) PageAnim.scrollPageAnim else ReadBookConfig.pageAnim
if (pageAnim < 0) {
pageAnim = ReadBookConfig.pageAnim
}
return pageAnim
}
fun setImageStyle(imageStyle: String?) {
config.imageStyle = imageStyle
}
fun getImageStyle(): String? {
return config.imageStyle
}
fun setTtsEngine(ttsEngine: String?) {
config.ttsEngine = ttsEngine
}
fun getTtsEngine(): String? {
return config.ttsEngine
}
fun setSplitLongChapter(limitLongContent: Boolean) {
config.splitLongChapter = limitLongContent
}
fun getSplitLongChapter(): Boolean {
return config.splitLongChapter
}
// readSimulating 的 setter 和 getter
fun setReadSimulating(readSimulating: Boolean) {
config.readSimulating = readSimulating
}
fun getReadSimulating(): Boolean {
return config.readSimulating
}
// startDate 的 setter 和 getter
fun setStartDate(startDate: LocalDate?) {
config.startDate = startDate
}
fun getStartDate(): LocalDate? {
if (!config.readSimulating || config.startDate == null) {
return LocalDate.now()
}
return config.startDate
}
// startChapter 的 setter 和 getter
fun setStartChapter(startChapter: Int) {
config.startChapter = startChapter
}
fun getStartChapter(): Int {
if (config.readSimulating) return config.startChapter ?: 0
return this.durChapterIndex
}
// dailyChapters 的 setter 和 getter
fun setDailyChapters(dailyChapters: Int) {
config.dailyChapters = dailyChapters
}
fun getDailyChapters(): Int {
return config.dailyChapters
}
fun getDelTag(tag: Long): Boolean {
return config.delTag and tag == tag
}
fun addDelTag(tag: Long) {
config.delTag = config.delTag and tag
}
fun removeDelTag(tag: Long) {
config.delTag = config.delTag and tag.inv()
}
fun getFolderName(): String {
folderName?.let {
return it
}
//防止书名过长,只取9位
folderName = getFolderNameNoCache()
return folderName!!
}
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 migrateTo(newBook: Book, toc: List<BookChapter>): Book {
newBook.durChapterIndex = BookHelp
.getDurChapter(durChapterIndex, durChapterTitle, toc, totalChapterNum)
newBook.durChapterTitle = toc[newBook.durChapterIndex].getDisplayTitle(
ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules(),
getUseReplaceRule()
)
newBook.durChapterPos = durChapterPos
newBook.durChapterTime = durChapterTime
newBook.group = group
newBook.order = order
newBook.customCoverUrl = customCoverUrl
newBook.customIntro = customIntro
newBook.customTag = customTag
newBook.canUpdate = canUpdate
newBook.readConfig = readConfig
return newBook
}
fun createBookMark(): Bookmark {
return Bookmark(
bookName = name,
bookAuthor = author,
)
}
fun save() {
if (appDb.bookDao.has(bookUrl)) {
appDb.bookDao.update(this)
} else {
appDb.bookDao.insert(this)
}
}
fun delete() {
if (ReadBook.book?.bookUrl == bookUrl) {
ReadBook.book = null
}
appDb.bookDao.delete(this)
}
@Suppress("ConstPropertyName")
companion object {
const val hTag = 2L
const val rubyTag = 4L
const val imgStyleDefault = "DEFAULT"
const val imgStyleFull = "FULL"
const val imgStyleText = "TEXT"
const val imgStyleSingle = "SINGLE"
}
@Parcelize
data class ReadConfig(
var reverseToc: Boolean = false,
var pageAnim: Int? = null,
var reSegment: Boolean = false,
var imageStyle: String? = null,
var useReplaceRule: Boolean? = null,// 正文使用净化替换规则
var delTag: Long = 0L,//去除标签
var ttsEngine: String? = null,
var splitLongChapter: Boolean = true,
var readSimulating: Boolean = false,
var startDate: LocalDate? = null,
var startChapter: Int? = null, // 用户设置的起始章节
var dailyChapters: Int = 3 // 用户设置的每日更新章节数
) : Parcelable
class Converters {
@TypeConverter
fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config)
@TypeConverter
fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json).getOrNull()
}
}
@@ -0,0 +1,176 @@
package io.legato.kazusa.data.entities
import android.annotation.SuppressLint
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Index
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.data.appDb
import io.legato.kazusa.exception.RegexTimeoutException
import io.legato.kazusa.help.RuleBigDataHelp
import io.legato.kazusa.help.config.AppConfig
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
import io.legato.kazusa.model.analyzeRule.RuleDataInterface
import io.legato.kazusa.utils.ChineseUtils
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.MD5Utils
import io.legato.kazusa.utils.NetworkUtils
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.replace
import io.legato.kazusa.utils.toastOnUi
import kotlinx.coroutines.CancellationException
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import splitties.init.appCtx
@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 isVolume: Boolean = false, // 是否是卷名
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 wordCount: 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: HashMap<String, String> by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
}
@Ignore
@IgnoredOnParcel
var titleMD5: String? = null
override fun putVariable(key: String, value: String?): Boolean {
if (super.putVariable(key, value)) {
variable = GSON.toJson(variableMap)
}
return true
}
override fun putBigVariable(key: String, value: String?) {
RuleBigDataHelp.putChapterVariable(bookUrl, url, key, value)
}
override fun getBigVariable(key: String): String? {
return RuleBigDataHelp.getChapterVariable(bookUrl, url, key)
}
override fun hashCode() = url.hashCode()
override fun equals(other: Any?): Boolean {
if (other is BookChapter) {
return other.url == url
}
return false
}
fun primaryStr(): String {
return bookUrl + url
}
fun getDisplayTitle(
replaceRules: List<ReplaceRule>? = null,
useReplace: Boolean = true,
chineseConvert: Boolean = true,
): String {
var displayTitle = title.replace(AppPattern.rnRegex, "")
if (chineseConvert) {
when (AppConfig.chineseConverterType) {
1 -> displayTitle = ChineseUtils.t2s(displayTitle)
2 -> displayTitle = ChineseUtils.s2t(displayTitle)
}
}
if (useReplace && replaceRules != null) kotlin.run {
replaceRules.forEach { item ->
if (item.pattern.isNotEmpty()) {
try {
val mDisplayTitle = if (item.isRegex) {
displayTitle.replace(
item.regex,
item.replacement,
item.getValidTimeoutMillisecond()
)
} else {
displayTitle.replace(item.pattern, item.replacement)
}
if (mDisplayTitle.isNotBlank()) {
displayTitle = mDisplayTitle
}
} catch (e: RegexTimeoutException) {
item.isEnabled = false
appDb.replaceRuleDao.update(item)
} catch (e: CancellationException) {
return@run
} catch (e: Exception) {
AppLog.put("${item.name}替换出错\n替换内容\n${displayTitle}", e)
appCtx.toastOnUi("${item.name}替换出错")
}
}
}
}
return displayTitle
}
fun getAbsoluteURL(): String {
//二级目录解析的卷链接为空 返回目录页的链接
if (url.startsWith(title) && isVolume) return baseUrl
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())
}
}
private fun ensureTitleMD5Init() {
if (titleMD5 == null) {
titleMD5 = MD5Utils.md5Encode16(title)
}
}
@SuppressLint("DefaultLocale")
@Suppress("unused")
fun getFileName(suffix: String = "nb"): String {
ensureTitleMD5Init()
return String.format("%05d-%s.%s", index, titleMD5, suffix)
}
@SuppressLint("DefaultLocale")
@Suppress("unused")
fun getFontName(): String {
ensureTitleMD5Init()
return String.format("%05d-%s.ttf", index, titleMD5)
}
}
@@ -0,0 +1,15 @@
package io.legato.kazusa.data.entities
import android.os.Parcelable
import androidx.room.ColumnInfo
import kotlinx.parcelize.Parcelize
@Parcelize
class BookChapterReview(
@ColumnInfo(defaultValue = "0")
var bookId: Long = 0,
var chapterId: Long = 0,
var summaryUrl: String = "",
): Parcelable {
}
@@ -0,0 +1,75 @@
package io.legato.kazusa.data.entities
import android.content.Context
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.legato.kazusa.R
import io.legato.kazusa.help.config.AppConfig
import kotlinx.parcelize.Parcelize
@Suppress("ConstPropertyName")
@Parcelize
@Entity(tableName = "book_groups")
data class BookGroup(
@PrimaryKey
val groupId: Long = 0b1,
var groupName: String = "",
var cover: String? = null,
var order: Int = 0,
@ColumnInfo(defaultValue = "1")
var enableRefresh: Boolean = true,
@ColumnInfo(defaultValue = "1")
var show: Boolean = true,
@ColumnInfo(defaultValue = "-1")
var bookSort: Int = -1
) : Parcelable {
companion object {
const val IdRoot = -100L
const val IdAll = -1L
const val IdLocal = -2L
const val IdAudio = -3L
const val IdNetNone = -4L
const val IdLocalNone = -5L
const val IdError = -11L
}
fun getManageName(context: Context): String {
return when (groupId) {
IdAll -> "$groupName(${context.getString(R.string.all)})"
IdAudio -> "$groupName(${context.getString(R.string.audio)})"
IdLocal -> "$groupName(${context.getString(R.string.local)})"
IdNetNone -> "$groupName(${context.getString(R.string.net_no_group)})"
IdLocalNone -> "$groupName(${context.getString(R.string.local_no_group)})"
IdError -> "$groupName(${context.getString(R.string.update_book_fail)})"
else -> groupName
}
}
fun getRealBookSort(): Int {
if (bookSort < 0) {
return AppConfig.bookshelfSort
}
return bookSort
}
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.bookSort == bookSort
&& other.enableRefresh == enableRefresh
&& other.show == show
&& other.order == order
}
return false
}
}
@@ -0,0 +1,21 @@
package io.legato.kazusa.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,310 @@
package io.legato.kazusa.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 androidx.room.TypeConverter
import androidx.room.TypeConverters
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.constant.BookSourceType
import io.legato.kazusa.data.entities.rule.BookInfoRule
import io.legato.kazusa.data.entities.rule.ContentRule
import io.legato.kazusa.data.entities.rule.ExploreRule
import io.legato.kazusa.data.entities.rule.ReviewRule
import io.legato.kazusa.data.entities.rule.SearchRule
import io.legato.kazusa.data.entities.rule.TocRule
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import io.legato.kazusa.utils.splitNotBlank
import kotlinx.parcelize.Parcelize
@Suppress("unused")
@Parcelize
@TypeConverters(BookSource.Converters::class)
@Entity(
tableName = "book_sources",
indices = [(Index(value = ["bookSourceUrl"], unique = false))]
)
data class BookSource(
// 地址,包括 http/https
@PrimaryKey
var bookSourceUrl: String = "",
// 名称
var bookSourceName: String = "",
// 分组
var bookSourceGroup: String? = null,
// 类型,0 文本,1 音频, 2 图片, 3 文件(指的是类似知轩藏书只提供下载的网站)
@BookSourceType.Type
var bookSourceType: Int = 0,
// 详情页url正则
var bookUrlPattern: String? = null,
// 手动排序编号
@ColumnInfo(defaultValue = "0")
var customOrder: Int = 0,
// 是否启用
@ColumnInfo(defaultValue = "1")
var enabled: Boolean = true,
// 启用发现
@ColumnInfo(defaultValue = "1")
var enabledExplore: Boolean = true,
// js库
override var jsLib: String? = null,
// 启用okhttp CookieJAr 自动保存每次请求的cookie
@ColumnInfo(defaultValue = "0")
override var enabledCookieJar: Boolean? = true,
// 并发率
override var concurrentRate: String? = null,
// 请求头
override var header: String? = null,
// 登录地址
override var loginUrl: String? = null,
// 登录UI
override var loginUi: String? = null,
// 登录检测js
var loginCheckJs: String? = null,
// 封面解密js
var coverDecodeJs: String? = null,
// 注释
var bookSourceComment: String? = null,
// 自定义变量说明
var variableComment: String? = null,
// 最后更新时间,用于排序
var lastUpdateTime: Long = 0,
// 响应时间,用于排序
var respondTime: Long = 180000L,
// 智能排序的权重
var weight: Int = 0,
// 发现url
var exploreUrl: String? = null,
// 发现筛选规则
var exploreScreen: String? = null,
// 发现规则
var ruleExplore: ExploreRule? = null,
// 搜索url
var searchUrl: String? = null,
// 搜索规则
var ruleSearch: SearchRule? = null,
// 书籍信息页规则
var ruleBookInfo: BookInfoRule? = null,
// 目录页规则
var ruleToc: TocRule? = null,
// 正文页规则
var ruleContent: ContentRule? = null,
// 段评规则
var ruleReview: ReviewRule? = null
) : Parcelable, BaseSource {
override fun getTag(): String {
return bookSourceName
}
override fun getKey(): String {
return bookSourceUrl
}
override fun hashCode(): Int {
return bookSourceUrl.hashCode()
}
override fun equals(other: Any?): Boolean {
return if (other is BookSource) other.bookSourceUrl == bookSourceUrl else false
}
fun getSearchRule(): SearchRule {
ruleSearch?.let { return it }
val rule = SearchRule()
ruleSearch = rule
return rule
}
fun getExploreRule(): ExploreRule {
ruleExplore?.let { return it }
val rule = ExploreRule()
ruleExplore = rule
return rule
}
fun getBookInfoRule(): BookInfoRule {
ruleBookInfo?.let { return it }
val rule = BookInfoRule()
ruleBookInfo = rule
return rule
}
fun getTocRule(): TocRule {
ruleToc?.let { return it }
val rule = TocRule()
ruleToc = rule
return rule
}
fun getContentRule(): ContentRule {
ruleContent?.let { return it }
val rule = ContentRule()
ruleContent = rule
return rule
}
// fun getReviewRule(): ReviewRule {
// ruleReview?.let { return it }
// val rule = ReviewRule()
// ruleReview = rule
// return rule
// }
fun getDisPlayNameGroup(): String {
return if (bookSourceGroup.isNullOrBlank()) {
bookSourceName
} else {
String.format("%s (%s)", bookSourceName, bookSourceGroup)
}
}
fun addGroup(groups: String): BookSource {
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
bookSourceGroup = TextUtils.join(",", it)
}
if (bookSourceGroup.isNullOrBlank()) bookSourceGroup = groups
return this
}
fun removeGroup(groups: String): BookSource {
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
bookSourceGroup = TextUtils.join(",", it)
}
return this
}
fun hasGroup(group: String): Boolean {
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
return it.indexOf(group) != -1
}
return false
}
fun removeInvalidGroups() {
removeGroup(getInvalidGroupNames())
}
fun removeErrorComment() {
bookSourceComment = bookSourceComment
?.split("\n\n")
?.filterNot {
it.startsWith("// Error: ")
}?.joinToString("\n")
}
fun addErrorComment(e: Throwable) {
bookSourceComment =
"// Error: ${e.localizedMessage}" + if (bookSourceComment.isNullOrBlank())
"" else "\n\n${bookSourceComment}"
}
fun getCheckKeyword(default: String): String {
ruleSearch?.checkKeyWord?.let {
if (it.isNotBlank()) {
return it
}
}
return default
}
fun getInvalidGroupNames(): String {
return bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.filter {
"失效" in it || it == "校验超时"
}?.joinToString() ?: ""
}
fun getDisplayVariableComment(otherComment: String): String {
return if (variableComment.isNullOrBlank()) {
otherComment
} else {
"${variableComment}\n$otherComment"
}
}
fun equal(source: BookSource): Boolean {
return equal(bookSourceName, source.bookSourceName)
&& equal(bookSourceUrl, source.bookSourceUrl)
&& equal(bookSourceGroup, source.bookSourceGroup)
&& bookSourceType == source.bookSourceType
&& equal(bookUrlPattern, source.bookUrlPattern)
&& equal(bookSourceComment, source.bookSourceComment)
&& customOrder == source.customOrder
&& enabled == source.enabled
&& enabledExplore == source.enabledExplore
&& enabledCookieJar == source.enabledCookieJar
&& equal(variableComment, source.variableComment)
&& equal(concurrentRate, source.concurrentRate)
&& equal(jsLib, source.jsLib)
&& equal(header, source.header)
&& equal(loginUrl, source.loginUrl)
&& equal(loginUi, source.loginUi)
&& equal(loginCheckJs, source.loginCheckJs)
&& equal(coverDecodeJs, source.coverDecodeJs)
&& 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())
class Converters {
@TypeConverter
fun exploreRuleToString(exploreRule: ExploreRule?): String =
GSON.toJson(exploreRule)
@TypeConverter
fun stringToExploreRule(json: String?) =
GSON.fromJsonObject<ExploreRule>(json).getOrNull()
@TypeConverter
fun searchRuleToString(searchRule: SearchRule?): String =
GSON.toJson(searchRule)
@TypeConverter
fun stringToSearchRule(json: String?) =
GSON.fromJsonObject<SearchRule>(json).getOrNull()
@TypeConverter
fun bookInfoRuleToString(bookInfoRule: BookInfoRule?): String =
GSON.toJson(bookInfoRule)
@TypeConverter
fun stringToBookInfoRule(json: String?) =
GSON.fromJsonObject<BookInfoRule>(json).getOrNull()
@TypeConverter
fun tocRuleToString(tocRule: TocRule?): String =
GSON.toJson(tocRule)
@TypeConverter
fun stringToTocRule(json: String?) =
GSON.fromJsonObject<TocRule>(json).getOrNull()
@TypeConverter
fun contentRuleToString(contentRule: ContentRule?): String =
GSON.toJson(contentRule)
@TypeConverter
fun stringToContentRule(json: String?) =
GSON.fromJsonObject<ContentRule>(json).getOrNull()
@TypeConverter
fun stringToReviewRule(json: String?): ReviewRule? = null
@TypeConverter
fun reviewRuleToString(reviewRule: ReviewRule?): String = "null"
}
}
@@ -0,0 +1,81 @@
package io.legato.kazusa.data.entities
import android.text.TextUtils
import androidx.room.DatabaseView
import io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.data.appDb
import io.legato.kazusa.utils.splitNotBlank
@DatabaseView(
"""select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore,
(loginUrl is not null and trim(loginUrl) <> '') hasLoginUrl, lastUpdateTime, respondTime, weight,
(exploreUrl is not null and trim(exploreUrl) <> '') hasExploreUrl
from book_sources""",
viewName = "book_sources_part"
)
data class BookSourcePart(
// 地址,包括 http/https
var bookSourceUrl: String = "",
// 名称
var bookSourceName: String = "",
// 分组
var bookSourceGroup: String? = null,
// 手动排序编号
var customOrder: Int = 0,
// 是否启用
var enabled: Boolean = true,
// 启用发现
var enabledExplore: Boolean = true,
// 是否有登录地址
var hasLoginUrl: Boolean = false,
// 最后更新时间,用于排序
var lastUpdateTime: Long = 0,
// 响应时间,用于排序
var respondTime: Long = 180000L,
// 智能排序的权重
var weight: Int = 0,
// 是否有发现url
var hasExploreUrl: Boolean = false
) {
override fun hashCode(): Int {
return bookSourceUrl.hashCode()
}
override fun equals(other: Any?): Boolean {
return if (other is BookSourcePart) other.bookSourceUrl == bookSourceUrl else false
}
fun getDisPlayNameGroup(): String {
return if (bookSourceGroup.isNullOrBlank()) {
bookSourceName
} else {
String.format("%s (%s)", bookSourceName, bookSourceGroup)
}
}
fun getBookSource(): BookSource? {
return appDb.bookSourceDao.getBookSource(bookSourceUrl)
}
fun addGroup(groups: String) {
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
bookSourceGroup = TextUtils.join(",", it)
}
if (bookSourceGroup.isNullOrBlank()) bookSourceGroup = groups
}
fun removeGroup(groups: String) {
bookSourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
bookSourceGroup = TextUtils.join(",", it)
}
}
}
fun List<BookSourcePart>.toBookSource(): List<BookSource> {
return mapNotNull { it.getBookSource() }
}
@@ -0,0 +1,24 @@
package io.legato.kazusa.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.legato.kazusa.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.legato.kazusa.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,50 @@
package io.legato.kazusa.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.legato.kazusa.model.analyzeRule.AnalyzeRule
import io.legato.kazusa.model.analyzeRule.AnalyzeRule.Companion.setCoroutineContext
import io.legato.kazusa.model.analyzeRule.AnalyzeUrl
import kotlin.coroutines.coroutineContext
/**
* 字典规则
*/
@Entity(tableName = "dictRules")
data class DictRule(
@PrimaryKey
var name: String = "",
var urlRule: String = "",
var showRule: String = "",
@ColumnInfo(defaultValue = "1")
var enabled: Boolean = true,
@ColumnInfo(defaultValue = "0")
var sortNumber: Int = 0
) {
override fun hashCode(): Int {
return name.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is DictRule) {
return name == other.name
}
return false
}
/**
* 搜索字典
*/
suspend fun search(word: String): String {
val analyzeUrl = AnalyzeUrl(urlRule, key = word, coroutineContext = coroutineContext)
val body = analyzeUrl.getStrResponseAwait().body
if (showRule.isBlank()) {
return body!!
}
val analyzeRule = AnalyzeRule().setCoroutineContext(coroutineContext)
return analyzeRule.getString(showRule, mContent = body)
}
}
@@ -0,0 +1,83 @@
package io.legato.kazusa.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.jayway.jsonpath.DocumentContext
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.jsonPath
import io.legato.kazusa.utils.readLong
import io.legato.kazusa.utils.readString
/**
* 在线朗读引擎
*/
@Entity(tableName = "httpTTS")
data class HttpTTS(
@PrimaryKey
val id: Long = System.currentTimeMillis(),
var name: String = "",
var url: String = "",
var contentType: String? = null,
@ColumnInfo(defaultValue = "0")
override var concurrentRate: String? = "0",
override var loginUrl: String? = null,
override var loginUi: String? = null,
override var header: String? = null,
override var jsLib: String? = null,
@ColumnInfo(defaultValue = "0")
override var enabledCookieJar: Boolean? = false,
var loginCheckJs: String? = null,
@ColumnInfo(defaultValue = "0")
var lastUpdateTime: Long = System.currentTimeMillis()
) : BaseSource {
override fun getTag(): String {
return name
}
override fun getKey(): String {
return "httpTts:$id"
}
@Suppress("MemberVisibilityCanBePrivate")
companion object {
fun fromJsonDoc(doc: DocumentContext): Result<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")
)
}
}
fun fromJson(json: String): Result<HttpTTS> {
return fromJsonDoc(jsonPath.parse(json))
}
fun fromJsonArray(jsonArray: String): Result<ArrayList<HttpTTS>> {
return kotlin.runCatching {
val sources = arrayListOf<HttpTTS>()
val doc = jsonPath.parse(jsonArray).read<List<*>>("$")
doc.forEach {
val jsonItem = jsonPath.parse(it)
fromJsonDoc(jsonItem).getOrThrow().let { source ->
sources.add(source)
}
}
return@runCatching sources
}
}
}
}
@@ -0,0 +1,20 @@
package io.legato.kazusa.data.entities
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import kotlinx.parcelize.Parcelize
@Parcelize
@Entity(tableName = "keyboardAssists", primaryKeys = ["type", "key"])
data class KeyboardAssist(
@ColumnInfo(defaultValue = "0")
var type: Int = 0,
@ColumnInfo(defaultValue = "")
var key: String,
@ColumnInfo(defaultValue = "")
var value: String,
@ColumnInfo(defaultValue = "0")
var serialNo: Int = 0
) : Parcelable
@@ -0,0 +1,14 @@
package io.legato.kazusa.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
@Entity(tableName = "readRecord", primaryKeys = ["deviceId", "bookName"])
data class ReadRecord(
var deviceId: String = "",
var bookName: String = "",
@ColumnInfo(defaultValue = "0")
var readTime: Long = 0L,
@ColumnInfo(defaultValue = "0")
var lastRead: Long = System.currentTimeMillis()
)
@@ -0,0 +1,7 @@
package io.legato.kazusa.data.entities
data class ReadRecordShow(
var bookName: String,
var readTime: Long,
var lastRead: Long
)
@@ -0,0 +1,121 @@
package io.legato.kazusa.data.entities
import android.os.Parcelable
import android.text.TextUtils
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legato.kazusa.R
import io.legato.kazusa.constant.AppLog
import io.legato.kazusa.exception.NoStackTraceException
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
import splitties.init.appCtx
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(),
//名称
@ColumnInfo(defaultValue = "")
var name: String = "",
//分组
var group: String? = null,
//替换内容
@ColumnInfo(defaultValue = "")
var pattern: String = "",
//替换为
@ColumnInfo(defaultValue = "")
var replacement: String = "",
//作用范围
var scope: String? = null,
//作用于标题
@ColumnInfo(defaultValue = "0")
var scopeTitle: Boolean = false,
//作用于正文
@ColumnInfo(defaultValue = "1")
var scopeContent: Boolean = true,
//排除范围
var excludeScope: String? = null,
//是否启用
@ColumnInfo(defaultValue = "1")
var isEnabled: Boolean = true,
//是否正则
@ColumnInfo(defaultValue = "1")
var isRegex: Boolean = true,
//超时时间
@ColumnInfo(defaultValue = "3000")
var timeoutMillisecond: Long = 3000L,
//排序
@ColumnInfo(name = "sortOrder", defaultValue = "0")
var order: Int = Int.MIN_VALUE
) : 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()
}
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
val regex: Regex by lazy {
pattern.toRegex()
}
fun getDisplayNameGroup(): String {
return if (group.isNullOrBlank()) {
name
} else {
String.format("%s (%s)", name, group)
}
}
fun isValid(): Boolean {
if (TextUtils.isEmpty(pattern)) {
return false
}
//判断正则表达式是否正确
if (isRegex) {
try {
Pattern.compile(pattern)
} catch (ex: PatternSyntaxException) {
AppLog.put("正则语法错误或不支持:${ex.localizedMessage}", ex)
return false
}
// Pattern.compile测试通过,但是部分情况下会替换超时,报错,一般发生在修改表达式时漏删了
if (pattern.endsWith('|') && !pattern.endsWith("\\|")) {
return false
}
}
return true
}
@Throws(NoStackTraceException::class)
fun checkValid() {
if (!isValid()) {
throw NoStackTraceException(appCtx.getString(R.string.replace_rule_invalid))
}
}
fun getValidTimeoutMillisecond(): Long {
if (timeoutMillisecond <= 0) {
return 3000L
}
return timeoutMillisecond
}
}
@@ -0,0 +1,57 @@
package io.legato.kazusa.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import kotlinx.parcelize.IgnoredOnParcel
@Entity(
tableName = "rssArticles",
primaryKeys = ["origin", "link"]
)
data class RssArticle(
override var origin: String = "",
var sort: String = "",
var title: String = "",
var order: Long = 0,
override var link: String = "",
var pubDate: String? = null,
var description: String? = null,
var content: String? = null,
var image: String? = null,
@ColumnInfo(defaultValue = "默认分组")
var group: String = "默认分组",
var read: Boolean = false,
override var variable: String? = null
) : BaseRssArticle {
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: HashMap<String, String> by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
}
fun toStar() = RssStar(
origin = origin,
sort = sort,
title = title,
starTime = System.currentTimeMillis(),
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image,
group = group,
variable = variable
)
}
@@ -0,0 +1,13 @@
package io.legato.kazusa.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "rssReadRecords")
data class RssReadRecord(
@PrimaryKey
val record: String,
val title: String? = null,
val readTime: Long? = null,
val read: Boolean = true
)
@@ -0,0 +1,182 @@
package io.legato.kazusa.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 io.legato.kazusa.constant.AppPattern
import io.legato.kazusa.utils.splitNotBlank
import kotlinx.parcelize.Parcelize
@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,
// 自定义变量说明
var variableComment: String? = null,
// js库
override var jsLib: String? = null,
// 启用okhttp CookieJAr 自动保存每次请求的cookie
@ColumnInfo(defaultValue = "0")
override var enabledCookieJar: Boolean? = true,
/**并发率**/
override var concurrentRate: String? = null,
/**请求头**/
override var header: String? = null,
/**登录地址**/
override var loginUrl: String? = null,
/**登录Ui**/
override var loginUi: String? = null,
/**登录检测js**/
var loginCheckJs: String? = null,
/**封面解密js**/
var coverDecodeJs: String? = null,
/**分类Url**/
var sortUrl: String? = null,
/**是否单url源**/
var singleUrl: Boolean = false,
/*列表规则*/
/**列表样式,0,1,2**/
@ColumnInfo(defaultValue = "0")
var articleStyle: Int = 0,
/**列表规则**/
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,
/**正文url白名单**/
var contentWhitelist: String? = null,
/**正文url黑名单**/
var contentBlacklist: String? = null,
/**
* 跳转url拦截,
* js, 返回true拦截,js变量url,可以通过js打开url,比如调用阅读搜索,添加书架等,简化规则写法,不用webView js注入
* **/
var shouldOverrideUrlLoading: String? = null,
/**webView样式**/
var style: String? = null,
@ColumnInfo(defaultValue = "1")
var enableJs: Boolean = true,
@ColumnInfo(defaultValue = "1")
var loadWithBaseUrl: Boolean = true,
/**注入js**/
var injectJs: String? = null,
/*其它规则*/
/**最后更新时间,用于排序**/
@ColumnInfo(defaultValue = "0")
var lastUpdateTime: Long = 0,
@ColumnInfo(defaultValue = "0")
var customOrder: Int = 0
) : Parcelable, BaseSource {
override fun getTag(): String {
return sourceName
}
override fun getKey(): String {
return sourceUrl
}
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(sourceName, source.sourceName)
&& equal(sourceIcon, source.sourceIcon)
&& enabled == source.enabled
&& equal(sourceGroup, source.sourceGroup)
&& enabledCookieJar == source.enabledCookieJar
&& equal(sourceComment, source.sourceComment)
&& equal(concurrentRate, source.concurrentRate)
&& equal(header, source.header)
&& equal(loginUrl, source.loginUrl)
&& equal(loginUi, source.loginUi)
&& equal(loginCheckJs, source.loginCheckJs)
&& equal(coverDecodeJs, source.coverDecodeJs)
&& equal(sortUrl, source.sortUrl)
&& singleUrl == source.singleUrl
&& articleStyle == source.articleStyle
&& 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
&& equal(variableComment, source.variableComment)
&& equal(style, source.style)
&& equal(injectJs, source.injectJs)
}
private fun equal(a: String?, b: String?): Boolean {
return a == b || (a.isNullOrEmpty() && b.isNullOrEmpty())
}
fun getDisplayNameGroup(): String {
return if (sourceGroup.isNullOrBlank()) {
sourceName
} else {
String.format("%s (%s)", sourceName, sourceGroup)
}
}
fun addGroup(groups: String): RssSource {
sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.addAll(groups.splitNotBlank(AppPattern.splitGroupRegex))
sourceGroup = TextUtils.join(",", it)
}
if (sourceGroup.isNullOrBlank()) sourceGroup = groups
return this
}
fun removeGroup(groups: String): RssSource {
sourceGroup?.splitNotBlank(AppPattern.splitGroupRegex)?.toHashSet()?.let {
it.removeAll(groups.splitNotBlank(AppPattern.splitGroupRegex).toSet())
sourceGroup = TextUtils.join(",", it)
}
return this
}
fun getDisplayVariableComment(otherComment: String): String {
return if (variableComment.isNullOrBlank()) {
otherComment
} else {
"${variableComment}\n$otherComment"
}
}
}
@@ -0,0 +1,49 @@
package io.legato.kazusa.data.entities
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.Ignore
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import kotlinx.parcelize.IgnoredOnParcel
@Entity(
tableName = "rssStars",
primaryKeys = ["origin", "link"]
)
data class RssStar(
override var origin: String = "",
var sort: String = "",
var title: String = "",
var starTime: Long = 0,
override var link: String = "",
var pubDate: String? = null,
var description: String? = null,
var content: String? = null,
var image: String? = null,
@ColumnInfo(defaultValue = "默认分组")
var group: String = "默认分组",
override var variable: String? = null
) : BaseRssArticle {
@delegate:Transient
@delegate:Ignore
@IgnoredOnParcel
override val variableMap by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
}
fun toRssArticle() = RssArticle(
origin = origin,
sort = sort,
title = title,
link = link,
pubDate = pubDate,
description = description,
content = content,
image = image,
group = group,
variable = variable
)
}
@@ -0,0 +1,16 @@
package io.legato.kazusa.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,139 @@
package io.legato.kazusa.data.entities
import android.content.Context
import android.os.Parcelable
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.Ignore
import androidx.room.Index
import androidx.room.PrimaryKey
import io.legato.kazusa.R
import io.legato.kazusa.constant.BookType
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.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 = "",
/** BookType */
var type: Int = BookType.text,
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,
/** 目录页Url (toc=table of Contents) */
var tocUrl: String = "",
var time: Long = System.currentTimeMillis(),
override var variable: String? = null,
var originOrder: Int = 0,
var chapterWordCountText: String? = null,
@ColumnInfo(defaultValue = "-1")
var chapterWordCount: Int = -1,
@ColumnInfo(defaultValue = "-1")
var respondTime: Int = -1
) : 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: HashMap<String, String> by lazy {
GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: HashMap()
}
@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 releaseHtmlData() {
infoHtml = null
tocHtml = null
}
fun primaryStr(): String {
return origin + bookUrl
}
fun sameBookTypeLocal(bookType: Int): Boolean {
return type and BookType.allBookTypeLocal == bookType and BookType.allBookTypeLocal
}
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.tocHtml = this@SearchBook.tocHtml
}
}
@@ -0,0 +1,20 @@
package io.legato.kazusa.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,57 @@
package io.legato.kazusa.data.entities
import android.os.Parcelable
import androidx.room.Entity
import androidx.room.PrimaryKey
import io.legato.kazusa.utils.GSON
import io.legato.kazusa.utils.fromJsonObject
import kotlinx.parcelize.Parcelize
import org.json.JSONObject
/**
* 服务器
*/
@Parcelize
@Entity(tableName = "servers")
data class Server(
@PrimaryKey
var id: Long = System.currentTimeMillis(),
var name: String = "",
var type: TYPE = TYPE.WEBDAV,
var config: String? = null,
var sortNumber: Int = 0
) : Parcelable {
enum class TYPE {
WEBDAV
}
override fun hashCode(): Int {
return id.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is Server) {
return id == other.id
}
return false
}
fun getConfigJsonObject(): JSONObject? {
val json = config
json ?: return null
return JSONObject(json)
}
fun getWebDavConfig(): WebDavConfig? {
return if (type == TYPE.WEBDAV) GSON.fromJsonObject<WebDavConfig>(config).getOrNull() else null
}
@Parcelize
data class WebDavConfig(
var url: String,
var username: String,
var password: String
) : Parcelable
}
@@ -0,0 +1,29 @@
package io.legato.kazusa.data.entities
import androidx.room.Entity
import androidx.room.PrimaryKey
@Entity(tableName = "txtTocRules")
data class TxtTocRule(
@PrimaryKey
var id: Long = System.currentTimeMillis(),
var name: String = "",
var rule: String = "",
var example: String? = null,
var serialNumber: Int = -1,
var enable: Boolean = true
) {
override fun hashCode(): Int {
return id.hashCode()
}
override fun equals(other: Any?): Boolean {
if (other is TxtTocRule) {
return id == other.id
}
return false
}
}
@@ -0,0 +1,42 @@
package io.legato.kazusa.data.entities.rule
import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legato.kazusa.utils.INITIAL_GSON
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,
var downloadUrls: String? = null
) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<BookInfoRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, BookInfoRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(
json.asString,
BookInfoRule::class.java
)
else -> null
}
}
}
}
@@ -0,0 +1,17 @@
package io.legato.kazusa.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,41 @@
package io.legato.kazusa.data.entities.rule
import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legato.kazusa.utils.INITIAL_GSON
import kotlinx.parcelize.Parcelize
/**
* 正文处理规则
*/
@Parcelize
data class ContentRule(
var content: String? = null,
var title: String? = null, //有些网站只能在正文中获取标题
var nextContentUrl: String? = null,
var webJs: String? = null,
var sourceRegex: String? = null,
var replaceRegex: String? = null, //替换规则
var imageStyle: String? = null, //默认大小居中,FULL最大宽度
var imageDecode: String? = null, //图片bytes二次解密js, 返回解密后的bytes
var payAction: String? = null, //购买操作,js或者包含{{js}}的url
) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<ContentRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, ContentRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(
json.asString,
ContentRule::class.java
)
else -> null
}
}
}
}
@@ -0,0 +1,16 @@
package io.legato.kazusa.data.entities.rule
/**
* 发现分类
*/
data class ExploreKind(
val title: String = "",
val url: String? = null,
val style: FlexChildStyle? = null
) {
fun style(): FlexChildStyle {
return style ?: FlexChildStyle.defaultStyle
}
}
@@ -0,0 +1,40 @@
package io.legato.kazusa.data.entities.rule
import android.os.Parcelable
import com.google.gson.JsonDeserializer
import io.legato.kazusa.utils.INITIAL_GSON
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 {
companion object {
val jsonDeserializer = JsonDeserializer<ExploreRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, ExploreRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(
json.asString,
ExploreRule::class.java
)
else -> null
}
}
}
}
Loaded 100 of 972 files, more files were not shown because too many files have changed in this diff. Show more