Rename
This commit is contained in:
1 parent
b86d90021c
commit
6c0757c91b
972 files changed
+6885
-7321
No files matched your search
@@ -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
Reference in new issue
Block a user