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

No files matched your search

@@ -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
}
}
}
}
@@ -0,0 +1,39 @@
package io.legato.kazusa.data.entities.rule
import android.view.View
import com.google.android.flexbox.FlexboxLayout
data class FlexChildStyle(
val layout_flexGrow: Float = 0F,
val layout_flexShrink: Float = 1F,
val layout_alignSelf: String = "auto",
val layout_flexBasisPercent: Float = -1F,
val layout_wrapBefore: Boolean = false,
) {
fun alignSelf(): Int {
return when (layout_alignSelf) {
"auto" -> -1
"flex_start" -> 0
"flex_end" -> 1
"center" -> 2
"baseline" -> 3
"stretch" -> 4
else -> -1
}
}
fun apply(view: View) {
val lp = view.layoutParams as FlexboxLayout.LayoutParams
lp.flexGrow = layout_flexGrow
lp.flexShrink = layout_flexShrink
lp.alignSelf = alignSelf()
lp.flexBasisPercent = layout_flexBasisPercent
lp.isWrapBefore = layout_wrapBefore
}
companion object {
val defaultStyle = FlexChildStyle()
}
}
@@ -0,0 +1,36 @@
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 ReviewRule(
var reviewUrl: String? = null, // 段评URL
var avatarRule: String? = null, // 段评发布者头像
var contentRule: String? = null, // 段评内容
var postTimeRule: String? = null, // 段评发布时间
var reviewQuoteUrl: String? = null, // 获取段评回复URL
// 这些功能将在以上功能完成以后实现
var voteUpUrl: String? = null, // 点赞URL
var voteDownUrl: String? = null, // 点踩URL
var postReviewUrl: String? = null, // 发送回复URL
var postQuoteUrl: String? = null, // 发送回复段评URL
var deleteUrl: String? = null, // 删除段评URL
) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<ReviewRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, ReviewRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, ReviewRule::class.java)
else -> null
}
}
}
}
@@ -0,0 +1,23 @@
package io.legato.kazusa.data.entities.rule
data class RowUi(
var name: String,
var type: String = "text",
var action: String? = null,
var style: FlexChildStyle? = null
) {
@Suppress("ConstPropertyName")
object Type {
const val text = "text"
const val password = "password"
const val button = "button"
}
fun style(): FlexChildStyle {
return style ?: FlexChildStyle.defaultStyle
}
}
@@ -0,0 +1,39 @@
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 SearchRule(
/**校验关键字**/
var checkKeyWord: String? = null,
override var bookList: String? = null,
override var name: String? = null,
override var author: String? = null,
override var intro: String? = null,
override var kind: String? = null,
override var lastChapter: String? = null,
override var updateTime: String? = null,
override var bookUrl: String? = null,
override var coverUrl: String? = null,
override var wordCount: String? = null
) : BookListRule, Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<SearchRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, SearchRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, SearchRule::class.java)
else -> null
}
}
}
}
@@ -0,0 +1,34 @@
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 TocRule(
var preUpdateJs: String? = null,
var chapterList: String? = null,
var chapterName: String? = null,
var chapterUrl: String? = null,
var formatJs: String? = null,
var isVolume: String? = null,
var isVip: String? = null,
var isPay: String? = null,
var updateTime: String? = null,
var nextTocUrl: String? = null
) : Parcelable {
companion object {
val jsonDeserializer = JsonDeserializer<TocRule?> { json, _, _ ->
when {
json.isJsonObject -> INITIAL_GSON.fromJson(json, TocRule::class.java)
json.isJsonPrimitive -> INITIAL_GSON.fromJson(json.asString, TocRule::class.java)
else -> null
}
}
}
}