diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index cd28bd5c5..0865e1e46 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -51,7 +51,7 @@ android {
applicationId = "io.legato.kazusa"
minSdk = 26
targetSdk = 37
- versionCode = System.getenv("COMMIT_NUMBER")?.toInt()?.let { 10000 + it } ?: 32640
+ versionCode = System.getenv("COMMIT_NUMBER")?.toInt()?.let { 10000 + it } ?: 32641
versionName = System.getenv("APP_VERSION_NAME") ?: projectVersionName
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
@@ -152,6 +152,22 @@ android {
}
}
+androidComponents {
+ onVariants(selector().all()) { variant ->
+ variant.outputs.forEach { output ->
+ val abi = output.filters
+ .firstOrNull { it.filterType.name == "ABI" }
+ ?.identifier
+ ?: "universal"
+ output.outputFileName.set(
+ output.versionName.zip(output.versionCode) { versionName, versionCode ->
+ "QieKan-$abi-$versionName-vc$versionCode.apk"
+ }
+ )
+ }
+ }
+}
+
kotlin {
jvmToolchain {
languageVersion.set(JavaLanguageVersion.of(21))
diff --git a/app/schemas/io.legado.app.data.AppDatabase/94.json b/app/schemas/io.legado.app.data.AppDatabase/94.json
new file mode 100644
index 000000000..9723f49fe
--- /dev/null
+++ b/app/schemas/io.legado.app.data.AppDatabase/94.json
@@ -0,0 +1,3104 @@
+{
+ "formatVersion": 1,
+ "database": {
+ "version": 94,
+ "identityHash": "98b13caf6e843799cbd18dc9f6ebb6cb",
+ "entities": [
+ {
+ "tableName": "books",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL DEFAULT '', `tocUrl` TEXT NOT NULL DEFAULT '', `origin` TEXT NOT NULL DEFAULT 'loc_book', `originName` TEXT NOT NULL DEFAULT '', `name` TEXT NOT NULL DEFAULT '', `author` TEXT NOT NULL DEFAULT '', `kind` TEXT, `customTag` TEXT, `coverUrl` TEXT, `customCoverUrl` TEXT, `intro` TEXT, `customIntro` TEXT, `remark` TEXT, `charset` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `group` INTEGER NOT NULL DEFAULT 0, `latestChapterTitle` TEXT, `latestChapterTime` INTEGER NOT NULL DEFAULT 0, `lastCheckTime` INTEGER NOT NULL DEFAULT 0, `lastCheckCount` INTEGER NOT NULL DEFAULT 0, `totalChapterNum` INTEGER NOT NULL DEFAULT 0, `durChapterTitle` TEXT, `durChapterIndex` INTEGER NOT NULL DEFAULT 0, `durChapterPos` INTEGER NOT NULL DEFAULT 0, `durChapterTime` INTEGER NOT NULL DEFAULT 0, `wordCount` TEXT, `canUpdate` INTEGER NOT NULL DEFAULT 1, `order` INTEGER NOT NULL DEFAULT 0, `originOrder` INTEGER NOT NULL DEFAULT 0, `variable` TEXT, `readConfig` TEXT, `syncTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`bookUrl`))",
+ "fields": [
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "tocUrl",
+ "columnName": "tocUrl",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'loc_book'"
+ },
+ {
+ "fieldPath": "originName",
+ "columnName": "originName",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "kind",
+ "columnName": "kind",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customTag",
+ "columnName": "customTag",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "coverUrl",
+ "columnName": "coverUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customCoverUrl",
+ "columnName": "customCoverUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "intro",
+ "columnName": "intro",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customIntro",
+ "columnName": "customIntro",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "remark",
+ "columnName": "remark",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "charset",
+ "columnName": "charset",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "latestChapterTitle",
+ "columnName": "latestChapterTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "latestChapterTime",
+ "columnName": "latestChapterTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastCheckTime",
+ "columnName": "lastCheckTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastCheckCount",
+ "columnName": "lastCheckCount",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "totalChapterNum",
+ "columnName": "totalChapterNum",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durChapterTitle",
+ "columnName": "durChapterTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "durChapterIndex",
+ "columnName": "durChapterIndex",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durChapterPos",
+ "columnName": "durChapterPos",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durChapterTime",
+ "columnName": "durChapterTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "wordCount",
+ "columnName": "wordCount",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "canUpdate",
+ "columnName": "canUpdate",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "originOrder",
+ "columnName": "originOrder",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "variable",
+ "columnName": "variable",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "readConfig",
+ "columnName": "readConfig",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "syncTime",
+ "columnName": "syncTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "bookUrl"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_books_name_author",
+ "unique": false,
+ "columnNames": [
+ "name",
+ "author"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)"
+ },
+ {
+ "name": "index_books_durChapterTime",
+ "unique": false,
+ "columnNames": [
+ "durChapterTime"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_books_durChapterTime` ON `${TABLE_NAME}` (`durChapterTime`)"
+ }
+ ]
+ },
+ {
+ "tableName": "book_groups",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`groupId` INTEGER NOT NULL, `groupName` TEXT NOT NULL, `cover` TEXT, `order` INTEGER NOT NULL, `enableRefresh` INTEGER NOT NULL DEFAULT 1, `show` INTEGER NOT NULL DEFAULT 1, `bookSort` INTEGER NOT NULL DEFAULT -1, `isPrivate` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`groupId`))",
+ "fields": [
+ {
+ "fieldPath": "groupId",
+ "columnName": "groupId",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupName",
+ "columnName": "groupName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cover",
+ "columnName": "cover",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enableRefresh",
+ "columnName": "enableRefresh",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "show",
+ "columnName": "show",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "bookSort",
+ "columnName": "bookSort",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "isPrivate",
+ "columnName": "isPrivate",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "groupId"
+ ]
+ }
+ },
+ {
+ "tableName": "book_sources",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookSourceUrl` TEXT NOT NULL, `bookSourceName` TEXT NOT NULL, `bookSourceGroup` TEXT, `bookSourceType` INTEGER NOT NULL, `bookUrlPattern` TEXT, `customOrder` INTEGER NOT NULL DEFAULT 0, `enabled` INTEGER NOT NULL DEFAULT 1, `enabledExplore` INTEGER NOT NULL DEFAULT 1, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `exploreScreen` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` TEXT, `eventListener` INTEGER NOT NULL DEFAULT 0, `customButton` INTEGER NOT NULL DEFAULT 0, `homepageModules` TEXT, PRIMARY KEY(`bookSourceUrl`))",
+ "fields": [
+ {
+ "fieldPath": "bookSourceUrl",
+ "columnName": "bookSourceUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookSourceName",
+ "columnName": "bookSourceName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookSourceGroup",
+ "columnName": "bookSourceGroup",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bookSourceType",
+ "columnName": "bookSourceType",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookUrlPattern",
+ "columnName": "bookUrlPattern",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customOrder",
+ "columnName": "customOrder",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "enabledExplore",
+ "columnName": "enabledExplore",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "jsLib",
+ "columnName": "jsLib",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabledCookieJar",
+ "columnName": "enabledCookieJar",
+ "affinity": "INTEGER",
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "concurrentRate",
+ "columnName": "concurrentRate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "header",
+ "columnName": "header",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginUrl",
+ "columnName": "loginUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginUi",
+ "columnName": "loginUi",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginCheckJs",
+ "columnName": "loginCheckJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "coverDecodeJs",
+ "columnName": "coverDecodeJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bookSourceComment",
+ "columnName": "bookSourceComment",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "variableComment",
+ "columnName": "variableComment",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastUpdateTime",
+ "columnName": "lastUpdateTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "respondTime",
+ "columnName": "respondTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "weight",
+ "columnName": "weight",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "exploreUrl",
+ "columnName": "exploreUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "exploreScreen",
+ "columnName": "exploreScreen",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleExplore",
+ "columnName": "ruleExplore",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "searchUrl",
+ "columnName": "searchUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleSearch",
+ "columnName": "ruleSearch",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleBookInfo",
+ "columnName": "ruleBookInfo",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleToc",
+ "columnName": "ruleToc",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleContent",
+ "columnName": "ruleContent",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleReview",
+ "columnName": "ruleReview",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "eventListener",
+ "columnName": "eventListener",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "customButton",
+ "columnName": "customButton",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "homepageModules",
+ "columnName": "homepageModules",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "bookSourceUrl"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_book_sources_bookSourceUrl",
+ "unique": false,
+ "columnNames": [
+ "bookSourceUrl"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_book_sources_bookSourceUrl` ON `${TABLE_NAME}` (`bookSourceUrl`)"
+ }
+ ]
+ },
+ {
+ "tableName": "chapters",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `wordCount` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, `reviewImg` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isVolume",
+ "columnName": "isVolume",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "index",
+ "columnName": "index",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isVip",
+ "columnName": "isVip",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isPay",
+ "columnName": "isPay",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "resourceUrl",
+ "columnName": "resourceUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "tag",
+ "columnName": "tag",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "wordCount",
+ "columnName": "wordCount",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "start",
+ "columnName": "start",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "end",
+ "columnName": "end",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "startFragmentId",
+ "columnName": "startFragmentId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "endFragmentId",
+ "columnName": "endFragmentId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "variable",
+ "columnName": "variable",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "reviewImg",
+ "columnName": "reviewImg",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "url",
+ "bookUrl"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_chapters_bookUrl",
+ "unique": false,
+ "columnNames": [
+ "bookUrl"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_chapters_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)"
+ },
+ {
+ "name": "index_chapters_bookUrl_index",
+ "unique": true,
+ "columnNames": [
+ "bookUrl",
+ "index"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapters_bookUrl_index` ON `${TABLE_NAME}` (`bookUrl`, `index`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "books",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "bookUrl"
+ ],
+ "referencedColumns": [
+ "bookUrl"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "replace_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT NOT NULL DEFAULT '', `group` TEXT, `pattern` TEXT NOT NULL DEFAULT '', `replacement` TEXT NOT NULL DEFAULT '', `scope` TEXT, `scopeTitle` INTEGER NOT NULL DEFAULT 0, `scopeContent` INTEGER NOT NULL DEFAULT 1, `excludeScope` TEXT, `isEnabled` INTEGER NOT NULL DEFAULT 1, `isRegex` INTEGER NOT NULL DEFAULT 1, `timeoutMillisecond` INTEGER NOT NULL DEFAULT 3000, `sortOrder` INTEGER NOT NULL DEFAULT 0)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "replacement",
+ "columnName": "replacement",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "scope",
+ "columnName": "scope",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "scopeTitle",
+ "columnName": "scopeTitle",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "scopeContent",
+ "columnName": "scopeContent",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "excludeScope",
+ "columnName": "excludeScope",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "isRegex",
+ "columnName": "isRegex",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "timeoutMillisecond",
+ "columnName": "timeoutMillisecond",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "3000"
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "sortOrder",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_replace_rules_id",
+ "unique": false,
+ "columnNames": [
+ "id"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_replace_rules_id` ON `${TABLE_NAME}` (`id`)"
+ }
+ ]
+ },
+ {
+ "tableName": "searchBooks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `origin` TEXT NOT NULL, `originName` TEXT NOT NULL, `type` INTEGER NOT NULL, `name` TEXT NOT NULL, `author` TEXT NOT NULL, `kind` TEXT, `coverUrl` TEXT, `intro` TEXT, `wordCount` TEXT, `latestChapterTitle` TEXT, `tocUrl` TEXT NOT NULL, `time` INTEGER NOT NULL, `variable` TEXT, `originOrder` INTEGER NOT NULL, `chapterWordCountText` TEXT, `chapterWordCount` INTEGER NOT NULL DEFAULT -1, `respondTime` INTEGER NOT NULL DEFAULT -1, PRIMARY KEY(`bookUrl`), FOREIGN KEY(`origin`) REFERENCES `book_sources`(`bookSourceUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+ "fields": [
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "originName",
+ "columnName": "originName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "author",
+ "columnName": "author",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "kind",
+ "columnName": "kind",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "coverUrl",
+ "columnName": "coverUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "intro",
+ "columnName": "intro",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "wordCount",
+ "columnName": "wordCount",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "latestChapterTitle",
+ "columnName": "latestChapterTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "tocUrl",
+ "columnName": "tocUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "time",
+ "columnName": "time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "variable",
+ "columnName": "variable",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "originOrder",
+ "columnName": "originOrder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chapterWordCountText",
+ "columnName": "chapterWordCountText",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chapterWordCount",
+ "columnName": "chapterWordCount",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ },
+ {
+ "fieldPath": "respondTime",
+ "columnName": "respondTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "-1"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "bookUrl"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_searchBooks_bookUrl",
+ "unique": true,
+ "columnNames": [
+ "bookUrl"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_searchBooks_bookUrl` ON `${TABLE_NAME}` (`bookUrl`)"
+ },
+ {
+ "name": "index_searchBooks_origin",
+ "unique": false,
+ "columnNames": [
+ "origin"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_searchBooks_origin` ON `${TABLE_NAME}` (`origin`)"
+ }
+ ],
+ "foreignKeys": [
+ {
+ "table": "book_sources",
+ "onDelete": "CASCADE",
+ "onUpdate": "NO ACTION",
+ "columns": [
+ "origin"
+ ],
+ "referencedColumns": [
+ "bookSourceUrl"
+ ]
+ }
+ ]
+ },
+ {
+ "tableName": "search_keywords",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`word` TEXT NOT NULL, `usage` INTEGER NOT NULL, `lastUseTime` INTEGER NOT NULL, PRIMARY KEY(`word`))",
+ "fields": [
+ {
+ "fieldPath": "word",
+ "columnName": "word",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "usage",
+ "columnName": "usage",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "lastUseTime",
+ "columnName": "lastUseTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "word"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_search_keywords_word",
+ "unique": true,
+ "columnNames": [
+ "word"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_keywords_word` ON `${TABLE_NAME}` (`word`)"
+ }
+ ]
+ },
+ {
+ "tableName": "cookies",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `cookie` TEXT NOT NULL, PRIMARY KEY(`url`))",
+ "fields": [
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "cookie",
+ "columnName": "cookie",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "url"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_cookies_url",
+ "unique": true,
+ "columnNames": [
+ "url"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)"
+ }
+ ]
+ },
+ {
+ "tableName": "rssSources",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`sourceUrl` TEXT NOT NULL, `sourceName` TEXT NOT NULL, `sourceIcon` TEXT NOT NULL, `sourceGroup` TEXT, `sourceComment` TEXT, `enabled` INTEGER NOT NULL, `variableComment` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `coverDecodeJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL DEFAULT 0, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `contentWhitelist` TEXT, `contentBlacklist` TEXT, `shouldOverrideUrlLoading` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL DEFAULT 1, `loadWithBaseUrl` INTEGER NOT NULL DEFAULT 1, `injectJs` TEXT, `preloadJs` TEXT, `startHtml` TEXT, `startStyle` TEXT, `startJs` TEXT, `showWebLog` INTEGER NOT NULL DEFAULT 0, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, `customOrder` INTEGER NOT NULL DEFAULT 0, `type` INTEGER NOT NULL DEFAULT 0, `preload` INTEGER NOT NULL DEFAULT 0, `cacheFirst` INTEGER NOT NULL DEFAULT 0, `searchUrl` TEXT, `redirectPolicy` TEXT NOT NULL DEFAULT 'ASK_CROSS_ORIGIN', PRIMARY KEY(`sourceUrl`))",
+ "fields": [
+ {
+ "fieldPath": "sourceUrl",
+ "columnName": "sourceUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceName",
+ "columnName": "sourceName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceIcon",
+ "columnName": "sourceIcon",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceGroup",
+ "columnName": "sourceGroup",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sourceComment",
+ "columnName": "sourceComment",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "variableComment",
+ "columnName": "variableComment",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "jsLib",
+ "columnName": "jsLib",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabledCookieJar",
+ "columnName": "enabledCookieJar",
+ "affinity": "INTEGER",
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "concurrentRate",
+ "columnName": "concurrentRate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "header",
+ "columnName": "header",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginUrl",
+ "columnName": "loginUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginUi",
+ "columnName": "loginUi",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginCheckJs",
+ "columnName": "loginCheckJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "coverDecodeJs",
+ "columnName": "coverDecodeJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sortUrl",
+ "columnName": "sortUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "singleUrl",
+ "columnName": "singleUrl",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "articleStyle",
+ "columnName": "articleStyle",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "ruleArticles",
+ "columnName": "ruleArticles",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleNextPage",
+ "columnName": "ruleNextPage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleTitle",
+ "columnName": "ruleTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "rulePubDate",
+ "columnName": "rulePubDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleDescription",
+ "columnName": "ruleDescription",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleImage",
+ "columnName": "ruleImage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleLink",
+ "columnName": "ruleLink",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "ruleContent",
+ "columnName": "ruleContent",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "contentWhitelist",
+ "columnName": "contentWhitelist",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "contentBlacklist",
+ "columnName": "contentBlacklist",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "shouldOverrideUrlLoading",
+ "columnName": "shouldOverrideUrlLoading",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "style",
+ "columnName": "style",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enableJs",
+ "columnName": "enableJs",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "loadWithBaseUrl",
+ "columnName": "loadWithBaseUrl",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "injectJs",
+ "columnName": "injectJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "preloadJs",
+ "columnName": "preloadJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "startHtml",
+ "columnName": "startHtml",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "startStyle",
+ "columnName": "startStyle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "startJs",
+ "columnName": "startJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "showWebLog",
+ "columnName": "showWebLog",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastUpdateTime",
+ "columnName": "lastUpdateTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "customOrder",
+ "columnName": "customOrder",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "preload",
+ "columnName": "preload",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "cacheFirst",
+ "columnName": "cacheFirst",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "searchUrl",
+ "columnName": "searchUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "redirectPolicy",
+ "columnName": "redirectPolicy",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'ASK_CROSS_ORIGIN'"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "sourceUrl"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_rssSources_sourceUrl",
+ "unique": false,
+ "columnNames": [
+ "sourceUrl"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)"
+ }
+ ]
+ },
+ {
+ "tableName": "bookmarks",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL DEFAULT '', `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))",
+ "fields": [
+ {
+ "fieldPath": "time",
+ "columnName": "time",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookName",
+ "columnName": "bookName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookAuthor",
+ "columnName": "bookAuthor",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "chapterIndex",
+ "columnName": "chapterIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chapterPos",
+ "columnName": "chapterPos",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chapterName",
+ "columnName": "chapterName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookText",
+ "columnName": "bookText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "time"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_bookmarks_bookName_bookAuthor",
+ "unique": false,
+ "columnNames": [
+ "bookName",
+ "bookAuthor"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_bookmarks_bookName_bookAuthor` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`)"
+ }
+ ]
+ },
+ {
+ "tableName": "rssArticles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `order` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `read` INTEGER NOT NULL, `variable` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `durPos` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`origin`, `link`, `sort`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sort",
+ "columnName": "sort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "link",
+ "columnName": "link",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pubDate",
+ "columnName": "pubDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'默认分组'"
+ },
+ {
+ "fieldPath": "read",
+ "columnName": "read",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "variable",
+ "columnName": "variable",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durPos",
+ "columnName": "durPos",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "origin",
+ "link",
+ "sort"
+ ]
+ }
+ },
+ {
+ "tableName": "rssReadRecords",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `title` TEXT, `readTime` INTEGER, `read` INTEGER NOT NULL, `origin` TEXT NOT NULL DEFAULT '', `sort` TEXT NOT NULL DEFAULT '', `image` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `durPos` INTEGER NOT NULL DEFAULT 0, `pubDate` TEXT, PRIMARY KEY(`record`))",
+ "fields": [
+ {
+ "fieldPath": "record",
+ "columnName": "record",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "readTime",
+ "columnName": "readTime",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "read",
+ "columnName": "read",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "sort",
+ "columnName": "sort",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durPos",
+ "columnName": "durPos",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "pubDate",
+ "columnName": "pubDate",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "record"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_rssReadRecords_origin",
+ "unique": false,
+ "columnNames": [
+ "origin"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_rssReadRecords_origin` ON `${TABLE_NAME}` (`origin`)"
+ }
+ ]
+ },
+ {
+ "tableName": "readRecordDetail",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL DEFAULT '', `bookUrl` TEXT, `date` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `readWords` INTEGER NOT NULL DEFAULT 0, `firstReadTime` INTEGER NOT NULL DEFAULT 0, `lastReadTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`, `bookAuthor`, `date`))",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookName",
+ "columnName": "bookName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookAuthor",
+ "columnName": "bookAuthor",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "date",
+ "columnName": "date",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "readTime",
+ "columnName": "readTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "readWords",
+ "columnName": "readWords",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "firstReadTime",
+ "columnName": "firstReadTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastReadTime",
+ "columnName": "lastReadTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "deviceId",
+ "bookName",
+ "bookAuthor",
+ "date"
+ ]
+ }
+ },
+ {
+ "tableName": "readRecordSession",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL DEFAULT '', `bookUrl` TEXT, `startTime` INTEGER NOT NULL, `endTime` INTEGER NOT NULL, `words` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookName",
+ "columnName": "bookName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookAuthor",
+ "columnName": "bookAuthor",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "startTime",
+ "columnName": "startTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "endTime",
+ "columnName": "endTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "words",
+ "columnName": "words",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "rssStars",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`origin` TEXT NOT NULL, `sort` TEXT NOT NULL, `title` TEXT NOT NULL, `starTime` INTEGER NOT NULL, `link` TEXT NOT NULL, `pubDate` TEXT, `description` TEXT, `content` TEXT, `image` TEXT, `group` TEXT NOT NULL DEFAULT '默认分组', `variable` TEXT, `type` INTEGER NOT NULL DEFAULT 0, `durPos` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`origin`, `link`))",
+ "fields": [
+ {
+ "fieldPath": "origin",
+ "columnName": "origin",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sort",
+ "columnName": "sort",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "starTime",
+ "columnName": "starTime",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "link",
+ "columnName": "link",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pubDate",
+ "columnName": "pubDate",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "description",
+ "columnName": "description",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "content",
+ "columnName": "content",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "image",
+ "columnName": "image",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "group",
+ "columnName": "group",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "'默认分组'"
+ },
+ {
+ "fieldPath": "variable",
+ "columnName": "variable",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "durPos",
+ "columnName": "durPos",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "origin",
+ "link"
+ ]
+ }
+ },
+ {
+ "tableName": "txtTocRules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `rule` TEXT NOT NULL, `example` TEXT, `serialNumber` INTEGER NOT NULL, `enable` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "rule",
+ "columnName": "rule",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "example",
+ "columnName": "example",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "serialNumber",
+ "columnName": "serialNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enable",
+ "columnName": "enable",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "readRecord",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL DEFAULT '', `bookUrl` TEXT, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`, `bookAuthor`))",
+ "fields": [
+ {
+ "fieldPath": "deviceId",
+ "columnName": "deviceId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookName",
+ "columnName": "bookName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookAuthor",
+ "columnName": "bookAuthor",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "readTime",
+ "columnName": "readTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "lastRead",
+ "columnName": "lastRead",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "deviceId",
+ "bookName",
+ "bookAuthor"
+ ]
+ }
+ },
+ {
+ "tableName": "httpTTS",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `url` TEXT NOT NULL, `contentType` TEXT, `concurrentRate` TEXT DEFAULT '0', `loginUrl` TEXT, `loginUi` TEXT, `header` TEXT, `jsLib` TEXT, `enabledCookieJar` INTEGER DEFAULT 0, `loginCheckJs` TEXT, `lastUpdateTime` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contentType",
+ "columnName": "contentType",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "concurrentRate",
+ "columnName": "concurrentRate",
+ "affinity": "TEXT",
+ "defaultValue": "'0'"
+ },
+ {
+ "fieldPath": "loginUrl",
+ "columnName": "loginUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "loginUi",
+ "columnName": "loginUi",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "header",
+ "columnName": "header",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "jsLib",
+ "columnName": "jsLib",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabledCookieJar",
+ "columnName": "enabledCookieJar",
+ "affinity": "INTEGER",
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "loginCheckJs",
+ "columnName": "loginCheckJs",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "lastUpdateTime",
+ "columnName": "lastUpdateTime",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "caches",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`key` TEXT NOT NULL, `value` TEXT, `deadline` INTEGER NOT NULL, PRIMARY KEY(`key`))",
+ "fields": [
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "deadline",
+ "columnName": "deadline",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "key"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_caches_key",
+ "unique": true,
+ "columnNames": [
+ "key"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)"
+ }
+ ]
+ },
+ {
+ "tableName": "ruleSubs",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`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`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "customOrder",
+ "columnName": "customOrder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "autoUpdate",
+ "columnName": "autoUpdate",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "update",
+ "columnName": "update",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "dictRules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`name` TEXT NOT NULL, `urlRule` TEXT NOT NULL, `showRule` TEXT NOT NULL, `enabled` INTEGER NOT NULL DEFAULT 1, `sortNumber` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`name`))",
+ "fields": [
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "urlRule",
+ "columnName": "urlRule",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "showRule",
+ "columnName": "showRule",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "1"
+ },
+ {
+ "fieldPath": "sortNumber",
+ "columnName": "sortNumber",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "name"
+ ]
+ }
+ },
+ {
+ "tableName": "keyboardAssists",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`type` INTEGER NOT NULL DEFAULT 0, `key` TEXT NOT NULL DEFAULT '', `value` TEXT NOT NULL DEFAULT '', `serialNo` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`type`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true,
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "serialNo",
+ "columnName": "serialNo",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "type",
+ "key"
+ ]
+ }
+ },
+ {
+ "tableName": "servers",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `name` TEXT NOT NULL, `type` TEXT NOT NULL, `config` TEXT, `sortNumber` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "config",
+ "columnName": "config",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sortNumber",
+ "columnName": "sortNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "search_content_history",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `bookName` TEXT DEFAULT '', `bookAuthor` TEXT DEFAULT '', `query` TEXT NOT NULL, `time` INTEGER NOT NULL)",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookName",
+ "columnName": "bookName",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "bookAuthor",
+ "columnName": "bookAuthor",
+ "affinity": "TEXT",
+ "defaultValue": "''"
+ },
+ {
+ "fieldPath": "query",
+ "columnName": "query",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "time",
+ "columnName": "time",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": true,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_search_content_history_bookName_bookAuthor_query",
+ "unique": true,
+ "columnNames": [
+ "bookName",
+ "bookAuthor",
+ "query"
+ ],
+ "orders": [],
+ "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_search_content_history_bookName_bookAuthor_query` ON `${TABLE_NAME}` (`bookName`, `bookAuthor`, `query`)"
+ }
+ ]
+ },
+ {
+ "tableName": "homepage_modules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `sourceUrl` TEXT NOT NULL, `moduleKey` TEXT NOT NULL, `type` TEXT NOT NULL, `title` TEXT NOT NULL, `args` TEXT, `layoutConfig` TEXT, `url` TEXT, `isEnabled` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL, `customSetId` TEXT, `isUserCreated` INTEGER NOT NULL, `customTitle` TEXT, `customSetTitle` TEXT, `sourceJsonHash` TEXT, `syncedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sourceUrl",
+ "columnName": "sourceUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "moduleKey",
+ "columnName": "moduleKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "type",
+ "columnName": "type",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "args",
+ "columnName": "args",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "layoutConfig",
+ "columnName": "layoutConfig",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "url",
+ "columnName": "url",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isEnabled",
+ "columnName": "isEnabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortOrder",
+ "columnName": "sortOrder",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "customSetId",
+ "columnName": "customSetId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "isUserCreated",
+ "columnName": "isUserCreated",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "customTitle",
+ "columnName": "customTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customSetTitle",
+ "columnName": "customSetTitle",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "sourceJsonHash",
+ "columnName": "sourceJsonHash",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "syncedAt",
+ "columnName": "syncedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "homepage_custom_sets",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `sortOrder` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortOrder",
+ "columnName": "sortOrder",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "highlightRules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `pattern` TEXT NOT NULL, `sampleText` TEXT NOT NULL, `targetScope` INTEGER NOT NULL, `enabled` INTEGER NOT NULL, `position` INTEGER NOT NULL, `textColor` INTEGER, `bgColor` INTEGER, `underlineMode` INTEGER NOT NULL, `underlineColor` INTEGER, `underlineWidth` REAL NOT NULL, `underlineOffset` REAL NOT NULL, `underlineSvgPath` TEXT, `bgImage` TEXT, `bgImageFit` INTEGER NOT NULL, `bgImageScale` REAL NOT NULL, `configName` TEXT, `fontPath` TEXT, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sampleText",
+ "columnName": "sampleText",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "targetScope",
+ "columnName": "targetScope",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "position",
+ "columnName": "position",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "textColor",
+ "columnName": "textColor",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "bgColor",
+ "columnName": "bgColor",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "underlineMode",
+ "columnName": "underlineMode",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "underlineColor",
+ "columnName": "underlineColor",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "underlineWidth",
+ "columnName": "underlineWidth",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "underlineOffset",
+ "columnName": "underlineOffset",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "underlineSvgPath",
+ "columnName": "underlineSvgPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bgImage",
+ "columnName": "bgImage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "bgImageFit",
+ "columnName": "bgImageFit",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bgImageScale",
+ "columnName": "bgImageScale",
+ "affinity": "REAL",
+ "notNull": true
+ },
+ {
+ "fieldPath": "configName",
+ "columnName": "configName",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "fontPath",
+ "columnName": "fontPath",
+ "affinity": "TEXT"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ai_provider_profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `protocol` TEXT NOT NULL, `baseUrl` TEXT NOT NULL, `modelsUrl` TEXT, `apiKey` TEXT NOT NULL, `authType` TEXT NOT NULL, `secretRef` TEXT, `headersJson` TEXT, `chatPath` TEXT, `responsesPath` TEXT, `messagesPath` TEXT, `modelsPath` TEXT, `customHeadersJson` TEXT, `enabled` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "protocol",
+ "columnName": "protocol",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "baseUrl",
+ "columnName": "baseUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelsUrl",
+ "columnName": "modelsUrl",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "apiKey",
+ "columnName": "apiKey",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "authType",
+ "columnName": "authType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "secretRef",
+ "columnName": "secretRef",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "headersJson",
+ "columnName": "headersJson",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chatPath",
+ "columnName": "chatPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "responsesPath",
+ "columnName": "responsesPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "messagesPath",
+ "columnName": "messagesPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "modelsPath",
+ "columnName": "modelsPath",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "customHeadersJson",
+ "columnName": "customHeadersJson",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ai_model_profiles",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `providerId` TEXT NOT NULL, `displayName` TEXT NOT NULL, `modelId` TEXT NOT NULL, `contextWindow` INTEGER NOT NULL, `maxOutputTokens` INTEGER NOT NULL, `capabilities` TEXT NOT NULL, `defaultParamsJson` TEXT, `enabled` INTEGER NOT NULL, `sortNumber` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "providerId",
+ "columnName": "providerId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "displayName",
+ "columnName": "displayName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelId",
+ "columnName": "modelId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "contextWindow",
+ "columnName": "contextWindow",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "maxOutputTokens",
+ "columnName": "maxOutputTokens",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "capabilities",
+ "columnName": "capabilities",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "defaultParamsJson",
+ "columnName": "defaultParamsJson",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortNumber",
+ "columnName": "sortNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ai_model_profiles_providerId",
+ "unique": false,
+ "columnNames": [
+ "providerId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_model_profiles_providerId` ON `${TABLE_NAME}` (`providerId`)"
+ }
+ ]
+ },
+ {
+ "tableName": "ai_task_presets",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `taskType` TEXT NOT NULL, `name` TEXT NOT NULL, `modelProfileId` TEXT NOT NULL, `promptTemplate` TEXT NOT NULL, `paramsJson` TEXT, `chunkPolicyJson` TEXT, `enabled` INTEGER NOT NULL, `isDefault` INTEGER NOT NULL, `sortNumber` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "taskType",
+ "columnName": "taskType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "name",
+ "columnName": "name",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelProfileId",
+ "columnName": "modelProfileId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "promptTemplate",
+ "columnName": "promptTemplate",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "paramsJson",
+ "columnName": "paramsJson",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "chunkPolicyJson",
+ "columnName": "chunkPolicyJson",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isDefault",
+ "columnName": "isDefault",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "sortNumber",
+ "columnName": "sortNumber",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ai_task_presets_taskType",
+ "unique": false,
+ "columnNames": [
+ "taskType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_task_presets_taskType` ON `${TABLE_NAME}` (`taskType`)"
+ },
+ {
+ "name": "index_ai_task_presets_modelProfileId",
+ "unique": false,
+ "columnNames": [
+ "modelProfileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_task_presets_modelProfileId` ON `${TABLE_NAME}` (`modelProfileId`)"
+ }
+ ]
+ },
+ {
+ "tableName": "ai_artifacts",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `taskType` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `chapterIndex` INTEGER, `contentHash` TEXT NOT NULL, `promptHash` TEXT NOT NULL, `modelProfileId` TEXT NOT NULL, `status` INTEGER NOT NULL, `output` TEXT, `errorMessage` TEXT, `schemaVersion` INTEGER NOT NULL, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "taskType",
+ "columnName": "taskType",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "bookUrl",
+ "columnName": "bookUrl",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "chapterIndex",
+ "columnName": "chapterIndex",
+ "affinity": "INTEGER"
+ },
+ {
+ "fieldPath": "contentHash",
+ "columnName": "contentHash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "promptHash",
+ "columnName": "promptHash",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelProfileId",
+ "columnName": "modelProfileId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "status",
+ "columnName": "status",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "output",
+ "columnName": "output",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "errorMessage",
+ "columnName": "errorMessage",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "schemaVersion",
+ "columnName": "schemaVersion",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ai_artifacts_bookUrl_chapterIndex_taskType",
+ "unique": false,
+ "columnNames": [
+ "bookUrl",
+ "chapterIndex",
+ "taskType"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_artifacts_bookUrl_chapterIndex_taskType` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`, `taskType`)"
+ },
+ {
+ "name": "index_ai_artifacts_contentHash_promptHash_modelProfileId",
+ "unique": false,
+ "columnNames": [
+ "contentHash",
+ "promptHash",
+ "modelProfileId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_artifacts_contentHash_promptHash_modelProfileId` ON `${TABLE_NAME}` (`contentHash`, `promptHash`, `modelProfileId`)"
+ }
+ ]
+ },
+ {
+ "tableName": "ai_chat_conversations",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `title` TEXT NOT NULL, `reasoningLevel` TEXT NOT NULL, `modelProfileId` TEXT, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "reasoningLevel",
+ "columnName": "reasoningLevel",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "modelProfileId",
+ "columnName": "modelProfileId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "ai_chat_messages",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `conversationId` TEXT NOT NULL, `role` TEXT NOT NULL, `partsJson` TEXT NOT NULL, `createdAt` INTEGER NOT NULL, `branchIndex` INTEGER NOT NULL, `isSelected` INTEGER NOT NULL, `parentMessageId` TEXT, `thinkingDuration` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "conversationId",
+ "columnName": "conversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "role",
+ "columnName": "role",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "partsJson",
+ "columnName": "partsJson",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "createdAt",
+ "columnName": "createdAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "branchIndex",
+ "columnName": "branchIndex",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "isSelected",
+ "columnName": "isSelected",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "parentMessageId",
+ "columnName": "parentMessageId",
+ "affinity": "TEXT"
+ },
+ {
+ "fieldPath": "thinkingDuration",
+ "columnName": "thinkingDuration",
+ "affinity": "INTEGER",
+ "notNull": true,
+ "defaultValue": "0"
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ai_chat_messages_conversationId_createdAt",
+ "unique": false,
+ "columnNames": [
+ "conversationId",
+ "createdAt"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_chat_messages_conversationId_createdAt` ON `${TABLE_NAME}` (`conversationId`, `createdAt`)"
+ }
+ ]
+ },
+ {
+ "tableName": "ai_memory",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`conversationId` TEXT NOT NULL, `key` TEXT NOT NULL, `value` TEXT NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`conversationId`, `key`))",
+ "fields": [
+ {
+ "fieldPath": "conversationId",
+ "columnName": "conversationId",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "key",
+ "columnName": "key",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "value",
+ "columnName": "value",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "updatedAt",
+ "columnName": "updatedAt",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "conversationId",
+ "key"
+ ]
+ },
+ "indices": [
+ {
+ "name": "index_ai_memory_conversationId",
+ "unique": false,
+ "columnNames": [
+ "conversationId"
+ ],
+ "orders": [],
+ "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_memory_conversationId` ON `${TABLE_NAME}` (`conversationId`)"
+ }
+ ]
+ },
+ {
+ "tableName": "highlight_tag_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `title` TEXT NOT NULL, `pattern` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "title",
+ "columnName": "title",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "enabled",
+ "columnName": "enabled",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ },
+ {
+ "tableName": "tag_group_rules",
+ "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` INTEGER NOT NULL, `pattern` TEXT NOT NULL, `groupName` TEXT NOT NULL, `order` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+ "fields": [
+ {
+ "fieldPath": "id",
+ "columnName": "id",
+ "affinity": "INTEGER",
+ "notNull": true
+ },
+ {
+ "fieldPath": "pattern",
+ "columnName": "pattern",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "groupName",
+ "columnName": "groupName",
+ "affinity": "TEXT",
+ "notNull": true
+ },
+ {
+ "fieldPath": "order",
+ "columnName": "order",
+ "affinity": "INTEGER",
+ "notNull": true
+ }
+ ],
+ "primaryKey": {
+ "autoGenerate": false,
+ "columnNames": [
+ "id"
+ ]
+ }
+ }
+ ],
+ "views": [
+ {
+ "viewName": "book_sources_part",
+ "createSql": "CREATE VIEW `${VIEW_NAME}` AS select bookSourceUrl, bookSourceName, bookSourceGroup, customOrder, enabled, enabledExplore, \n (loginUrl is not null and trim(loginUrl) <> '') hasLoginUrl, lastUpdateTime, respondTime, weight, \n (exploreUrl is not null and trim(exploreUrl) <> '') hasExploreUrl \n from book_sources"
+ }
+ ],
+ "setupQueries": [
+ "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)",
+ "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '98b13caf6e843799cbd18dc9f6ebb6cb')"
+ ]
+ }
+}
\ No newline at end of file
diff --git a/app/src/debug/res/values-zh-rCN/strings.xml b/app/src/debug/res/values-zh-rCN/strings.xml
index e4464c88d..e56dea754 100644
--- a/app/src/debug/res/values-zh-rCN/strings.xml
+++ b/app/src/debug/res/values-zh-rCN/strings.xml
@@ -1,4 +1,4 @@
- 阅读·D
- 阅读·D·搜索
-
\ No newline at end of file
+ 且看
+ 且看·搜索
+
diff --git a/app/src/debug/res/values/strings.xml b/app/src/debug/res/values/strings.xml
index ffc41e3cd..e56dea754 100644
--- a/app/src/debug/res/values/strings.xml
+++ b/app/src/debug/res/values/strings.xml
@@ -1,4 +1,4 @@
- legado·D
- legado·D·search
-
\ No newline at end of file
+ 且看
+ 且看·搜索
+
diff --git a/app/src/main/java/io/legado/app/constant/PreferKey.kt b/app/src/main/java/io/legado/app/constant/PreferKey.kt
index d82bbd94f..8a671f04b 100644
--- a/app/src/main/java/io/legado/app/constant/PreferKey.kt
+++ b/app/src/main/java/io/legado/app/constant/PreferKey.kt
@@ -11,6 +11,7 @@ object PreferKey {
const val showUnread = "showUnread"
const val showUnreadNew = "showUnreadNew"
const val bookGroupStyle = "bookGroupStyle"
+ const val bookshelfPrivacyMode = "bookshelfPrivacyMode"
const val useDefaultCover = "useDefaultCover"
const val loadCoverOnlyWifi = "loadCoverOnlyWifi"
const val coverShowName = "coverShowName"
diff --git a/app/src/main/java/io/legado/app/data/AppDatabase.kt b/app/src/main/java/io/legado/app/data/AppDatabase.kt
index 187d0b071..599d8e490 100644
--- a/app/src/main/java/io/legado/app/data/AppDatabase.kt
+++ b/app/src/main/java/io/legado/app/data/AppDatabase.kt
@@ -90,7 +90,7 @@ val appDb by lazy {
}
@Database(
- version = 93,
+ version = 94,
exportSchema = true,
entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
@@ -153,7 +153,8 @@ val appDb by lazy {
AutoMigration(from = 89, to = 90),
AutoMigration(from = 90, to = 91),
AutoMigration(from = 91, to = 92),
- AutoMigration(from = 92, to = 93)
+ AutoMigration(from = 92, to = 93),
+ AutoMigration(from = 93, to = 94, spec = DatabaseMigrations.Migration_93_94::class)
]
)
abstract class AppDatabase : RoomDatabase() {
diff --git a/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt b/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt
index 6dcaa5559..04345770b 100644
--- a/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt
+++ b/app/src/main/java/io/legado/app/data/DatabaseMigrations.kt
@@ -496,4 +496,35 @@ object DatabaseMigrations {
columnName = "enabledReview"
)
class Migration_64_65 : AutoMigrationSpec
+
+ @Suppress("ClassName")
+ class Migration_93_94 : AutoMigrationSpec {
+
+ override fun onPostMigrate(db: SupportSQLiteDatabase) {
+ backfillBookUrl(db, "readRecord")
+ backfillBookUrl(db, "readRecordDetail")
+ backfillBookUrl(db, "readRecordSession")
+ }
+
+ private fun backfillBookUrl(db: SupportSQLiteDatabase, tableName: String) {
+ db.execSQL(
+ """
+ UPDATE `$tableName`
+ SET bookUrl = (
+ SELECT MIN(book.bookUrl)
+ FROM books AS book
+ WHERE TRIM(book.name) = TRIM(`$tableName`.bookName)
+ AND (`$tableName`.bookAuthor = '' OR book.author = `$tableName`.bookAuthor)
+ )
+ WHERE bookUrl IS NULL
+ AND 1 = (
+ SELECT COUNT(*)
+ FROM books AS book
+ WHERE TRIM(book.name) = TRIM(`$tableName`.bookName)
+ AND (`$tableName`.bookAuthor = '' OR book.author = `$tableName`.bookAuthor)
+ )
+ """.trimIndent()
+ )
+ }
+ }
}
diff --git a/app/src/main/java/io/legado/app/data/dao/BookDao.kt b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
index d4faef07d..fd145e056 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookDao.kt
@@ -22,11 +22,14 @@ data class GroupBookCount(
val count: Int
)
+private const val USER_GROUP_ID_FILTER =
+ "(groupId > 0 OR groupId = ${Long.MIN_VALUE})"
+
private const val PRIVATE_GROUP_MASK =
- "(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0 AND isPrivate = 1)"
+ "(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE $USER_GROUP_ID_FILTER AND isPrivate = 1)"
private const val PUBLIC_GROUP_MASK =
- "(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE groupId > 0 AND isPrivate = 0)"
+ "(SELECT COALESCE(SUM(groupId), 0) FROM book_groups WHERE $USER_GROUP_ID_FILTER AND isPrivate = 0)"
private const val PUBLIC_BOOK_FILTER =
"(`group` = 0 OR (`group` & $PRIVATE_GROUP_MASK) = 0)"
@@ -162,6 +165,21 @@ interface BookDao {
)
fun flowBookShelf(): Flow>
+ @Query(
+ """
+ SELECT
+ bookUrl, name, author, origin, originName,
+ coverUrl, customCoverUrl, durChapterTitle, durChapterTime,
+ durChapterPos, latestChapterTitle, latestChapterTime,
+ lastCheckCount, totalChapterNum, durChapterIndex,
+ type, `group`, `order`, canUpdate,
+ ifnull(customIntro, intro) as intro, kind, wordCount
+ FROM books
+ ORDER BY durChapterTime DESC
+ """
+ )
+ fun flowBookShelfIncludingPrivate(): Flow>
+
@Query("SELECT * FROM books WHERE type & ${BookType.audio} > 0")
fun flowAudio(): Flow>
@@ -235,7 +253,7 @@ interface BookDao {
@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
+ and ((SELECT sum(groupId) FROM book_groups where groupId > 0 or groupId = ${Long.MIN_VALUE}) & `group`) = 0
"""
)
fun flowNetNoGroup(): Flow>
@@ -276,7 +294,7 @@ interface BookDao {
@Query(
"""
select * from books where type & ${BookType.local} > 0
- and ((SELECT sum(groupId) FROM book_groups where groupId > 0) & `group`) = 0
+ and ((SELECT sum(groupId) FROM book_groups where groupId > 0 or groupId = ${Long.MIN_VALUE}) & `group`) = 0
"""
)
fun flowLocalNoGroup(): Flow>
@@ -314,7 +332,7 @@ interface BookDao {
)
fun flowBookShelfLocalNoGroup(): Flow>
- @Query("SELECT * FROM books WHERE (`group` & :group) > 0")
+ @Query("SELECT * FROM books WHERE (`group` & :group) != 0")
fun flowByUserGroup(group: Long): Flow>
@Query(
@@ -343,7 +361,7 @@ interface BookDao {
kind,
wordCount
FROM books
- WHERE (`group` & :group) > 0
+ WHERE (`group` & :group) != 0
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :group) = 1 OR $PUBLIC_BOOK_FILTER)
"""
)
@@ -671,7 +689,7 @@ interface BookDao {
)
fun flowBookShelfText(): Flow>
- @Query("SELECT * FROM books WHERE (`group` & :group) > 0")
+ @Query("SELECT * FROM books WHERE (`group` & :group) != 0")
fun getBooksByGroup(group: Long): List
@Query("SELECT * FROM books WHERE `name` in (:names)")
@@ -703,6 +721,15 @@ interface BookDao {
@Query("SELECT * FROM books WHERE name = :name and author = :author")
fun getBook(name: String, author: String): Book?
+ @Query(
+ """
+ SELECT * FROM books
+ WHERE name = :name AND author = :author
+ ORDER BY durChapterTime DESC, bookUrl ASC
+ """
+ )
+ fun getBooks(name: String, author: String): List
+
@Query("""select distinct bs.* from books, book_sources bs
where origin == bookSourceUrl and origin not like '${BookType.localTag}%'
and origin not like '${BookType.webDavTag}%'""")
@@ -774,7 +801,7 @@ interface BookDao {
@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")
+ @Query("update books set `group` = `group` & ~:group where `group` & :group != 0")
fun removeGroup(group: Long)
@Query("delete from books where type & ${BookType.notShelf} > 0")
@@ -818,7 +845,7 @@ interface BookDao {
@Query(
"""
SELECT COUNT(*) FROM books
- WHERE (`group` & :groupId) > 0
+ WHERE (`group` & :groupId) != 0
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
"""
)
@@ -1096,7 +1123,7 @@ interface BookDao {
type, `group`, `order`, canUpdate,
ifnull(customIntro, intro) as intro, kind, wordCount
FROM books
- WHERE (`group` & :groupId) > 0
+ WHERE (`group` & :groupId) != 0
AND ((SELECT isPrivate FROM book_groups WHERE groupId = :groupId) = 1 OR $PUBLIC_BOOK_FILTER)
ORDER BY durChapterTime DESC
LIMIT 10
diff --git a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
index d27ea74c0..b767262ae 100644
--- a/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/BookGroupDao.kt
@@ -27,16 +27,16 @@ interface BookGroupDao {
"""
with const as (
SELECT
- COALESCE(SUM(CASE WHEN groupId > 0 AND isPrivate = 0 THEN groupId ELSE 0 END), 0) sumPublicGroupId,
- COALESCE(SUM(CASE WHEN groupId > 0 AND isPrivate = 1 THEN groupId ELSE 0 END), 0) sumPrivateGroupId
+ COALESCE(SUM(CASE WHEN (groupId > 0 OR groupId = ${Long.MIN_VALUE}) AND isPrivate = 0 THEN groupId ELSE 0 END), 0) sumPublicGroupId,
+ COALESCE(SUM(CASE WHEN (groupId > 0 OR groupId = ${Long.MIN_VALUE}) AND isPrivate = 1 THEN groupId ELSE 0 END), 0) sumPrivateGroupId
FROM book_groups
)
SELECT book_groups.* FROM book_groups join const
where show > 0
and (
- (groupId >= 0 and exists (
+ ((groupId >= 0 or groupId = ${Long.MIN_VALUE}) and exists (
select 1 from books
- where `group` & book_groups.groupId > 0
+ where `group` & book_groups.groupId != 0
and (book_groups.isPrivate = 1 or `group` = 0 or (const.sumPrivateGroupId & `group`) = 0)
))
or groupId = ${BookGroup.IdAll}
@@ -124,13 +124,13 @@ interface BookGroupDao {
)
val show: LiveData>
- @Query("SELECT * FROM book_groups where groupId >= 0 ORDER BY `order`")
+ @Query("SELECT * FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE} ORDER BY `order`")
fun flowSelect(): Flow>
- @get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0")
+ @get:Query("SELECT sum(groupId) FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE}")
val idsSum: Long
- @get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0")
+ @get:Query("SELECT MAX(`order`) FROM book_groups where groupId >= 0 or groupId = ${Long.MIN_VALUE}")
val maxOrder: Int
@get:Query("SELECT * FROM book_groups ORDER BY `order`")
@@ -142,7 +142,7 @@ interface BookGroupDao {
@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")
+ @Query("select groupName from book_groups where (groupId > 0 or groupId = ${Long.MIN_VALUE}) and (groupId & :id) != 0")
fun getGroupNames(id: Long): List
@Insert(onConflict = OnConflictStrategy.REPLACE)
diff --git a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
index 4035116c0..e68ad361b 100644
--- a/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
+++ b/app/src/main/java/io/legado/app/data/dao/ReadRecordDao.kt
@@ -5,6 +5,7 @@ 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.legado.app.data.entities.readRecord.HomeRecentBookRow
import io.legado.app.data.entities.readRecord.ReadRecord
@@ -44,17 +45,26 @@ interface ReadRecordDao {
"""
WITH recent AS (
SELECT
- bookName,
- bookAuthor,
- MAX(lastRead) AS lastRead
- FROM readRecord
- GROUP BY bookName, bookAuthor
- ORDER BY lastRead DESC
- LIMIT :limit
+ base.bookName,
+ base.bookAuthor,
+ (
+ SELECT latest.bookUrl
+ FROM readRecord AS latest
+ WHERE latest.bookName = base.bookName
+ AND latest.bookAuthor = base.bookAuthor
+ AND latest.bookUrl IS NOT NULL
+ AND latest.bookUrl != ''
+ ORDER BY latest.lastRead DESC, latest.deviceId ASC
+ LIMIT 1
+ ) AS recordBookUrl,
+ MAX(base.lastRead) AS lastRead
+ FROM readRecord AS base
+ GROUP BY base.bookName, base.bookAuthor
)
SELECT
recent.bookName AS recordName,
recent.bookAuthor AS recordAuthor,
+ recent.recordBookUrl AS recordBookUrl,
book.bookUrl AS bookUrl,
book.origin AS origin,
book.coverUrl AS coverUrl,
@@ -63,18 +73,26 @@ interface ReadRecordDao {
book.totalChapterNum AS totalChapterNum,
book.durChapterIndex AS chapterIndex
FROM recent
- LEFT JOIN books AS book ON book.bookUrl = (
- SELECT candidate.bookUrl
- FROM books AS candidate
- WHERE candidate.name = recent.bookName
- AND candidate.author = recent.bookAuthor
- ORDER BY candidate.durChapterTime DESC, candidate.bookUrl ASC
- LIMIT 1
+ LEFT JOIN books AS book ON book.bookUrl = COALESCE(
+ (
+ SELECT currentBook.bookUrl
+ FROM books AS currentBook
+ WHERE currentBook.bookUrl = recent.recordBookUrl
+ LIMIT 1
+ ),
+ (
+ SELECT candidate.bookUrl
+ FROM books AS candidate
+ WHERE candidate.name = recent.bookName
+ AND candidate.author = recent.bookAuthor
+ ORDER BY candidate.durChapterTime DESC, candidate.bookUrl ASC
+ LIMIT 1
+ )
)
ORDER BY recent.lastRead DESC
"""
)
- fun observeRecentHomeBooks(limit: Int): Flow>
+ fun observeRecentHomeBooks(): Flow>
@Query("select sum(readTime) from readRecord where bookName = :bookName")
fun getReadTime(bookName: String): Long?
@@ -85,6 +103,33 @@ interface ReadRecordDao {
@Query("SELECT * FROM readRecord WHERE deviceId = :deviceId AND bookName = :bookName AND bookAuthor = :bookAuthor")
suspend fun getReadRecord(deviceId: String, bookName: String, bookAuthor: String): ReadRecord?
+ @Query(
+ """
+ SELECT bookUrl FROM books
+ WHERE TRIM(name) = TRIM(:bookName)
+ AND (:bookAuthor = '' OR author = :bookAuthor)
+ ORDER BY durChapterTime DESC, bookUrl ASC
+ LIMIT 2
+ """
+ )
+ suspend fun findMatchingBookUrls(bookName: String, bookAuthor: String): List
+
+ @Query("UPDATE readRecord SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
+ fun replaceReadRecordBookUrl(oldBookUrl: String, newBookUrl: String)
+
+ @Query("UPDATE readRecordDetail SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
+ fun replaceReadRecordDetailBookUrl(oldBookUrl: String, newBookUrl: String)
+
+ @Query("UPDATE readRecordSession SET bookUrl = :newBookUrl WHERE bookUrl = :oldBookUrl")
+ fun replaceReadRecordSessionBookUrl(oldBookUrl: String, newBookUrl: String)
+
+ @Transaction
+ fun replaceBookUrl(oldBookUrl: String, newBookUrl: String) {
+ replaceReadRecordBookUrl(oldBookUrl, newBookUrl)
+ replaceReadRecordDetailBookUrl(oldBookUrl, newBookUrl)
+ replaceReadRecordSessionBookUrl(oldBookUrl, newBookUrl)
+ }
+
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(vararg readRecord: ReadRecord)
diff --git a/app/src/main/java/io/legado/app/data/entities/readRecord/HomeRecentBookRow.kt b/app/src/main/java/io/legado/app/data/entities/readRecord/HomeRecentBookRow.kt
index 3f0fd6ddc..39ee1c7f5 100644
--- a/app/src/main/java/io/legado/app/data/entities/readRecord/HomeRecentBookRow.kt
+++ b/app/src/main/java/io/legado/app/data/entities/readRecord/HomeRecentBookRow.kt
@@ -3,6 +3,7 @@ package io.legado.app.data.entities.readRecord
data class HomeRecentBookRow(
val recordName: String,
val recordAuthor: String,
+ val recordBookUrl: String?,
val bookUrl: String?,
val origin: String?,
val coverUrl: String?,
diff --git a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecord.kt b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecord.kt
index fa9a57cd7..74cf860f7 100644
--- a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecord.kt
+++ b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecord.kt
@@ -9,8 +9,9 @@ data class ReadRecord(
var bookName: String = "",
@ColumnInfo(defaultValue = "")
var bookAuthor: String = "",
+ var bookUrl: String? = null,
@ColumnInfo(defaultValue = "0")
var readTime: Long = 0L,
@ColumnInfo(defaultValue = "0")
var lastRead: Long = System.currentTimeMillis()
-)
\ No newline at end of file
+)
diff --git a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordDetail.kt b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordDetail.kt
index ca929e2a2..3a082f5d6 100644
--- a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordDetail.kt
+++ b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordDetail.kt
@@ -12,6 +12,7 @@ data class ReadRecordDetail(
val bookName: String = "",
@ColumnInfo(defaultValue = "")
val bookAuthor: String = "",
+ val bookUrl: String? = null,
val date: String = "",
// 当天阅读总时长
diff --git a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordSession.kt b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordSession.kt
index 8d9d4b861..452a86e70 100644
--- a/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordSession.kt
+++ b/app/src/main/java/io/legado/app/data/entities/readRecord/ReadRecordSession.kt
@@ -13,6 +13,7 @@ data class ReadRecordSession(
val bookName: String = "",
@ColumnInfo(defaultValue = "")
val bookAuthor: String = "",
+ val bookUrl: String? = null,
// 一次阅读的开始/结束
val startTime: Long = 0,
diff --git a/app/src/main/java/io/legado/app/data/repository/AiToolRepository.kt b/app/src/main/java/io/legado/app/data/repository/AiToolRepository.kt
index ae0cd2a65..395b7d5aa 100644
--- a/app/src/main/java/io/legado/app/data/repository/AiToolRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/AiToolRepository.kt
@@ -19,6 +19,7 @@ import io.legado.app.help.book.ContentProcessor
import io.legado.app.utils.GSON
import io.legado.app.utils.MD5Utils
import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.withContext
class AiToolRepository(
@@ -27,7 +28,8 @@ class AiToolRepository(
private val bookmarkDao: BookmarkDao,
private val readRecordDao: ReadRecordDao,
private val aiArtifactDao: AiArtifactDao,
- private val aiMemoryGateway: AiMemoryGateway
+ private val aiMemoryGateway: AiMemoryGateway,
+ private val bookPrivacyRepository: BookPrivacyRepository,
) : AiToolGateway {
override fun availableTools(): List = tools
@@ -175,11 +177,15 @@ class AiToolRepository(
return GSON.toJson(mapOf("bookmarks" to bookmarks))
}
- private fun getReadingStats(args: JsonObject): String {
+ private suspend fun getReadingStats(args: JsonObject): String {
val query = args.string("query").orEmpty().trim()
val date = args.string("date")?.trim().orEmpty()
val limit = args.int("limit", 10).coerceIn(1, 30)
- val records = readRecordDao.all
+ val privacyState = bookPrivacyRepository.observe().first()
+ val visibleRecords = readRecordDao.all.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
+ val records = visibleRecords
.asSequence()
.filter {
query.isBlank() ||
@@ -200,7 +206,8 @@ class AiToolRepository(
val dailyDetails = readRecordDao.allDetail
.asSequence()
.filter {
- (date.isBlank() || it.date == date) &&
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor) &&
+ (date.isBlank() || it.date == date) &&
(query.isBlank() ||
it.bookName.contains(query, ignoreCase = true) ||
it.bookAuthor.contains(query, ignoreCase = true))
@@ -222,7 +229,7 @@ class AiToolRepository(
.toList()
return GSON.toJson(
mapOf(
- "totalReadTimeMillis" to readRecordDao.all.sumOf { it.readTime },
+ "totalReadTimeMillis" to visibleRecords.sumOf { it.readTime },
"recentRecords" to records,
"dailyDetails" to dailyDetails
)
diff --git a/app/src/main/java/io/legado/app/data/repository/BookPrivacyRepository.kt b/app/src/main/java/io/legado/app/data/repository/BookPrivacyRepository.kt
new file mode 100644
index 000000000..1402af4a9
--- /dev/null
+++ b/app/src/main/java/io/legado/app/data/repository/BookPrivacyRepository.kt
@@ -0,0 +1,102 @@
+package io.legado.app.data.repository
+
+import io.legado.app.constant.PreferKey
+import io.legado.app.data.dao.BookDao
+import io.legado.app.data.dao.BookGroupDao
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookGroup
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import java.text.Normalizer
+import java.util.Locale
+
+data class BookRecordKey(
+ val name: String,
+ val author: String,
+)
+
+data class BookPrivacyState(
+ val privacyModeEnabled: Boolean,
+ val privateGroupMask: Long,
+ val visibleBookUrls: Set,
+ val visibleBookKeys: Set,
+ val visibleBookNames: Set,
+ val hiddenBookUrls: Set,
+ val hiddenBookKeys: Set,
+ val hiddenBookNames: Set,
+) {
+
+ fun isReadRecordVisible(
+ bookUrl: String?,
+ bookName: String,
+ bookAuthor: String = "",
+ ): Boolean {
+ if (!privacyModeEnabled) return true
+ if (!bookUrl.isNullOrBlank()) {
+ if (bookUrl in hiddenBookUrls) return false
+ if (bookUrl in visibleBookUrls) return true
+ }
+ val normalizedName = normalizeBookName(bookName)
+ val normalizedAuthor = normalizeBookName(bookAuthor)
+ if (normalizedAuthor.isNotEmpty()) {
+ val key = BookRecordKey(normalizedName, normalizedAuthor)
+ if (key in hiddenBookKeys) return false
+ if (key in visibleBookKeys) return true
+ }
+ return normalizedName in visibleBookNames && normalizedName !in hiddenBookNames
+ }
+
+ companion object {
+ fun create(
+ books: List,
+ groups: List,
+ privacyModeEnabled: Boolean,
+ ): BookPrivacyState {
+ val privateGroupMask = groups.asSequence()
+ .filter {
+ (it.groupId > 0L || it.groupId == Long.MIN_VALUE) && it.isPrivate
+ }
+ .fold(0L) { mask, group -> mask or group.groupId }
+ val hiddenBooks = books.filter { (it.group and privateGroupMask) != 0L }
+ val visibleBooks = books.filterNot { (it.group and privateGroupMask) != 0L }
+ return BookPrivacyState(
+ privacyModeEnabled = privacyModeEnabled,
+ privateGroupMask = privateGroupMask,
+ visibleBookUrls = visibleBooks.mapTo(hashSetOf()) { it.bookUrl },
+ visibleBookKeys = visibleBooks.mapTo(hashSetOf()) {
+ BookRecordKey(normalizeBookName(it.name), normalizeBookName(it.author))
+ },
+ visibleBookNames = visibleBooks.mapTo(hashSetOf()) {
+ normalizeBookName(it.name)
+ },
+ hiddenBookUrls = hiddenBooks.mapTo(hashSetOf()) { it.bookUrl },
+ hiddenBookKeys = hiddenBooks.mapTo(hashSetOf()) {
+ BookRecordKey(normalizeBookName(it.name), normalizeBookName(it.author))
+ },
+ hiddenBookNames = hiddenBooks.mapTo(hashSetOf()) {
+ normalizeBookName(it.name)
+ },
+ )
+ }
+ }
+}
+
+class BookPrivacyRepository(
+ private val bookDao: BookDao,
+ private val bookGroupDao: BookGroupDao,
+ private val settingsRepository: SettingsRepository,
+) {
+
+ fun observe(): Flow = combine(
+ bookDao.flowAll(),
+ bookGroupDao.flowAll(),
+ settingsRepository.getBoolean(PreferKey.bookshelfPrivacyMode, true),
+ BookPrivacyState::create,
+ ).distinctUntilChanged()
+}
+
+internal fun normalizeBookName(name: String): String =
+ Normalizer.normalize(name, Normalizer.Form.NFKC)
+ .lowercase(Locale.ROOT)
+ .filterNot { it.isWhitespace() || it == '\u200B' || it == '\uFEFF' }
diff --git a/app/src/main/java/io/legado/app/data/repository/BookRepository.kt b/app/src/main/java/io/legado/app/data/repository/BookRepository.kt
index 358e5005c..eab6c1880 100644
--- a/app/src/main/java/io/legado/app/data/repository/BookRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/BookRepository.kt
@@ -8,32 +8,48 @@ import io.legado.app.data.entities.BookChapter
import io.legado.app.ui.main.bookshelf.BookShelfItem
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.withContext
class BookRepository(
private val bookDao: BookDao,
- private val bookChapterDao: BookChapterDao
+ private val bookChapterDao: BookChapterDao,
+ private val bookPrivacyRepository: BookPrivacyRepository,
) {
fun getAllBooks(): Flow> {
return bookDao.flowAll()
}
- suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
- return withContext(Dispatchers.IO) {
- bookDao.getBook(bookName, bookAuthor)?.getDisplayCover()
+ fun getVisibleBooks(): Flow> {
+ return combine(bookDao.flowAll(), bookPrivacyRepository.observe()) { books, privacyState ->
+ books.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.name, it.author)
+ }
}
}
- suspend fun getChapterTitle(bookName: String, bookAuthor: String, chapterIndex: Int): String? {
- return withContext(Dispatchers.IO) {
- val book = bookDao.getBook(bookName, bookAuthor)
- val bookUrl = book?.bookUrl
- if (bookUrl.isNullOrEmpty()) return@withContext null
+ suspend fun getBookCoverByNameAndAuthor(bookName: String, bookAuthor: String): String? {
+ return getVisibleBook(bookName, bookAuthor)?.getDisplayCover()
+ }
+ suspend fun getChapterTitle(bookName: String, bookAuthor: String, chapterIndex: Int): String? {
+ val bookUrl = getVisibleBook(bookName, bookAuthor)?.bookUrl ?: return null
+ return withContext(Dispatchers.IO) {
bookChapterDao.getChapterTitleByUrlAndIndex(bookUrl, chapterIndex)
}
}
+ suspend fun getVisibleBook(name: String, author: String): Book? {
+ return withContext(Dispatchers.IO) {
+ val privacyState = bookPrivacyRepository.observe().first()
+ bookDao.getBooks(name, author).firstOrNull {
+ privacyState.isReadRecordVisible(it.bookUrl, it.name, it.author)
+ }
+ }
+ }
+
suspend fun getBook(bookUrl: String): Book? {
return withContext(Dispatchers.IO) {
bookDao.getBook(bookUrl)
@@ -50,6 +66,12 @@ class BookRepository(
return bookDao.flowBookShelfByGroup(groupId)
}
+ fun flowBookShelfIncludingPrivate(): Flow> {
+ return bookDao.flowBookShelfIncludingPrivate().map { books ->
+ books.filterNot { it.isNotShelf }
+ }
+ }
+
fun flowSystemGroupCounts(): Flow> {
return bookDao.flowSystemGroupCounts()
}
diff --git a/app/src/main/java/io/legado/app/data/repository/HomeDashboardRepository.kt b/app/src/main/java/io/legado/app/data/repository/HomeDashboardRepository.kt
index 9793543b7..e4cd01e30 100644
--- a/app/src/main/java/io/legado/app/data/repository/HomeDashboardRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/HomeDashboardRepository.kt
@@ -8,50 +8,67 @@ import io.legado.app.domain.model.DEFAULT_HOME_DASHBOARD_SECTIONS
import io.legado.app.domain.model.HomeDashboardSection
import io.legado.app.domain.model.HomeReadingBook
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.map
class HomeDashboardRepository(
private val readRecordDao: ReadRecordDao,
private val localPreferencesRepository: LocalPreferencesRepository,
+ private val readRecordRepository: ReadRecordRepository,
+ private val bookPrivacyRepository: BookPrivacyRepository,
) : HomeDashboardGateway {
override fun observeTotalReadBooks(): Flow =
- readRecordDao.observeTotalReadBookCount()
+ readRecordRepository.getTotalReadBookCount()
override fun observeTotalReadTime(): Flow =
- readRecordDao.getTotalReadTime().map { it ?: 0L }
+ readRecordRepository.getTotalReadTime()
override fun observeReadTime(date: String): Flow =
- readRecordDao.observeReadTimeByDate(date).map { it ?: 0L }
+ readRecordRepository.getReadTime(date)
- override fun observeRecentBooks(limit: Int): Flow> =
- readRecordDao.observeRecentHomeBooks(limit).map { rows ->
- rows.map { row ->
- HomeReadingBook(
- bookUrl = row.bookUrl,
- name = row.recordName,
- author = row.recordAuthor,
- origin = row.origin,
- coverPath = if (row.customCoverUrl.isNullOrEmpty()) {
- row.coverUrl
- } else {
- row.customCoverUrl
- },
- chapterTitle = row.chapterTitle,
- chapterProgress = if (
- row.totalChapterNum != null &&
- row.totalChapterNum > 0 &&
- row.chapterIndex != null
- ) {
- (row.chapterIndex + 1)
- .coerceIn(0, row.totalChapterNum)
- .toFloat() / row.totalChapterNum
- } else {
- null
- },
- )
- }
+ override fun observeRecentBooks(limit: Int): Flow> {
+ return combine(
+ readRecordDao.observeRecentHomeBooks(),
+ bookPrivacyRepository.observe(),
+ ) { rows, privacyState ->
+ rows.asSequence()
+ .filter { row ->
+ privacyState.isReadRecordVisible(
+ row.recordBookUrl,
+ row.recordName,
+ row.recordAuthor,
+ )
+ }
+ .take(limit)
+ .map { row ->
+ HomeReadingBook(
+ bookUrl = row.bookUrl,
+ name = row.recordName,
+ author = row.recordAuthor,
+ origin = row.origin,
+ coverPath = if (row.customCoverUrl.isNullOrEmpty()) {
+ row.coverUrl
+ } else {
+ row.customCoverUrl
+ },
+ chapterTitle = row.chapterTitle,
+ chapterProgress = if (
+ row.totalChapterNum != null &&
+ row.totalChapterNum > 0 &&
+ row.chapterIndex != null
+ ) {
+ (row.chapterIndex + 1)
+ .coerceIn(0, row.totalChapterNum)
+ .toFloat() / row.totalChapterNum
+ } else {
+ null
+ },
+ )
+ }
+ .toList()
}
+ }
override fun observeDailyGoal(defaultValue: Int): Flow =
localPreferencesRepository.getPreference(
diff --git a/app/src/main/java/io/legado/app/data/repository/ReadRecordRepository.kt b/app/src/main/java/io/legado/app/data/repository/ReadRecordRepository.kt
index 53367eb20..991b471ca 100644
--- a/app/src/main/java/io/legado/app/data/repository/ReadRecordRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/ReadRecordRepository.kt
@@ -10,6 +10,8 @@ import io.legado.app.data.entities.readRecord.ReadRecordDetail
import io.legado.app.data.entities.readRecord.ReadRecordSession
import io.legado.app.data.entities.readRecord.ReadRecordTimelineDay
import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import java.util.Date
import kotlin.math.max
@@ -18,6 +20,7 @@ import kotlin.math.min
class ReadRecordRepository(
private val dao: ReadRecordDao,
private val database: AppDatabase,
+ private val bookPrivacyRepository: BookPrivacyRepository,
) {
private fun getCurrentDeviceId(): String = ""
@@ -25,37 +28,73 @@ class ReadRecordRepository(
* 获取总阅读时长流
*/
fun getTotalReadTime(): Flow {
- return dao.getTotalReadTime().map { it ?: 0L }
+ return getLatestReadRecords().map { records -> records.sumOf { it.readTime } }
+ }
+
+ fun getTotalReadBookCount(): Flow {
+ return getLatestReadRecords().map { records ->
+ records.distinctBy { it.bookName to it.bookAuthor }.size
+ }
+ }
+
+ fun getReadTime(date: String): Flow {
+ return getAllRecordDetails().map { details ->
+ details.filter { it.date == date }.sumOf { it.readTime }
+ }
}
/**
* 根据搜索关键字获取最新的阅读书籍列表流
*/
fun getLatestReadRecords(query: String = ""): Flow> {
- return if (query.isBlank()) {
+ val records = if (query.isBlank()) {
dao.getAllReadRecordsSortedByLastRead()
} else {
dao.searchReadRecordsByLastRead(query)
}
+ return combine(records, bookPrivacyRepository.observe()) { items, privacyState ->
+ items.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
+ }
}
/**
* 获取所有的每日统计详情流
*/
fun getAllRecordDetails(query: String = ""): Flow> {
- return if (query.isBlank()) {
+ val details = if (query.isBlank()) {
dao.getAllDetails()
} else {
dao.searchDetails(query)
}
+ return combine(details, bookPrivacyRepository.observe()) { items, privacyState ->
+ items.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
+ }
}
fun getAllSessions(): Flow> {
- return dao.getAllSessions(getCurrentDeviceId())
+ return combine(
+ dao.getAllSessions(getCurrentDeviceId()),
+ bookPrivacyRepository.observe(),
+ ) { sessions, privacyState ->
+ sessions.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
+ }
}
fun getBookSessions(bookName: String, bookAuthor: String): Flow> {
- return dao.getSessionsByBookFlow(getCurrentDeviceId(), bookName, bookAuthor)
+ return combine(
+ dao.getSessionsByBookFlow(getCurrentDeviceId(), bookName, bookAuthor),
+ bookPrivacyRepository.observe(),
+ ) { sessions, privacyState ->
+ sessions.filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
+ }
}
fun getBookTimelineDays(bookName: String, bookAuthor: String): Flow> {
@@ -72,15 +111,27 @@ class ReadRecordRepository(
}
fun getBookReadTime(bookName: String, bookAuthor: String): Flow {
- return dao.getReadTimeFlow(getCurrentDeviceId(), bookName, bookAuthor).map { it ?: 0L }
+ return combine(
+ dao.getReadTimeFlow(getCurrentDeviceId(), bookName, bookAuthor),
+ bookPrivacyRepository.observe(),
+ ) { readTime, privacyState ->
+ if (privacyState.isReadRecordVisible(null, bookName, bookAuthor)) {
+ readTime ?: 0L
+ } else {
+ 0L
+ }
+ }
}
suspend fun getMergeCandidates(targetRecord: ReadRecord): List {
+ val privacyState = bookPrivacyRepository.observe().first()
return dao.getReadRecordsByNameExcludingAuthor(
targetRecord.deviceId,
targetRecord.bookName,
targetRecord.bookAuthor
- )
+ ).filter {
+ privacyState.isReadRecordVisible(it.bookUrl, it.bookName, it.bookAuthor)
+ }
}
/**
@@ -104,6 +155,7 @@ class ReadRecordRepository(
if (existingRecord != null) {
dao.update(
existingRecord.copy(
+ bookUrl = session.bookUrl ?: existingRecord.bookUrl,
readTime = existingRecord.readTime + durationDelta,
lastRead = session.endTime
)
@@ -114,6 +166,7 @@ class ReadRecordRepository(
deviceId = session.deviceId,
bookName = session.bookName,
bookAuthor = session.bookAuthor,
+ bookUrl = session.bookUrl,
readTime = durationDelta,
lastRead = session.endTime
)
@@ -135,17 +188,22 @@ class ReadRecordRepository(
dateString
)
if (existingDetail != null) {
- existingDetail.readTime += durationDelta
- existingDetail.readWords += wordsDelta
- existingDetail.firstReadTime = min(existingDetail.firstReadTime, session.startTime)
- existingDetail.lastReadTime = max(existingDetail.lastReadTime, session.endTime)
- dao.insertDetail(existingDetail)
+ dao.insertDetail(
+ existingDetail.copy(
+ bookUrl = session.bookUrl ?: existingDetail.bookUrl,
+ readTime = existingDetail.readTime + durationDelta,
+ readWords = existingDetail.readWords + wordsDelta,
+ firstReadTime = min(existingDetail.firstReadTime, session.startTime),
+ lastReadTime = max(existingDetail.lastReadTime, session.endTime),
+ )
+ )
} else {
dao.insertDetail(
ReadRecordDetail(
deviceId = session.deviceId,
bookName = session.bookName,
bookAuthor = session.bookAuthor,
+ bookUrl = session.bookUrl,
date = dateString,
readTime = durationDelta,
readWords = wordsDelta,
@@ -204,6 +262,7 @@ class ReadRecordRepository(
)
dao.insertDetail(
existingDetail?.copy(
+ bookUrl = existingDetail.bookUrl ?: remainingSessions.resolvedBookUrl(),
readTime = totalTime,
readWords = totalWords,
firstReadTime = firstRead,
@@ -212,6 +271,7 @@ class ReadRecordRepository(
deviceId = session.deviceId,
bookName = session.bookName,
bookAuthor = session.bookAuthor,
+ bookUrl = remainingSessions.resolvedBookUrl(),
date = dateString,
readTime = totalTime,
readWords = totalWords,
@@ -233,6 +293,7 @@ class ReadRecordRepository(
} else {
val totalTime = allRemainingSessions.sumOf { it.endTime - it.startTime }
val lastRead = allRemainingSessions.maxOf { it.endTime }
+ val bookUrl = allRemainingSessions.resolvedBookUrl()
val existingRecord = dao.getReadRecord(deviceId, bookName, bookAuthor)
if (existingRecord == null) {
@@ -241,6 +302,7 @@ class ReadRecordRepository(
deviceId = deviceId,
bookName = bookName,
bookAuthor = bookAuthor,
+ bookUrl = bookUrl,
readTime = totalTime,
lastRead = lastRead,
)
@@ -248,6 +310,7 @@ class ReadRecordRepository(
} else {
dao.update(
existingRecord.copy(
+ bookUrl = existingRecord.bookUrl ?: bookUrl,
readTime = totalTime,
lastRead = lastRead
)
@@ -288,9 +351,11 @@ class ReadRecordRepository(
targetRecord.bookName,
targetRecord.bookAuthor
) ?: targetRecord
+ val targetBookUrl = target.bookUrl ?: source.bookUrl
dao.insert(
target.copy(
+ bookUrl = targetBookUrl,
readTime = target.readTime + source.readTime,
lastRead = max(target.lastRead, source.lastRead)
)
@@ -311,12 +376,14 @@ class ReadRecordRepository(
if (existingTargetDetail == null) {
dao.insertDetail(
detail.copy(
- bookAuthor = targetRecord.bookAuthor
+ bookAuthor = targetRecord.bookAuthor,
+ bookUrl = targetBookUrl,
)
)
} else {
dao.insertDetail(
existingTargetDetail.copy(
+ bookUrl = targetBookUrl ?: existingTargetDetail.bookUrl,
readTime = existingTargetDetail.readTime + detail.readTime,
readWords = existingTargetDetail.readWords + detail.readWords,
firstReadTime = min(existingTargetDetail.firstReadTime, detail.firstReadTime),
@@ -333,11 +400,22 @@ class ReadRecordRepository(
sourceRecord.bookAuthor
)
sourceSessions.forEach { session ->
- dao.updateSession(session.copy(bookAuthor = targetRecord.bookAuthor))
+ dao.updateSession(
+ session.copy(
+ bookAuthor = targetRecord.bookAuthor,
+ bookUrl = targetBookUrl,
+ )
+ )
}
dao.deleteReadRecord(source)
updateReadRecordTotal(targetRecord.deviceId, targetRecord.bookName, targetRecord.bookAuthor)
}
+ private fun List.resolvedBookUrl(): String? =
+ asSequence()
+ .mapNotNull { it.bookUrl?.takeIf(String::isNotBlank) }
+ .distinct()
+ .singleOrNull()
+
}
diff --git a/app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt b/app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt
index 2d73b08ac..cdcdd5d3e 100644
--- a/app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/ReadSettingsRepository.kt
@@ -410,7 +410,7 @@ class ReadSettingsRepository(
textBottomJustify = this[Keys.TextBottomJustify] ?: true,
adaptSpecialStyle = this[Keys.AdaptSpecialStyle] ?: true,
useZhLayout = this[Keys.UseZhLayout] ?: false,
- showBrightnessView = this[Keys.ShowBrightnessView] ?: "1",
+ showBrightnessView = getStringCompat(PreferKey.showBrightnessView, "1"),
brightnessVwPos = this[Keys.BrightnessVwPos] ?: "1",
readBrightness = this[Keys.ReadBrightness] ?: 100,
brightnessAuto = this[Keys.BrightnessAuto] ?: false,
diff --git a/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt b/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt
index 65b55b489..c5ca5b0f9 100644
--- a/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt
+++ b/app/src/main/java/io/legado/app/data/repository/SettingsRepository.kt
@@ -27,6 +27,17 @@ val Context.dataStore: DataStore by preferencesDataStore(
}
)
+internal fun normalizeLegacyPreferenceValue(key: String, value: Any): Any =
+ when {
+ key == PreferKey.showBrightnessView && value is Boolean -> if (value) "1" else "0"
+ else -> value
+ }
+
+internal fun Preferences.getStringCompat(key: String, defaultValue: String): String {
+ val value = asMap().entries.firstOrNull { it.key.name == key }?.value ?: return defaultValue
+ return normalizeLegacyPreferenceValue(key, value) as? String ?: defaultValue
+}
+
/**
* 设置仓储
* 以 DataStore 为唯一写入源,读取以 DataStore 为准。
@@ -110,15 +121,15 @@ class SettingsRepository(private val context: Context) {
suspend fun batchPutFromMap(map: Map) {
dataStore.edit { preferences ->
map.forEach { (key, value) ->
- when (value) {
- is String -> preferences[stringPreferencesKey(key)] = value
- is Int -> preferences[intPreferencesKey(key)] = value
- is Boolean -> preferences[booleanPreferencesKey(key)] = value
- is Long -> preferences[longPreferencesKey(key)] = value
- is Float -> preferences[floatPreferencesKey(key)] = value
+ when (val normalizedValue = value?.let { normalizeLegacyPreferenceValue(key, it) }) {
+ is String -> preferences[stringPreferencesKey(key)] = normalizedValue
+ is Int -> preferences[intPreferencesKey(key)] = normalizedValue
+ is Boolean -> preferences[booleanPreferencesKey(key)] = normalizedValue
+ is Long -> preferences[longPreferencesKey(key)] = normalizedValue
+ is Float -> preferences[floatPreferencesKey(key)] = normalizedValue
is Set<*> -> {
@Suppress("UNCHECKED_CAST")
- preferences[stringSetPreferencesKey(key)] = value as Set
+ preferences[stringSetPreferencesKey(key)] = normalizedValue as Set
}
}
}
diff --git a/app/src/main/java/io/legado/app/di/appModule.kt b/app/src/main/java/io/legado/app/di/appModule.kt
index f2a9f13cd..33f8de7a6 100644
--- a/app/src/main/java/io/legado/app/di/appModule.kt
+++ b/app/src/main/java/io/legado/app/di/appModule.kt
@@ -18,6 +18,7 @@ import io.legado.app.data.repository.BackupRestoreRepository
import io.legado.app.data.repository.BookCacheCleanupRepository
import io.legado.app.data.repository.BookDomainRepositoryImpl
import io.legado.app.data.repository.BookGroupRepository
+import io.legado.app.data.repository.BookPrivacyRepository
import io.legado.app.data.repository.BookRepository
import io.legado.app.data.repository.BookSourceCallbackRepository
import io.legado.app.data.repository.BookSourceRepository
@@ -179,8 +180,11 @@ val appModule = module {
single { get().bookGroupDao }
single { get().bookSourceDao }
+ singleOf(::BookPrivacyRepository)
singleOf(::ReadRecordRepository)
- single { HomeDashboardRepository(get(), get()) }
+ single {
+ HomeDashboardRepository(get(), get(), get(), get())
+ }
singleOf(::BookRepository)
singleOf(::BookGroupRepository)
singleOf(::BookSourceRepository)
@@ -230,7 +234,9 @@ val appModule = module {
single { AiChatRepository(get()) }
single { AiMemoryRepository(get()) }
single { AiTextRepositoryImpl() }
- single { AiToolRepository(get(), get(), get(), get(), get(), get()) }
+ single {
+ AiToolRepository(get(), get(), get(), get(), get(), get(), get())
+ }
single { AppStartupRepository(get()) }
single { BackupRestoreRepository() }
single { CacheBookDownloadRepository(get()) }
diff --git a/app/src/main/java/io/legado/app/domain/usecase/ChangeBookSourceUseCase.kt b/app/src/main/java/io/legado/app/domain/usecase/ChangeBookSourceUseCase.kt
index e972954c2..b7e5a3cb3 100644
--- a/app/src/main/java/io/legado/app/domain/usecase/ChangeBookSourceUseCase.kt
+++ b/app/src/main/java/io/legado/app/domain/usecase/ChangeBookSourceUseCase.kt
@@ -4,6 +4,7 @@ import io.legado.app.constant.AppLog
import io.legado.app.constant.BookType
import io.legado.app.data.dao.BookChapterDao
import io.legado.app.data.dao.BookDao
+import io.legado.app.data.dao.ReadRecordDao
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookChapter
import io.legado.app.data.entities.BookSource
@@ -68,6 +69,7 @@ enum class BatchChangeSourcePreviewStatus {
class ChangeBookSourceUseCase(
private val bookDao: BookDao,
private val bookChapterDao: BookChapterDao,
+ private val readRecordDao: ReadRecordDao,
) {
fun applyMigration(
@@ -97,6 +99,7 @@ class ChangeBookSourceUseCase(
bookChapterDao.delByBook(oldBook.bookUrl)
bookDao.delete(oldBook)
bookDao.insert(newBook)
+ readRecordDao.replaceBookUrl(oldBookUrl, newBook.bookUrl)
if (options.migrateChapters) {
bookChapterDao.insert(*chapters.toTypedArray())
ReadBook.onChapterListUpdated(newBook)
diff --git a/app/src/main/java/io/legado/app/help/storage/Restore.kt b/app/src/main/java/io/legado/app/help/storage/Restore.kt
index 83ae74453..39dee962e 100644
--- a/app/src/main/java/io/legado/app/help/storage/Restore.kt
+++ b/app/src/main/java/io/legado/app/help/storage/Restore.kt
@@ -214,17 +214,28 @@ object Restore : KoinComponent {
}
fileToListT(path, "readRecord.json")?.let {
it.forEach { readRecord ->
- if (readRecord.deviceId != androidId) {
+ val restoredRecord = readRecord.copy(
+ bookUrl = resolveBookUrl(
+ readRecord.bookUrl,
+ readRecord.bookName,
+ readRecord.bookAuthor,
+ )
+ )
+ if (restoredRecord.deviceId != androidId) {
try {
- appDb.readRecordDao.insert(readRecord)
+ appDb.readRecordDao.insert(restoredRecord)
} catch (_: SQLiteConstraintException) {
}
} else {
val time = appDb.readRecordDao
- .getReadTime(readRecord.deviceId, readRecord.bookName, readRecord.bookAuthor)
- if (time == null || time < readRecord.readTime) {
+ .getReadTime(
+ restoredRecord.deviceId,
+ restoredRecord.bookName,
+ restoredRecord.bookAuthor,
+ )
+ if (time == null || time < restoredRecord.readTime) {
try {
- appDb.readRecordDao.insert(readRecord)
+ appDb.readRecordDao.insert(restoredRecord)
} catch (_: SQLiteConstraintException) {
}
}
@@ -234,7 +245,15 @@ object Restore : KoinComponent {
fileToListT(path, "readRecordDetail.json")?.let {
it.forEach { detail ->
try {
- appDb.readRecordDao.insertDetail(detail)
+ appDb.readRecordDao.insertDetail(
+ detail.copy(
+ bookUrl = resolveBookUrl(
+ detail.bookUrl,
+ detail.bookName,
+ detail.bookAuthor,
+ )
+ )
+ )
} catch (_: SQLiteConstraintException) {
}
}
@@ -242,7 +261,15 @@ object Restore : KoinComponent {
fileToListT(path, "readRecordSession.json")?.let {
it.forEach { session ->
try {
- appDb.readRecordDao.insertSession(session)
+ appDb.readRecordDao.insertSession(
+ session.copy(
+ bookUrl = resolveBookUrl(
+ session.bookUrl,
+ session.bookName,
+ session.bookAuthor,
+ )
+ )
+ )
} catch (_: SQLiteConstraintException) {
}
}
@@ -410,6 +437,15 @@ object Restore : KoinComponent {
return map
}
+ private suspend fun resolveBookUrl(
+ existingBookUrl: String?,
+ bookName: String,
+ bookAuthor: String,
+ ): String? {
+ if (!existingBookUrl.isNullOrBlank()) return existingBookUrl
+ return appDb.readRecordDao.findMatchingBookUrls(bookName, bookAuthor).singleOrNull()
+ }
+
private inline fun fileToListT(path: String, fileName: String): List? {
try {
val file = File(path, fileName)
diff --git a/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt b/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt
index c4352562a..1c0861b1b 100644
--- a/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt
+++ b/app/src/main/java/io/legado/app/help/update/AppReleaseInfo.kt
@@ -68,11 +68,12 @@ data class Asset(
val downloadCount: Int,
val id: Int,
val name: String,
- val state: String,
+ val state: String?,
val url: String
) {
val isValid: Boolean
- get() = (contentType == "application/vnd.android.package-archive") && (state == "uploaded")
+ get() = name.endsWith(".apk", ignoreCase = true) &&
+ (state == null || state == "uploaded")
fun assetToAppReleaseInfo(preRelease: Boolean, note: String, version: String): AppReleaseInfo {
val instant = Instant.parse(createdAt)
@@ -89,4 +90,4 @@ data class Asset(
versionName = version
)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt b/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt
index 09c943dcb..da408b12b 100644
--- a/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt
+++ b/app/src/main/java/io/legado/app/help/update/AppUpdateGitHub.kt
@@ -14,6 +14,9 @@ import kotlinx.coroutines.CoroutineScope
object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
+ private const val RELEASES_API =
+ "https://gitea.yamby.cn/api/v1/repos/yusheng/QieKan-3.0/releases"
+
private val checkVariant: AppVariant
get() = when (AppConfig.updateToVariant) {
"official_version" -> AppVariant.OFFICIAL
@@ -24,9 +27,9 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
private suspend fun getLatestRelease(): List {
val url = if (checkVariant == AppVariant.OFFICIAL)
- "https://api.github.com/repos/HapeLee/legado-with-MD3/releases/latest"
+ "$RELEASES_API/latest"
else
- "https://api.github.com/repos/HapeLee/legado-with-MD3/releases"
+ RELEASES_API
val res = okHttpClient.newCallResponse { url(url) }
if (!res.isSuccessful) throw NoStackTraceException("获取新版本出错(${res.code})")
@@ -65,7 +68,7 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
}
suspend fun getReleaseByTag(tag: String): AppUpdate.UpdateInfo? {
- val url = "https://api.github.com/repos/HapeLee/legado-with-MD3/releases/tags/$tag"
+ val url = "$RELEASES_API/tags/$tag"
val res = okHttpClient.newCallResponse { url(url) }
if (!res.isSuccessful) return null
@@ -165,4 +168,4 @@ object AppUpdateGitHub : AppUpdate.AppUpdateInterface {
fun String.versionCompare(other: String): Int {
return SemVer.parse(this).compareTo(SemVer.parse(other))
}
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/model/ReadBook.kt b/app/src/main/java/io/legado/app/model/ReadBook.kt
index 1c8a357ff..73d1ab9e0 100644
--- a/app/src/main/java/io/legado/app/model/ReadBook.kt
+++ b/app/src/main/java/io/legado/app/model/ReadBook.kt
@@ -124,6 +124,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
ReadBook.book = book
readRecord.bookName = book.name
readRecord.bookAuthor = book.author
+ readRecord.bookUrl = book.bookUrl
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
@@ -330,6 +331,7 @@ object ReadBook : CoroutineScope by MainScope(), KoinComponent {
deviceId = "",
bookName = currentBookName,
bookAuthor = currentBookAuthor,
+ bookUrl = book?.bookUrl,
startTime = readStartTime,
endTime = readStartTime,
words = durChapterIndex.toLong()
diff --git a/app/src/main/java/io/legado/app/model/ReadManga.kt b/app/src/main/java/io/legado/app/model/ReadManga.kt
index 1531107c5..47c22b81f 100644
--- a/app/src/main/java/io/legado/app/model/ReadManga.kt
+++ b/app/src/main/java/io/legado/app/model/ReadManga.kt
@@ -92,6 +92,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
ReadManga.book = book
readRecord.bookName = book.name
readRecord.bookAuthor = book.author
+ readRecord.bookUrl = book.bookUrl
readRecord.readTime = appDb.readRecordDao.getReadTime("", book.name, book.author) ?: 0
chapterSize = appDb.bookChapterDao.getChapterCount(book.bookUrl)
simulatedChapterSize = if (book.readSimulating()) {
@@ -162,6 +163,7 @@ object ReadManga : CoroutineScope by MainScope() , KoinComponent{
deviceId = "",
bookName = currentBookName,
bookAuthor = currentBookAuthor,
+ bookUrl = book?.bookUrl,
startTime = readStartTime,
endTime = readStartTime,
words = durChapterIndex.toLong()
diff --git a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
index e8bc447fd..c4a0feef6 100644
--- a/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/about/AboutScreen.kt
@@ -55,7 +55,7 @@ fun AboutScreen(
state: AboutUiState,
onIntent: (AboutIntent) -> Unit,
onBack: () -> Unit = {},
- versionName: String = appInfo.versionName,
+ versionName: String = "${appInfo.versionName} (${appInfo.versionCode})",
) {
if (ThemeResolver.isMiuixEngine(LegadoTheme.composeEngine)) {
MiuixAboutScreen(
@@ -145,13 +145,13 @@ private fun MaterialAboutScreen(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.Center
) {
- FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
+ FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }) {
Icon(
painter = painterResource(R.drawable.ic_web_outline),
contentDescription = stringResource(R.string.back)
)
}
- FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }) {
+ FilledTonalIconButton(onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }) {
Icon(
painter = painterResource(R.drawable.ic_github),
contentDescription = stringResource(R.string.back)
@@ -172,7 +172,7 @@ private fun MaterialAboutScreen(
SettingItemWithDivider {
SettingItem(
title = stringResource(R.string.contributors),
- onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) }
+ onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) }
)
}
SettingItemWithDivider {
diff --git a/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt b/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt
index 53141d010..d41c3791a 100644
--- a/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/about/MiuixAboutScreen.kt
@@ -84,7 +84,7 @@ fun MiuixAboutScreen(
state: AboutUiState,
onIntent: (AboutIntent) -> Unit,
onBack: () -> Unit = {},
- versionName: String = appInfo.versionName,
+ versionName: String = "${appInfo.versionName} (${appInfo.versionCode})",
) {
val topAppBarScrollBehavior = MiuixScrollBehavior()
val lazyListState = rememberLazyListState()
@@ -395,7 +395,7 @@ private fun AboutContent(
endActions = {
ValueText("GitHub")
},
- onClick = { onIntent(AboutIntent.OpenUrl("https://github.com/HapeLee/legado-with-MD3")) },
+ onClick = { onIntent(AboutIntent.OpenUrl("https://gitea.yamby.cn/yusheng/QieKan-3.0")) },
)
}
diff --git a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
index be4a20797..4e13fb4fb 100644
--- a/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
+++ b/app/src/main/java/io/legado/app/ui/book/group/GroupEditSheet.kt
@@ -115,7 +115,7 @@ fun GroupEditContent(
val sortEntryValues = remember(sortOptions) {
Array(sortOptions.size) { (it - 1).toString() }
}
- val canSetPrivate = group == null || group.groupId > 0
+ val canSetPrivate = group == null || group.groupId > 0 || group.groupId == Long.MIN_VALUE
val selectImage = rememberLauncherForActivityResult(SelectImageContract()) { result ->
result.uri?.let { uri ->
diff --git a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewViewModel.kt b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewViewModel.kt
index f2814285e..d33a1abb8 100644
--- a/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/book/readRecord/ReadRecordOverviewViewModel.kt
@@ -53,7 +53,7 @@ class ReadRecordOverviewViewModel(
_referenceDate,
repository.getAllRecordDetails(""),
repository.getLatestReadRecords(""),
- bookRepository.getAllBooks()
+ bookRepository.getVisibleBooks()
) { period, refDate, details, latestRecords, allBooks ->
getReadRecordOverviewUseCase(period, refDate, details, latestRecords, allBooks)
}.stateIn(
diff --git a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
index c7ee1fccf..ad6ba1fdc 100644
--- a/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
+++ b/app/src/main/java/io/legado/app/ui/main/MainNavGraph.kt
@@ -20,6 +20,7 @@ import androidx.navigation3.runtime.entryProvider
import androidx.navigation3.ui.LocalNavAnimatedContentScope
import androidx.navigation3.ui.NavDisplay
import io.legado.app.help.config.AppConfig
+import io.legado.app.data.repository.BookRepository
import io.legado.app.model.Download
import io.legado.app.ui.about.AboutEffect
import io.legado.app.ui.about.AboutScreen
@@ -74,11 +75,10 @@ import io.legado.app.utils.startActivityForBook
import io.legado.app.utils.toastOnUi
import io.legado.app.utils.toggleSystemBar
import kotlinx.coroutines.CompletableDeferred
-import kotlinx.coroutines.Dispatchers.IO
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withContext
import org.koin.androidx.compose.koinViewModel
+import org.koin.compose.koinInject
import org.koin.core.parameter.parametersOf
@OptIn(ExperimentalSharedTransitionApi::class)
@@ -559,13 +559,12 @@ fun MainActivity.mainEntryProvider(
}
entry {
+ val bookRepository = koinInject()
ReadRecordScreen(
onBackClick = { onNavigateBack() },
onBookClick = { name, author ->
lifecycleScope.launch {
- val book = withContext(IO) {
- io.legado.app.data.appDb.bookDao.getBook(name, author)
- }
+ val book = bookRepository.getVisibleBook(name, author)
if (book != null) this@mainEntryProvider.startActivityForBook(book)
else {
onNavigateToRoute(MainRouteSearch(key = name))
@@ -579,13 +578,12 @@ fun MainActivity.mainEntryProvider(
}
entry {
+ val bookRepository = koinInject()
ReadRecordOverviewScreen(
onBackClick = { onNavigateBack() },
onBookClick = { name, author ->
lifecycleScope.launch {
- val book = withContext(IO) {
- io.legado.app.data.appDb.bookDao.getBook(name, author)
- }
+ val book = bookRepository.getVisibleBook(name, author)
if (book != null) this@mainEntryProvider.startActivityForBook(book)
else {
onNavigateToRoute(MainRouteSearch(key = name))
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
index f7e3f0246..39c64a9aa 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookItem.kt
@@ -55,6 +55,7 @@ import io.legado.app.ui.widget.components.icon.AppIcon
import io.legado.app.ui.widget.components.image.cover.BookshelfCover
import io.legado.app.ui.widget.components.image.cover.CoilBookCover
import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
import io.legado.app.utils.toTimeAgo
/**
@@ -83,6 +84,7 @@ fun BookshelfItem(
titleMaxLines: Int = 2,
coverShadow: Boolean = false,
titleColor: Color? = null,
+ titleHidden: Boolean = false,
descAnnotated: AnnotatedString? = null,
coverWidth: Int = 84,
onClick: () -> Unit,
@@ -151,6 +153,7 @@ fun BookshelfItem(
modifier = Modifier
.align(Alignment.BottomStart)
.fillMaxWidth()
+ .hiddenAttributeUnderline(titleHidden, Color.White)
.background(
Brush.verticalGradient(
colors = listOf(
@@ -173,6 +176,10 @@ fun BookshelfItem(
textAlign = if (titleCenter) TextAlign.Center else TextAlign.Start,
modifier = Modifier
.fillMaxWidth()
+ .hiddenAttributeUnderline(
+ titleHidden,
+ LegadoTheme.colorScheme.onSurfaceVariant
+ )
.padding(start = 4.dp, end = 4.dp, bottom = 4.dp)
)
}
@@ -237,7 +244,12 @@ fun BookshelfItem(
},
maxLines = BookshelfConfig.bookshelfTitleMaxLines,
overflow = TextOverflow.Ellipsis,
- modifier = Modifier.weight(1f)
+ modifier = Modifier
+ .weight(1f)
+ .hiddenAttributeUnderline(
+ titleHidden,
+ LegadoTheme.colorScheme.onSurfaceVariant
+ )
)
titleEnd?.let {
Box(modifier = Modifier.padding(top = 4.dp, start = 4.dp)) {
@@ -436,6 +448,7 @@ fun BookGroupItemGrid(
)
},
title = group.groupName,
+ titleHidden = group.isPrivate,
modifier = modifier,
titleSmallFont = titleSmallFont,
titleCenter = titleCenter,
@@ -491,6 +504,7 @@ fun BookGroupItemList(
isCompact = BookshelfConfig.bookshelfGroupListStyle == 1 || isCompact,
cover = { BookGroupCover(books = previewBooks, coverPath = group.cover, modifier = it) },
title = group.groupName,
+ titleHidden = group.isPrivate,
subTitle = countText,
descAnnotated = descAnnotated,
titleSmallFont = titleSmallFont,
@@ -549,6 +563,10 @@ fun BookGroupItemHorizontalCovers(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
modifier = Modifier.weight(1f)
+ .hiddenAttributeUnderline(
+ group.isPrivate,
+ LegadoTheme.colorScheme.onSurfaceVariant
+ )
)
if (countText != null) {
AppText(
@@ -689,6 +707,7 @@ fun BookItem(
)
},
title = book.name,
+ titleHidden = bookUi.isHidden,
subTitle = if (layoutMode == 0 && isCompact) {
stringResource(R.string.author_read, book.author, unreadCount)
} else {
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt
index 70acf40cd..2064af3e8 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookShelfItem.kt
@@ -50,7 +50,7 @@ data class BookShelfItem(
/**
* 将 DTO 转换为专为 Compose 设计的 UI 状态
*/
- fun toUiItem(): BookUiItem {
+ fun toUiItem(privateGroupMask: Long = 0L): BookUiItem {
val tagList = mutableListOf()
kind?.splitNotBlank(",", "\n")?.filter { it.isNotBlank() }?.let {
tagList.addAll(it)
@@ -61,7 +61,8 @@ data class BookShelfItem(
return BookUiItem(
book = this,
- displayTags = tagList.toImmutableList()
+ displayTags = tagList.toImmutableList(),
+ isHidden = privateGroupMask != 0L && (group and privateGroupMask) != 0L
)
}
}
@@ -72,7 +73,8 @@ data class BookShelfItem(
@Stable
data class BookUiItem(
val book: BookShelfItem,
- val displayTags: ImmutableList
+ val displayTags: ImmutableList,
+ val isHidden: Boolean = false
) {
fun matches(key: String): Boolean {
return book.name.contains(key, true) ||
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
index 95f06c76f..e89e5f697 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfScreen.kt
@@ -65,6 +65,7 @@ import androidx.compose.material.icons.filled.Refresh
import androidx.compose.material.icons.filled.Save
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.UploadFile
+import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material.icons.filled.Wifi
import androidx.compose.material.icons.outlined.ViewCarousel
import androidx.compose.material3.ExperimentalMaterial3Api
@@ -73,6 +74,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.SnackbarHost
import androidx.compose.material3.SnackbarHostState
import androidx.compose.material3.SnackbarResult
+import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.derivedStateOf
@@ -238,7 +240,9 @@ fun BookshelfScreen(
)
val folderGridState = rememberLazyGridState()
val standaloneSearchGridState = rememberLazyGridState()
- val groupGridStates = mutableMapOf()
+ // Pager may keep composing an old keyed page briefly after the visible group list changes.
+ // Retain states for removed groups until those pages are disposed to avoid a missing map key.
+ val groupGridStates = remember { mutableMapOf() }
uiState.groups.forEach { group ->
key(group.groupId) {
groupGridStates[group.groupId] = rememberLazyGridState()
@@ -507,6 +511,19 @@ fun BookshelfScreen(
},
leadingIcon = { Icon(Icons.Outlined.ViewCarousel, null) }
)
+ RoundDropdownMenuItem(
+ text = stringResource(R.string.privacy_mode),
+ onClick = {
+ viewModel.setPrivacyMode(!uiState.privacyModeEnabled)
+ },
+ leadingIcon = { Icon(Icons.Default.VisibilityOff, null) },
+ trailingIcon = {
+ Switch(
+ checked = uiState.privacyModeEnabled,
+ onCheckedChange = viewModel::setPrivacyMode
+ )
+ }
+ )
RoundDropdownMenuItem(
text = stringResource(R.string.add_url),
onClick = {
@@ -578,6 +595,9 @@ fun BookshelfScreen(
AppTabRow(
tabTitles = tabTitles,
+ hiddenTabIndices = uiState.groups.mapIndexedNotNull { index, group ->
+ index.takeIf { group.isPrivate }
+ }.toSet(),
selectedTabIndex = selectedTabIndex,
onTabSelected = { index ->
scope.launch { pagerState.animateScrollToPage(index) }
@@ -608,6 +628,7 @@ fun BookshelfScreen(
uiState.groups.forEachIndexed { index, group ->
RoundDropdownMenuItem(
text = group.groupName,
+ hidden = group.isPrivate,
onClick = {
if (uiState.isSearch) {
viewModel.changeGroup(group.groupId)
@@ -641,7 +662,9 @@ fun BookshelfScreen(
it.groupId == BookGroup.IdAll
}
val hiddenGroups = uiState.allGroups.filter {
- !it.show && it.groupId != BookGroup.IdAll
+ !it.show &&
+ it.groupId != BookGroup.IdAll &&
+ (!uiState.privacyModeEnabled || !it.isPrivate)
}
if (allGroup != null || hiddenGroups.isNotEmpty()) {
@@ -656,6 +679,7 @@ fun BookshelfScreen(
allGroup?.let { group ->
RoundDropdownMenuItem(
text = group.groupName,
+ hidden = group.isPrivate,
onClick = {
viewModel.changeGroup(group.groupId)
dismiss()
@@ -675,6 +699,7 @@ fun BookshelfScreen(
hiddenGroups.forEach { group ->
RoundDropdownMenuItem(
text = group.groupName,
+ hidden = group.isPrivate,
onClick = {
viewModel.changeGroup(group.groupId)
dismiss()
@@ -999,6 +1024,7 @@ fun BookshelfScreen(
uiState.groups.forEach { group ->
RoundDropdownMenuItem(
text = group.groupName,
+ hidden = group.isPrivate,
onClick = {
val targetIndex =
uiState.groups.indexOfFirst { it.groupId == group.groupId }
@@ -1140,7 +1166,10 @@ private fun BookshelfOverlays(
GroupSelectSheet(
show = activeOverlay == BookshelfOverlay.GroupSelectSheet,
- groups = groups.filter { it.groupId > 0 },
+ groups = groups.filter {
+ (it.groupId > 0 || it.groupId == Long.MIN_VALUE) &&
+ (!uiState.privacyModeEnabled || !it.isPrivate)
+ },
currentGroupId = 0L,
onDismissRequest = { viewModel.dismissOverlay() },
onConfirm = { groupId ->
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
index 06c6c9f0b..ea75cdb78 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfUiState.kt
@@ -58,6 +58,7 @@ data class BookshelfUiState(
val bookGroupStyle: Int = 0,
val bookshelfSort: Int = 0,
val bookshelfSortOrder: Int = 1,
+ val privacyModeEnabled: Boolean = true,
val title: String = "",
val subtitle: String? = null,
val currentGroupName: String? = null,
diff --git a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
index 220e4c517..c57bb1247 100644
--- a/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
+++ b/app/src/main/java/io/legado/app/ui/main/bookshelf/BookshelfViewModel.kt
@@ -10,6 +10,7 @@ import io.legado.app.base.BaseViewModel
import io.legado.app.constant.AppConst
import io.legado.app.constant.AppLog
import io.legado.app.constant.EventBus
+import io.legado.app.constant.PreferKey
import io.legado.app.data.appDb
import io.legado.app.data.entities.Book
import io.legado.app.data.entities.BookGroup
@@ -18,6 +19,7 @@ import io.legado.app.data.repository.BookGroupRepository
import io.legado.app.data.repository.BookRepository
import io.legado.app.data.repository.BookSourceRepository
import io.legado.app.data.repository.BookshelfRepository
+import io.legado.app.data.repository.SettingsRepository
import io.legado.app.data.repository.UploadRepository
import io.legado.app.domain.usecase.AddBookUseCase
import io.legado.app.domain.usecase.BatchCacheDownloadUseCase
@@ -81,6 +83,7 @@ class BookshelfViewModel(
private val bookGroupRepository: BookGroupRepository,
private val bookSourceRepository: BookSourceRepository,
private val bookshelfRepository: BookshelfRepository,
+ private val settingsRepository: SettingsRepository,
private val uploadRepository: UploadRepository,
private val batchCacheDownloadUseCase: BatchCacheDownloadUseCase,
private val updateBooksGroupUseCase: UpdateBooksGroupUseCase,
@@ -110,6 +113,13 @@ class BookshelfViewModel(
val sortOrder: Int
)
+ private data class GroupBooksConfig(
+ val groups: List,
+ val sortConfig: BookshelfSortConfig,
+ val privacyModeEnabled: Boolean,
+ val privateGroupMask: Long
+ )
+
private fun readSortConfig() = BookshelfSortConfig(
sort = BookshelfConfig.bookshelfSort,
sortOrder = BookshelfConfig.bookshelfSortOrder
@@ -142,7 +152,40 @@ class BookshelfViewModel(
protected val _eventChannel = Channel()
val events = _eventChannel.receiveAsFlow()
- val groupsFlow: SharedFlow> = bookGroupRepository.flowShow()
+ val allGroupsFlow: StateFlow> = bookGroupRepository.flowAll()
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+
+ private val privacyModeFlow: StateFlow = settingsRepository
+ .getBoolean(PreferKey.bookshelfPrivacyMode, true)
+ .stateIn(viewModelScope, SharingStarted.Eagerly, true)
+
+ private val privateGroupMaskFlow: StateFlow = allGroupsFlow
+ .map { groups ->
+ groups.asSequence()
+ .filter {
+ (it.groupId > 0L || it.groupId == Long.MIN_VALUE) && it.isPrivate
+ }
+ .fold(0L) { mask, group -> mask or group.groupId }
+ }
+ .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L)
+
+ private val allBooksIncludingPrivateFlow = bookRepository.flowBookShelfIncludingPrivate()
+ .shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
+
+ private val displayedAllBookCountFlow = combine(
+ bookRepository.flowAllBookShelfCount(),
+ allBooksIncludingPrivateFlow,
+ privacyModeFlow
+ ) { publicCount, allBooks, privacyModeEnabled ->
+ if (privacyModeEnabled) publicCount else allBooks.size
+ }
+
+ val groupsFlow: SharedFlow> = combine(
+ bookGroupRepository.flowShow(),
+ privacyModeFlow
+ ) { groups, privacyModeEnabled ->
+ if (privacyModeEnabled) groups.filterNot { it.isPrivate } else groups
+ }
.onEach {
if (it.isNotEmpty()) {
isInitialLoadingFlow.value = false
@@ -150,8 +193,21 @@ class BookshelfViewModel(
}
.shareIn(viewModelScope, SharingStarted.WhileSubscribed(5000), replay = 1)
- val allGroupsFlow: StateFlow> = bookGroupRepository.flowAll()
- .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), emptyList())
+ private fun displayedBooksFlow(
+ groupId: Long,
+ privacyModeEnabled: Boolean
+ ): Flow> {
+ return if (
+ !privacyModeEnabled &&
+ (groupId == BookGroup.IdAll || groupId > 0L || groupId == Long.MIN_VALUE)
+ ) {
+ allBooksIncludingPrivateFlow.map { books ->
+ if (groupId == BookGroup.IdAll) books else books.filter { (it.group and groupId) != 0L }
+ }
+ } else {
+ bookRepository.flowBookShelfByGroup(groupId)
+ }
+ }
private data class GroupPreviewState(
val previews: ImmutableMap>,
@@ -180,38 +236,44 @@ class BookshelfViewModel(
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), BookshelfGroupSelectorState())
@OptIn(ExperimentalCoroutinesApi::class)
- val booksFlow: Flow> = groupIdFlow
- .flatMapLatest { groupId ->
+ val booksFlow: Flow> = combine(
+ groupIdFlow,
+ privacyModeFlow
+ ) { groupId, privacyModeEnabled -> groupId to privacyModeEnabled }
+ .flatMapLatest { (groupId, privacyModeEnabled) ->
combine(
- bookRepository.flowBookShelfByGroup(groupId),
- groupsFlow,
- sortConfigFlow
- ) { list, groups, sortConfig ->
+ displayedBooksFlow(groupId, privacyModeEnabled),
+ allGroupsFlow,
+ sortConfigFlow,
+ privateGroupMaskFlow
+ ) { list, groups, sortConfig, privateGroupMask ->
bookshelfRepository.sortBooks(
list,
groups.find { it.groupId == groupId },
sortConfig.sort,
sortConfig.sortOrder
- ).map { it.toUiItem() }
+ ).map { it.toUiItem(privateGroupMask) }
}
}.distinctUntilChanged().flowOn(Dispatchers.Default)
@OptIn(ExperimentalCoroutinesApi::class)
private val allGroupBooksImmutableFlow: Flow>> =
- combine(groupsFlow, sortConfigFlow) { groups, sortConfig ->
- groups to sortConfig
- }.flatMapLatest { (groups, sortConfig) ->
+ combine(groupsFlow, sortConfigFlow, privacyModeFlow, privateGroupMaskFlow) {
+ groups, sortConfig, privacyModeEnabled, privateGroupMask ->
+ GroupBooksConfig(groups, sortConfig, privacyModeEnabled, privateGroupMask)
+ }.flatMapLatest { config ->
+ val groups = config.groups
if (groups.isEmpty()) {
flowOf(persistentMapOf())
} else {
val flows = groups.map { group ->
- bookRepository.flowBookShelfByGroup(group.groupId).map { books ->
+ displayedBooksFlow(group.groupId, config.privacyModeEnabled).map { books ->
group.groupId to bookshelfRepository.sortBooks(
books,
group,
- sortConfig.sort,
- sortConfig.sortOrder
- ).map { it.toUiItem() }.toImmutableList()
+ config.sortConfig.sort,
+ config.sortConfig.sortOrder
+ ).map { it.toUiItem(config.privateGroupMask) }.toImmutableList()
}
}
combine(flows) { results ->
@@ -256,7 +318,7 @@ class BookshelfViewModel(
groupsFlow,
bookGroupStyleFlow,
bookRepository.flowSystemGroupCounts(),
- bookRepository.flowAllBookShelfCount()
+ displayedAllBookCountFlow
) { groups, bookGroupStyle, systemCounts, totalCount ->
DataForPreviews(
groups,
@@ -276,7 +338,9 @@ class BookshelfViewModel(
flowOf(GroupPreviewState(persistentMapOf(), persistentMapOf(), allBookCount))
} else {
val groupFlows = groups.map { group ->
- val countFlow: Flow = if (group.groupId > 0) {
+ val countFlow: Flow = if (
+ group.groupId > 0L || group.groupId == Long.MIN_VALUE
+ ) {
bookRepository.flowUserGroupBookCount(group.groupId)
} else {
flowOf(systemCountsMap[group.groupId] ?: 0)
@@ -372,7 +436,7 @@ class BookshelfViewModel(
GroupPreviewState(persistentMapOf(), persistentMapOf(), 0)
)
- private val dataStateFlow = combine(
+ private val dataCoreFlow = combine(
booksFlow,
groupsFlow,
allGroupsFlow,
@@ -380,14 +444,21 @@ class BookshelfViewModel(
internalStateFlow
) { books, groups, allGroups, previews, internal ->
BookshelfDataCore(books, groups, allGroups, previews, internal)
- }.combine(allGroupBooksImmutableFlow) { core, allGroupBooks ->
+ }
+
+ private val dataStateFlow = combine(
+ dataCoreFlow,
+ allGroupBooksImmutableFlow,
+ privacyModeFlow
+ ) { core, allGroupBooks, privacyModeEnabled ->
BookshelfDataState(
books = core.books,
groups = core.groups.map { it.toBookGroupUi() },
allGroups = core.allGroups.map { it.toBookGroupUi() },
previews = core.previews,
internal = core.internal,
- allGroupBooks = allGroupBooks
+ allGroupBooks = allGroupBooks,
+ privacyModeEnabled = privacyModeEnabled
)
}
@@ -405,7 +476,8 @@ class BookshelfViewModel(
val allGroups: List,
val previews: GroupPreviewState,
val internal: InternalState,
- val allGroupBooks: ImmutableMap>
+ val allGroupBooks: ImmutableMap>,
+ val privacyModeEnabled: Boolean
)
val uiState: StateFlow = combine(
@@ -459,6 +531,7 @@ class BookshelfViewModel(
bookGroupStyle = interaction.bookGroupStyle,
bookshelfSort = internal.sortConfig.sort,
bookshelfSortOrder = internal.sortConfig.sortOrder,
+ privacyModeEnabled = data.privacyModeEnabled,
title = title,
subtitle = when {
interaction.isEditMode -> {
@@ -506,6 +579,14 @@ class BookshelfViewModel(
viewModelScope.launch {
groupPreviewsFlow.collect { groupPreviewsStateFlow.value = it }
}
+ viewModelScope.launch {
+ combine(privacyModeFlow, allGroupsFlow, groupIdFlow) {
+ privacyModeEnabled, groups, groupId ->
+ privacyModeEnabled && groups.any { it.groupId == groupId && it.isPrivate }
+ }.distinctUntilChanged().filter { it }.collect {
+ changeGroup(BookGroup.IdAll)
+ }
+ }
viewModelScope.launch {
combine(booksFlow, selectedGroupCanReorderFlow) { books, canReorderBooks ->
books to canReorderBooks
@@ -584,6 +665,12 @@ class BookshelfViewModel(
clearSelection()
}
+ fun setPrivacyMode(enabled: Boolean) {
+ viewModelScope.launch {
+ settingsRepository.putBoolean(PreferKey.bookshelfPrivacyMode, enabled)
+ }
+ }
+
fun showOverlay(overlay: BookshelfOverlay) {
activeOverlayFlow.value = overlay
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt
index 69cad7e1c..7fcfdc06a 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/image/cover/CoilBookCover.kt
@@ -466,9 +466,11 @@ private fun CoverTextOverlay(
val startX = viewWidth * 0.84f
val fm = paint.fontMetrics
val charHeight = fm.bottom - fm.top
- var startY = viewHeight * 0.16f - (author.length * charHeight)
- startY = startY.coerceAtLeast(viewHeight * 0.2f)
- author.forEach { char ->
+ val visibleAuthor = author.takeLast(
+ ((viewHeight * 0.35f) / charHeight).toInt().coerceAtLeast(1)
+ )
+ var startY = viewHeight * 0.92f - (visibleAuthor.length - 1) * charHeight
+ visibleAuthor.forEach { char ->
nativeCanvas.drawText(char.toString(), startX, startY, paint)
startY += charHeight
}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
index 8dfa92d4b..7e56681cf 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/menuItem/RoundDropdownMenuItem.kt
@@ -35,6 +35,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.icon.AppIcon
+import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
import top.yukonga.miuix.kmp.icon.MiuixIcons
import top.yukonga.miuix.kmp.icon.basic.Check
import top.yukonga.miuix.kmp.theme.MiuixTheme
@@ -50,6 +51,7 @@ fun RoundDropdownMenuItem(
leadingIcon: @Composable (() -> Unit)? = null,
trailingIcon: @Composable (() -> Unit)? = null,
enabled: Boolean = true,
+ hidden: Boolean = false,
contentPadding: PaddingValues = MenuDefaults.DropdownMenuItemContentPadding,
interactionSource: MutableInteractionSource? = null,
) {
@@ -88,7 +90,9 @@ fun RoundDropdownMenuItem(
contentAlignment = Alignment.CenterStart
) {
MiuixText(
- modifier = Modifier.widthIn(max = 200.dp),
+ modifier = Modifier
+ .widthIn(max = 200.dp)
+ .hiddenAttributeUnderline(hidden, textColor),
text = text,
fontSize = MiuixTheme.textStyles.body1.fontSize,
fontWeight = FontWeight.Medium,
@@ -157,7 +161,9 @@ fun RoundDropdownMenuItem(
contentAlignment = Alignment.CenterStart
) {
Text(
- modifier = Modifier.widthIn(max = 200.dp),
+ modifier = Modifier
+ .widthIn(max = 200.dp)
+ .hiddenAttributeUnderline(hidden, contentColor),
text = text,
style = LegadoTheme.typography.labelLargeEmphasized,
color = contentColor
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/tabRow/AppTabRow.kt b/app/src/main/java/io/legado/app/ui/widget/components/tabRow/AppTabRow.kt
index 8af61683b..4c0a82cf5 100644
--- a/app/src/main/java/io/legado/app/ui/widget/components/tabRow/AppTabRow.kt
+++ b/app/src/main/java/io/legado/app/ui/widget/components/tabRow/AppTabRow.kt
@@ -12,6 +12,7 @@ import androidx.compose.ui.unit.dp
import io.legado.app.ui.theme.LegadoTheme
import io.legado.app.ui.theme.ThemeResolver
import io.legado.app.ui.widget.components.text.AppText
+import io.legado.app.ui.widget.components.text.hiddenAttributeUnderline
import top.yukonga.miuix.kmp.basic.TabRowDefaults
import top.yukonga.miuix.kmp.basic.TabRowWithContour
@@ -21,11 +22,12 @@ fun AppTabRow(
selectedTabIndex: Int,
onTabSelected: (Int) -> Unit,
modifier: Modifier = Modifier,
- isScrollable: Boolean = true
+ isScrollable: Boolean = true,
+ hiddenTabIndices: Set = emptySet()
) {
val composeEngine = LegadoTheme.composeEngine
- if (ThemeResolver.isMiuixEngine(composeEngine)) {
+ if (ThemeResolver.isMiuixEngine(composeEngine) && hiddenTabIndices.isEmpty()) {
TabRowWithContour(
tabs = tabTitles,
selectedTabIndex = selectedTabIndex,
@@ -50,7 +52,8 @@ fun AppTabRow(
AppTab(
selected = selectedTabIndex == index,
onClick = { onTabSelected(index) },
- title = title
+ title = title,
+ hidden = index in hiddenTabIndices
)
}
}
@@ -65,7 +68,8 @@ fun AppTabRow(
AppTab(
selected = selectedTabIndex == index,
onClick = { onTabSelected(index) },
- title = title
+ title = title,
+ hidden = index in hiddenTabIndices
)
}
}
@@ -77,7 +81,8 @@ fun AppTabRow(
private fun AppTab(
selected: Boolean,
onClick: () -> Unit,
- title: String
+ title: String,
+ hidden: Boolean
) {
Tab(
selected = selected,
@@ -88,9 +93,15 @@ private fun AppTab(
maxLines = 1,
overflow = TextOverflow.Ellipsis,
style = LegadoTheme.typography.labelLargeEmphasized,
- modifier = Modifier.padding(horizontal = 8.dp),
+ modifier = Modifier
+ .padding(horizontal = 8.dp)
+ .hiddenAttributeUnderline(
+ hidden,
+ if (selected) LegadoTheme.colorScheme.primary
+ else LegadoTheme.colorScheme.onSurfaceVariant
+ ),
color = if (selected) LegadoTheme.colorScheme.primary else LegadoTheme.colorScheme.onSurfaceVariant
)
}
)
-}
\ No newline at end of file
+}
diff --git a/app/src/main/java/io/legado/app/ui/widget/components/text/HiddenAttributeUnderline.kt b/app/src/main/java/io/legado/app/ui/widget/components/text/HiddenAttributeUnderline.kt
new file mode 100644
index 000000000..119f986c2
--- /dev/null
+++ b/app/src/main/java/io/legado/app/ui/widget/components/text/HiddenAttributeUnderline.kt
@@ -0,0 +1,25 @@
+package io.legado.app.ui.widget.components.text
+
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.PathEffect
+import androidx.compose.ui.unit.dp
+
+fun Modifier.hiddenAttributeUnderline(
+ hidden: Boolean,
+ color: Color
+): Modifier = if (!hidden) {
+ this
+} else {
+ drawBehind {
+ val strokeWidth = 1.dp.toPx()
+ drawLine(
+ color = color,
+ start = androidx.compose.ui.geometry.Offset(0f, size.height - strokeWidth),
+ end = androidx.compose.ui.geometry.Offset(size.width, size.height - strokeWidth),
+ strokeWidth = strokeWidth,
+ pathEffect = PathEffect.dashPathEffect(floatArrayOf(4.dp.toPx(), 3.dp.toPx()))
+ )
+ }
+}
diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml
index 86151c4f5..1ad4596ab 100644
--- a/app/src/main/res/values-zh-rCN/strings.xml
+++ b/app/src/main/res/values-zh-rCN/strings.xml
@@ -1,7 +1,7 @@
- 阅读
- 阅读·A
+ 且看
+ 且看·A
阅读 · 搜索
阅读需要访问存储卡权限,请前往“设置”—“应用权限”,打开所需权限
@@ -134,7 +134,7 @@
换源
全新设计的阅读 3.0
- 阅读 3.0 下载地址:\nhttps://github.com/gedoor/legado/releases
+ 且看 3.0 下载地址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
版本 %s
后台校验书源
@@ -1295,9 +1295,10 @@
管理私有文件夹的文件
创建文件夹
允许下拉刷新
- 设为隐私分组
- 关闭隐私分组
- 开启后,本分组内的书籍将从公开书架隐藏
+ 设为隐藏分组
+ 关闭隐藏分组
+ 开启后,本分组及其中书籍将在隐私模式下隐藏
+ 隐私模式
文字下划线
选中新增源
选中更新源
diff --git a/app/src/main/res/values-zh-rHK/strings.xml b/app/src/main/res/values-zh-rHK/strings.xml
index f35b50b23..379c90d2a 100644
--- a/app/src/main/res/values-zh-rHK/strings.xml
+++ b/app/src/main/res/values-zh-rHK/strings.xml
@@ -1,7 +1,7 @@
- 閲讀
- 閲讀·A
+ 且看
+ 且看·A
閲讀·搜尋
閲讀需要訪問存儲卡權限,請前往「設定」—「應用程式權限」—開啟所需要的權限
@@ -99,7 +99,7 @@
\u3000\u3000這是一款使用 Kotlin 全新開發的開源的閲讀應用程式,歡迎你的加入。
- 閲讀3.0下載地址:\nhttps://github.com/gedoor/legado/releases
+ 且看 3.0 下載地址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
Version %s
後臺校驗書源
@@ -1145,9 +1145,10 @@
管理私有文件夹的文件
创建文件夹
允许下拉刷新
- 設為私隱分組
- 關閉私隱分組
- 開啟後,本分組內嘅書籍將從公開書架隱藏
+ 設為隱藏分組
+ 關閉隱藏分組
+ 開啟後,本分組及其中書籍會在私隱模式下隱藏
+ 私隱模式
文字下划线
选中新增源
选中更新源
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 20bc308bd..4df6844f5 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -1,7 +1,7 @@
- 閱讀
- 閱讀·A
+ 且看
+ 且看·A
閱讀·搜尋
閱讀需要存取記憶卡權限,請前往「設定」—「應用程式權限」—打開所需權限
@@ -101,7 +101,7 @@
\u3000\u3000這是一款使用Kotlin全新開發的開源的閱讀軟體,歡迎您的加入。
- 閱讀3.0下載網址:\nhttps://github.com/gedoor/legado/releases
+ 且看 3.0 下載網址:\nhttps://gitea.yamby.cn/yusheng/QieKan-3.0/releases
Version %s
後臺校驗書源
@@ -1148,9 +1148,10 @@
管理私有文件夹的文件
创建文件夹
允许下拉刷新
- 設為隱私分組
- 關閉隱私分組
- 開啟後,本分組內的書籍將從公開書架隱藏
+ 設為隱藏分組
+ 關閉隱藏分組
+ 開啟後,本分組及其中書籍將在隱私模式下隱藏
+ 隱私模式
文字下划线
选中新增源
选中更新源
diff --git a/app/src/main/res/values/non_translat.xml b/app/src/main/res/values/non_translat.xml
index 7d32a5235..2bc25e9c6 100644
--- a/app/src/main/res/values/non_translat.xml
+++ b/app/src/main/res/values/non_translat.xml
@@ -5,14 +5,14 @@
gekunfei@live.com
https://github.com/gedoor/legado/graphs/contributors
- https://github.com/HapeLee/legado-with-MD3
+ https://gitea.yamby.cn/yusheng/QieKan-3.0
https://gedoor.github.io/
http://%1$s:%2$d
- GitHub
+ Gitea
【%s】
🔒%s
🔓%s
丨
-
\ No newline at end of file
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 9751e4ad2..0f870890a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -1,8 +1,8 @@
- Legado
- Legado·A
+ 且看
+ 且看·A
Legado·search
Legado needs storage access to find and read books. Please go "App Settings" to allow "Storage Permission".
@@ -155,7 +155,7 @@
\u3000\u3000 This is an open source reading software newly developed by Kotlin, welcome to join us.
- Legado (YueDu 3.0) download link:\n https://github.com/gedoor/legado/releases
+ QieKan 3.0 download link:\n https://gitea.yamby.cn/yusheng/QieKan-3.0/releases
Version %s
Background-verification
@@ -1336,9 +1336,10 @@
Managing files in the application\'s private folder
Create Folder
Allow drop-down refresh
- Set as private group
- Disable private group
- After enabling, books in this group are hidden from the public bookshelf
+ Set as hidden group
+ Disable hidden group
+ The group and its books are hidden while privacy mode is enabled
+ Privacy mode
Text underline
Select new source
Select update source
diff --git a/app/src/test/java/io/legado/app/data/repository/BookPrivacyStateTest.kt b/app/src/test/java/io/legado/app/data/repository/BookPrivacyStateTest.kt
new file mode 100644
index 000000000..53563b273
--- /dev/null
+++ b/app/src/test/java/io/legado/app/data/repository/BookPrivacyStateTest.kt
@@ -0,0 +1,69 @@
+package io.legado.app.data.repository
+
+import io.legado.app.data.entities.Book
+import io.legado.app.data.entities.BookGroup
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class BookPrivacyStateTest {
+
+ private val hiddenGroup = BookGroup(groupId = 32L, isPrivate = true)
+ private val visibleBook = Book(
+ bookUrl = "visible-url",
+ name = "MS攻略",
+ author = "公开作者",
+ group = 0L,
+ )
+ private val hiddenBook = Book(
+ bookUrl = "hidden-url",
+ name = "秘密书",
+ author = "隐藏作者",
+ group = 32L,
+ )
+
+ @Test
+ fun `privacy mode only exposes records linked to visible books`() {
+ val state = BookPrivacyState.create(
+ books = listOf(visibleBook, hiddenBook),
+ groups = listOf(hiddenGroup),
+ privacyModeEnabled = true,
+ )
+
+ assertTrue(state.isReadRecordVisible("visible-url", "旧标题"))
+ assertTrue(state.isReadRecordVisible(null, "MS 攻略", "公开作者"))
+ assertTrue(state.isReadRecordVisible("stale-url", "MS 攻略", "公开作者"))
+ assertFalse(state.isReadRecordVisible("hidden-url", "任意标题"))
+ assertFalse(state.isReadRecordVisible(null, "秘密书"))
+ assertFalse(state.isReadRecordVisible("stale-url", "秘密书"))
+ assertFalse(state.isReadRecordVisible(null, "无法关联的历史别名"))
+ }
+
+ @Test
+ fun `author disambiguates visible and hidden books with the same title`() {
+ val state = BookPrivacyState.create(
+ books = listOf(
+ visibleBook.copy(name = "同名书"),
+ hiddenBook.copy(name = "同名书"),
+ ),
+ groups = listOf(hiddenGroup),
+ privacyModeEnabled = true,
+ )
+
+ assertTrue(state.isReadRecordVisible("stale-url", "同名书", "公开作者"))
+ assertFalse(state.isReadRecordVisible("stale-url", "同名书", "隐藏作者"))
+ assertFalse(state.isReadRecordVisible("stale-url", "同名书"))
+ }
+
+ @Test
+ fun `privacy mode off exposes all historical records`() {
+ val state = BookPrivacyState.create(
+ books = listOf(visibleBook, hiddenBook),
+ groups = listOf(hiddenGroup),
+ privacyModeEnabled = false,
+ )
+
+ assertTrue(state.isReadRecordVisible("hidden-url", "秘密书"))
+ assertTrue(state.isReadRecordVisible(null, "无法关联的历史别名"))
+ }
+}
diff --git a/app/src/test/java/io/legado/app/data/repository/SettingsRepositoryCompatibilityTest.kt b/app/src/test/java/io/legado/app/data/repository/SettingsRepositoryCompatibilityTest.kt
new file mode 100644
index 000000000..781eb68e7
--- /dev/null
+++ b/app/src/test/java/io/legado/app/data/repository/SettingsRepositoryCompatibilityTest.kt
@@ -0,0 +1,35 @@
+package io.legado.app.data.repository
+
+import androidx.datastore.preferences.core.booleanPreferencesKey
+import androidx.datastore.preferences.core.mutablePreferencesOf
+import androidx.datastore.preferences.core.stringPreferencesKey
+import io.legado.app.constant.PreferKey
+import org.junit.Assert.assertEquals
+import org.junit.Test
+
+class SettingsRepositoryCompatibilityTest {
+
+ @Test
+ fun normalizeLegacyBrightnessVisibilityBoolean() {
+ assertEquals("1", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, true))
+ assertEquals("0", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, false))
+ assertEquals("2", normalizeLegacyPreferenceValue(PreferKey.showBrightnessView, "2"))
+ }
+
+ @Test
+ fun readLegacyBrightnessVisibilityBooleanAsString() {
+ val enabled = mutablePreferencesOf(
+ booleanPreferencesKey(PreferKey.showBrightnessView) to true
+ )
+ val disabled = mutablePreferencesOf(
+ booleanPreferencesKey(PreferKey.showBrightnessView) to false
+ )
+ val vertical = mutablePreferencesOf(
+ stringPreferencesKey(PreferKey.showBrightnessView) to "2"
+ )
+
+ assertEquals("1", enabled.getStringCompat(PreferKey.showBrightnessView, "0"))
+ assertEquals("0", disabled.getStringCompat(PreferKey.showBrightnessView, "1"))
+ assertEquals("2", vertical.getStringCompat(PreferKey.showBrightnessView, "1"))
+ }
+}
diff --git a/app/src/test/java/io/legado/app/ui/main/bookshelf/BookShelfItemTest.kt b/app/src/test/java/io/legado/app/ui/main/bookshelf/BookShelfItemTest.kt
new file mode 100644
index 000000000..08488057b
--- /dev/null
+++ b/app/src/test/java/io/legado/app/ui/main/bookshelf/BookShelfItemTest.kt
@@ -0,0 +1,33 @@
+package io.legado.app.ui.main.bookshelf
+
+import org.junit.Assert.assertTrue
+import org.junit.Test
+
+class BookShelfItemTest {
+
+ @Test
+ fun `marks book in highest-bit private group as hidden`() {
+ val book = BookShelfItem(
+ bookUrl = "book-url",
+ name = "book",
+ author = "author",
+ origin = "origin",
+ originName = "origin-name",
+ coverUrl = null,
+ customCoverUrl = null,
+ durChapterTitle = null,
+ durChapterTime = 0L,
+ durChapterPos = 0,
+ latestChapterTitle = null,
+ latestChapterTime = 0L,
+ lastCheckCount = 0,
+ totalChapterNum = 0,
+ durChapterIndex = 0,
+ type = 0,
+ group = Long.MIN_VALUE,
+ order = 0,
+ )
+
+ assertTrue(book.toUiItem(privateGroupMask = Long.MIN_VALUE).isHidden)
+ }
+}
diff --git a/app/version.properties b/app/version.properties
index ec8ec3805..5167805d3 100644
--- a/app/version.properties
+++ b/app/version.properties
@@ -1,5 +1,5 @@
VERSION_MAJOR=3
VERSION_MINOR=26
-VERSION_PATCH=13
+VERSION_PATCH=14
VERSION_SUFFIX=1
-# 1 = Pre, 0 = Release
\ No newline at end of file
+# 1 = Pre, 0 = Release