Przeglądaj źródła

Merge pull request #1893 from 52fisher/feat-hl

feat(highlight): add font weight, italic and 9-slice background support
Kudomaga 3 miesięcy temu
rodzic
commit
1f1541f974

+ 4662 - 0
app/schemas/io.legado.app.data.AppDatabase/99.json

@@ -0,0 +1,4662 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 99,
+    "identityHash": "0458658832e21f9dde1ecae6a827691f",
+    "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, `tocLevel` INTEGER NOT NULL DEFAULT 0, `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": "tocLevel",
+            "columnName": "tocLevel",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "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 '', `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": "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 '', `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": "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 '', `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": "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, `fontWeight` INTEGER NOT NULL, `isItalic` INTEGER NOT NULL, `npLeft` REAL NOT NULL, `npRight` REAL NOT NULL, `npTop` REAL NOT NULL, `npBottom` REAL NOT NULL, 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"
+          },
+          {
+            "fieldPath": "fontWeight",
+            "columnName": "fontWeight",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isItalic",
+            "columnName": "isItalic",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "npLeft",
+            "columnName": "npLeft",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "npRight",
+            "columnName": "npRight",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "npTop",
+            "columnName": "npTop",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "npBottom",
+            "columnName": "npBottom",
+            "affinity": "REAL",
+            "notNull": true
+          }
+        ],
+        "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"
+          ]
+        }
+      },
+      {
+        "tableName": "book_content_processes",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `chapterIndex` INTEGER, `kind` TEXT NOT NULL, `stage` TEXT NOT NULL, `target` TEXT NOT NULL, `anchorJson` TEXT NOT NULL, `actionJson` TEXT NOT NULL, `styleJson` TEXT, `source` TEXT NOT NULL, `aiArtifactId` TEXT, `sourceContentHash` TEXT, `enabled` INTEGER NOT NULL, `sortOrder` INTEGER NOT NULL, `status` INTEGER NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "kind",
+            "columnName": "kind",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "stage",
+            "columnName": "stage",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "target",
+            "columnName": "target",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "anchorJson",
+            "columnName": "anchorJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "actionJson",
+            "columnName": "actionJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "styleJson",
+            "columnName": "styleJson",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "aiArtifactId",
+            "columnName": "aiArtifactId",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "sourceContentHash",
+            "columnName": "sourceContentHash",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sortOrder",
+            "columnName": "sortOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "status",
+            "columnName": "status",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "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_book_content_processes_bookUrl_chapterIndex_enabled_sortOrder",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "chapterIndex",
+              "enabled",
+              "sortOrder"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_content_processes_bookUrl_chapterIndex_enabled_sortOrder` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`, `enabled`, `sortOrder`)"
+          },
+          {
+            "name": "index_book_content_processes_bookUrl_kind",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "kind"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_content_processes_bookUrl_kind` ON `${TABLE_NAME}` (`bookUrl`, `kind`)"
+          },
+          {
+            "name": "index_book_content_processes_aiArtifactId",
+            "unique": false,
+            "columnNames": [
+              "aiArtifactId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_content_processes_aiArtifactId` ON `${TABLE_NAME}` (`aiArtifactId`)"
+          }
+        ]
+      },
+      {
+        "tableName": "ai_prompt_presets",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `taskType` TEXT NOT NULL, `name` TEXT NOT NULL, `instruction` TEXT NOT NULL, `enabled` INTEGER NOT NULL, `builtIn` 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": "instruction",
+            "columnName": "instruction",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "builtIn",
+            "columnName": "builtIn",
+            "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_prompt_presets_taskType_enabled_sortNumber",
+            "unique": false,
+            "columnNames": [
+              "taskType",
+              "enabled",
+              "sortNumber"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_ai_prompt_presets_taskType_enabled_sortNumber` ON `${TABLE_NAME}` (`taskType`, `enabled`, `sortNumber`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_character_profiles",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `name` TEXT NOT NULL, `aliasesJson` TEXT NOT NULL, `avatarUri` TEXT, `tagsJson` TEXT NOT NULL, `role` TEXT NOT NULL, `voiceGender` TEXT NOT NULL DEFAULT 'unknown', `voiceAgeBand` TEXT NOT NULL DEFAULT 'unknown', `personality` TEXT NOT NULL, `summary` TEXT NOT NULL, `status` INTEGER NOT NULL, `source` TEXT NOT NULL, `confidence` REAL NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "name",
+            "columnName": "name",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "aliasesJson",
+            "columnName": "aliasesJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "avatarUri",
+            "columnName": "avatarUri",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "tagsJson",
+            "columnName": "tagsJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "role",
+            "columnName": "role",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "voiceGender",
+            "columnName": "voiceGender",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'unknown'"
+          },
+          {
+            "fieldPath": "voiceAgeBand",
+            "columnName": "voiceAgeBand",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'unknown'"
+          },
+          {
+            "fieldPath": "personality",
+            "columnName": "personality",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "summary",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "status",
+            "columnName": "status",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "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_book_character_profiles_bookUrl_name",
+            "unique": true,
+            "columnNames": [
+              "bookUrl",
+              "name"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_book_character_profiles_bookUrl_name` ON `${TABLE_NAME}` (`bookUrl`, `name`)"
+          },
+          {
+            "name": "index_book_character_profiles_bookUrl_updatedAt",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "updatedAt"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_character_profiles_bookUrl_updatedAt` ON `${TABLE_NAME}` (`bookUrl`, `updatedAt`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_character_events",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `characterId` TEXT NOT NULL, `chapterIndex` INTEGER, `chapterTitle` TEXT NOT NULL, `eventTimeText` TEXT NOT NULL, `content` TEXT NOT NULL, `importance` INTEGER NOT NULL, `sourceTextHash` TEXT, `evidenceJson` TEXT NOT NULL, `source` TEXT NOT NULL, `confidence` REAL NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "characterId",
+            "columnName": "characterId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "chapterTitle",
+            "columnName": "chapterTitle",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "eventTimeText",
+            "columnName": "eventTimeText",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "importance",
+            "columnName": "importance",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "sourceTextHash",
+            "columnName": "sourceTextHash",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "evidenceJson",
+            "columnName": "evidenceJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "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_book_character_events_bookUrl_characterId_chapterIndex",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "characterId",
+              "chapterIndex"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_character_events_bookUrl_characterId_chapterIndex` ON `${TABLE_NAME}` (`bookUrl`, `characterId`, `chapterIndex`)"
+          },
+          {
+            "name": "index_book_character_events_bookUrl_chapterIndex",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "chapterIndex"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_character_events_bookUrl_chapterIndex` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_character_relations",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `fromCharacterId` TEXT NOT NULL, `toCharacterId` TEXT NOT NULL, `relationType` TEXT NOT NULL, `summary` TEXT NOT NULL, `attitude` TEXT NOT NULL, `evidenceJson` TEXT NOT NULL, `chapterIndex` INTEGER, `status` INTEGER NOT NULL, `source` TEXT NOT NULL, `confidence` REAL NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "fromCharacterId",
+            "columnName": "fromCharacterId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "toCharacterId",
+            "columnName": "toCharacterId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "relationType",
+            "columnName": "relationType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "summary",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "attitude",
+            "columnName": "attitude",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "evidenceJson",
+            "columnName": "evidenceJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "status",
+            "columnName": "status",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "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_book_character_relations_bookUrl_fromCharacterId_toCharacterId",
+            "unique": true,
+            "columnNames": [
+              "bookUrl",
+              "fromCharacterId",
+              "toCharacterId"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_book_character_relations_bookUrl_fromCharacterId_toCharacterId` ON `${TABLE_NAME}` (`bookUrl`, `fromCharacterId`, `toCharacterId`)"
+          },
+          {
+            "name": "index_book_character_relations_bookUrl_fromCharacterId",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "fromCharacterId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_character_relations_bookUrl_fromCharacterId` ON `${TABLE_NAME}` (`bookUrl`, `fromCharacterId`)"
+          },
+          {
+            "name": "index_book_character_relations_bookUrl_toCharacterId",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "toCharacterId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_character_relations_bookUrl_toCharacterId` ON `${TABLE_NAME}` (`bookUrl`, `toCharacterId`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_knowledge_entries",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `type` TEXT NOT NULL, `title` TEXT NOT NULL, `keywordsJson` TEXT NOT NULL, `content` TEXT NOT NULL, `scopeStartChapter` INTEGER, `scopeEndChapter` INTEGER, `priority` INTEGER NOT NULL, `source` TEXT NOT NULL, `confidence` REAL NOT NULL, `evidenceJson` TEXT NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "type",
+            "columnName": "type",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "keywordsJson",
+            "columnName": "keywordsJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "scopeStartChapter",
+            "columnName": "scopeStartChapter",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "scopeEndChapter",
+            "columnName": "scopeEndChapter",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "priority",
+            "columnName": "priority",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "fieldPath": "evidenceJson",
+            "columnName": "evidenceJson",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "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_book_knowledge_entries_bookUrl_type_priority",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "type",
+              "priority"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_knowledge_entries_bookUrl_type_priority` ON `${TABLE_NAME}` (`bookUrl`, `type`, `priority`)"
+          },
+          {
+            "name": "index_book_knowledge_entries_bookUrl_title",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "title"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_knowledge_entries_bookUrl_title` ON `${TABLE_NAME}` (`bookUrl`, `title`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_outline_nodes",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `parentId` TEXT, `nodeType` TEXT NOT NULL, `title` TEXT NOT NULL, `summary` TEXT NOT NULL, `startChapterIndex` INTEGER, `endChapterIndex` INTEGER, `order` INTEGER NOT NULL, `source` TEXT NOT NULL, `confidence` REAL NOT NULL, `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": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "parentId",
+            "columnName": "parentId",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "nodeType",
+            "columnName": "nodeType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "summary",
+            "columnName": "summary",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "startChapterIndex",
+            "columnName": "startChapterIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "endChapterIndex",
+            "columnName": "endChapterIndex",
+            "affinity": "INTEGER"
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true
+          },
+          {
+            "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_book_outline_nodes_bookUrl_nodeType_startChapterIndex_endChapterIndex",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "nodeType",
+              "startChapterIndex",
+              "endChapterIndex"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_outline_nodes_bookUrl_nodeType_startChapterIndex_endChapterIndex` ON `${TABLE_NAME}` (`bookUrl`, `nodeType`, `startChapterIndex`, `endChapterIndex`)"
+          },
+          {
+            "name": "index_book_outline_nodes_bookUrl_parentId_order",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "parentId",
+              "order"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_outline_nodes_bookUrl_parentId_order` ON `${TABLE_NAME}` (`bookUrl`, `parentId`, `order`)"
+          }
+        ]
+      },
+      {
+        "tableName": "read_aloud_voices",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `engineType` TEXT NOT NULL, `engineId` TEXT NOT NULL DEFAULT '', `speakerId` TEXT NOT NULL DEFAULT '', `displayName` TEXT NOT NULL, `traitsJson` TEXT NOT NULL DEFAULT '[]', `emotionCatalogJson` TEXT NOT NULL DEFAULT '[]', `managedBy` TEXT NOT NULL DEFAULT 'user', `enabled` INTEGER NOT NULL DEFAULT 1, `available` INTEGER NOT NULL DEFAULT 1, `revision` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "engineType",
+            "columnName": "engineType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "engineId",
+            "columnName": "engineId",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "speakerId",
+            "columnName": "speakerId",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "displayName",
+            "columnName": "displayName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "traitsJson",
+            "columnName": "traitsJson",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'[]'"
+          },
+          {
+            "fieldPath": "emotionCatalogJson",
+            "columnName": "emotionCatalogJson",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'[]'"
+          },
+          {
+            "fieldPath": "managedBy",
+            "columnName": "managedBy",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'user'"
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "available",
+            "columnName": "available",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "revision",
+            "columnName": "revision",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_read_aloud_voices_engineType_engineId_speakerId",
+            "unique": true,
+            "columnNames": [
+              "engineType",
+              "engineId",
+              "speakerId"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_read_aloud_voices_engineType_engineId_speakerId` ON `${TABLE_NAME}` (`engineType`, `engineId`, `speakerId`)"
+          },
+          {
+            "name": "index_read_aloud_voices_enabled_displayName",
+            "unique": false,
+            "columnNames": [
+              "enabled",
+              "displayName"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_read_aloud_voices_enabled_displayName` ON `${TABLE_NAME}` (`enabled`, `displayName`)"
+          }
+        ]
+      },
+      {
+        "tableName": "book_voice_bindings",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookUrl` TEXT NOT NULL, `subjectType` TEXT NOT NULL, `subjectId` TEXT NOT NULL, `voiceId` TEXT NOT NULL, `locked` INTEGER NOT NULL DEFAULT 0, `source` TEXT NOT NULL DEFAULT 'user', `confidence` REAL NOT NULL DEFAULT 1, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`bookUrl`, `subjectType`, `subjectId`))",
+        "fields": [
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "subjectType",
+            "columnName": "subjectType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "subjectId",
+            "columnName": "subjectId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "voiceId",
+            "columnName": "voiceId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "locked",
+            "columnName": "locked",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'user'"
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "bookUrl",
+            "subjectType",
+            "subjectId"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_book_voice_bindings_bookUrl_subjectType",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "subjectType"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_voice_bindings_bookUrl_subjectType` ON `${TABLE_NAME}` (`bookUrl`, `subjectType`)"
+          },
+          {
+            "name": "index_book_voice_bindings_voiceId",
+            "unique": false,
+            "columnNames": [
+              "voiceId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_book_voice_bindings_voiceId` ON `${TABLE_NAME}` (`voiceId`)"
+          }
+        ]
+      },
+      {
+        "tableName": "chapter_speech_analysis",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `contentHash` TEXT NOT NULL, `resolverVersion` TEXT NOT NULL, `characterRevision` TEXT NOT NULL DEFAULT '', `status` TEXT NOT NULL DEFAULT 'pending', `error` TEXT NOT NULL DEFAULT '', `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "contentHash",
+            "columnName": "contentHash",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "resolverVersion",
+            "columnName": "resolverVersion",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "characterRevision",
+            "columnName": "characterRevision",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "status",
+            "columnName": "status",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'pending'"
+          },
+          {
+            "fieldPath": "error",
+            "columnName": "error",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_chapter_speech_analysis_bookUrl_chapterIndex_contentHash_resolverVersion",
+            "unique": true,
+            "columnNames": [
+              "bookUrl",
+              "chapterIndex",
+              "contentHash",
+              "resolverVersion"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapter_speech_analysis_bookUrl_chapterIndex_contentHash_resolverVersion` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`, `contentHash`, `resolverVersion`)"
+          },
+          {
+            "name": "index_chapter_speech_analysis_bookUrl_chapterIndex_updatedAt",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "chapterIndex",
+              "updatedAt"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_chapter_speech_analysis_bookUrl_chapterIndex_updatedAt` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`, `updatedAt`)"
+          }
+        ]
+      },
+      {
+        "tableName": "chapter_speech_segments",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `analysisId` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `paragraphIndex` INTEGER NOT NULL, `start` INTEGER NOT NULL, `end` INTEGER NOT NULL, `chapterPosition` INTEGER NOT NULL, `text` TEXT NOT NULL, `roleType` TEXT NOT NULL, `characterId` TEXT, `characterName` TEXT NOT NULL DEFAULT '', `emotion` TEXT NOT NULL DEFAULT '', `confidence` REAL NOT NULL DEFAULT 0, `source` TEXT NOT NULL, `userLocked` INTEGER NOT NULL DEFAULT 0, `createdAt` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`id`))",
+        "fields": [
+          {
+            "fieldPath": "id",
+            "columnName": "id",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "analysisId",
+            "columnName": "analysisId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "paragraphIndex",
+            "columnName": "paragraphIndex",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "start",
+            "columnName": "start",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "end",
+            "columnName": "end",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterPosition",
+            "columnName": "chapterPosition",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "text",
+            "columnName": "text",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "roleType",
+            "columnName": "roleType",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "characterId",
+            "columnName": "characterId",
+            "affinity": "TEXT"
+          },
+          {
+            "fieldPath": "characterName",
+            "columnName": "characterName",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "emotion",
+            "columnName": "emotion",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "confidence",
+            "columnName": "confidence",
+            "affinity": "REAL",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "source",
+            "columnName": "source",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "userLocked",
+            "columnName": "userLocked",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_chapter_speech_segments_analysisId_paragraphIndex_start_end",
+            "unique": true,
+            "columnNames": [
+              "analysisId",
+              "paragraphIndex",
+              "start",
+              "end"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_chapter_speech_segments_analysisId_paragraphIndex_start_end` ON `${TABLE_NAME}` (`analysisId`, `paragraphIndex`, `start`, `end`)"
+          },
+          {
+            "name": "index_chapter_speech_segments_bookUrl_chapterIndex_paragraphIndex",
+            "unique": false,
+            "columnNames": [
+              "bookUrl",
+              "chapterIndex",
+              "paragraphIndex"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_chapter_speech_segments_bookUrl_chapterIndex_paragraphIndex` ON `${TABLE_NAME}` (`bookUrl`, `chapterIndex`, `paragraphIndex`)"
+          },
+          {
+            "name": "index_chapter_speech_segments_characterId",
+            "unique": false,
+            "columnNames": [
+              "characterId"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_chapter_speech_segments_characterId` ON `${TABLE_NAME}` (`characterId`)"
+          }
+        ]
+      },
+      {
+        "tableName": "cloud_tts_engines",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`id` TEXT NOT NULL, `name` TEXT NOT NULL, `provider` TEXT NOT NULL, `baseUrl` TEXT NOT NULL DEFAULT '', `apiKey` TEXT NOT NULL DEFAULT '', `secretKey` TEXT NOT NULL DEFAULT '', `region` TEXT NOT NULL DEFAULT '', `appId` TEXT NOT NULL DEFAULT '', `model` TEXT NOT NULL DEFAULT '', `optionsJson` TEXT NOT NULL DEFAULT '{}', `enabled` INTEGER NOT NULL DEFAULT 1, `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": "provider",
+            "columnName": "provider",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "baseUrl",
+            "columnName": "baseUrl",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "apiKey",
+            "columnName": "apiKey",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "secretKey",
+            "columnName": "secretKey",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "region",
+            "columnName": "region",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "appId",
+            "columnName": "appId",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "model",
+            "columnName": "model",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "optionsJson",
+            "columnName": "optionsJson",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "'{}'"
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "fieldPath": "createdAt",
+            "columnName": "createdAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "id"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_cloud_tts_engines_provider_enabled",
+            "unique": false,
+            "columnNames": [
+              "provider",
+              "enabled"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_cloud_tts_engines_provider_enabled` ON `${TABLE_NAME}` (`provider`, `enabled`)"
+          }
+        ]
+      },
+      {
+        "tableName": "exact_chapter_page_counts",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bookId` TEXT NOT NULL, `chapterId` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `contentHash` INTEGER NOT NULL, `layoutSignature` INTEGER NOT NULL, `engineVersion` INTEGER NOT NULL, `pageCount` INTEGER NOT NULL, `updatedAt` INTEGER NOT NULL, PRIMARY KEY(`bookId`, `chapterId`, `layoutSignature`), FOREIGN KEY(`bookId`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "bookId",
+            "columnName": "bookId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterId",
+            "columnName": "chapterId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "chapterIndex",
+            "columnName": "chapterIndex",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "contentHash",
+            "columnName": "contentHash",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "layoutSignature",
+            "columnName": "layoutSignature",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "engineVersion",
+            "columnName": "engineVersion",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pageCount",
+            "columnName": "pageCount",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "updatedAt",
+            "columnName": "updatedAt",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "autoGenerate": false,
+          "columnNames": [
+            "bookId",
+            "chapterId",
+            "layoutSignature"
+          ]
+        },
+        "indices": [
+          {
+            "name": "index_exact_chapter_page_counts_bookId_layoutSignature",
+            "unique": false,
+            "columnNames": [
+              "bookId",
+              "layoutSignature"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_exact_chapter_page_counts_bookId_layoutSignature` ON `${TABLE_NAME}` (`bookId`, `layoutSignature`)"
+          }
+        ],
+        "foreignKeys": [
+          {
+            "table": "books",
+            "onDelete": "CASCADE",
+            "onUpdate": "NO ACTION",
+            "columns": [
+              "bookId"
+            ],
+            "referencedColumns": [
+              "bookUrl"
+            ]
+          }
+        ]
+      }
+    ],
+    "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, '0458658832e21f9dde1ecae6a827691f')"
+    ]
+  }
+}

+ 1 - 1
app/src/main/java/io/legado/app/data/AppDatabase.kt

@@ -110,7 +110,7 @@ val appDb by lazy {
 }
 }
 
 
 @Database(
 @Database(
-    version = 98,
+    version = 99,
     exportSchema = true,
     exportSchema = true,
     entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
     entities = [Book::class, BookGroup::class, BookSource::class, BookChapter::class,
         ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,
         ReplaceRule::class, SearchBook::class, SearchKeyword::class, Cookie::class,

+ 12 - 1
app/src/main/java/io/legado/app/data/DatabaseMigrations.kt

@@ -20,7 +20,7 @@ object DatabaseMigrations {
             migration_31_32, migration_32_33, migration_33_34, migration_34_35,
             migration_31_32, migration_32_33, migration_33_34, migration_34_35,
             migration_35_36, migration_36_37, migration_37_38, migration_38_39,
             migration_35_36, migration_36_37, migration_37_38, migration_38_39,
             migration_39_40, migration_40_41, migration_41_42, migration_42_43,
             migration_39_40, migration_40_41, migration_41_42, migration_42_43,
-            migration_82_83,
+            migration_82_83, migration_98_99,
         )
         )
     }
     }
 
 
@@ -496,4 +496,15 @@ object DatabaseMigrations {
         columnName = "enabledReview"
         columnName = "enabledReview"
     )
     )
     class Migration_64_65 : AutoMigrationSpec
     class Migration_64_65 : AutoMigrationSpec
+
+    private val migration_98_99 = object : Migration(98, 99) {
+        override fun migrate(database: SupportSQLiteDatabase) {
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN fontWeight INTEGER NOT NULL DEFAULT 400")
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN isItalic INTEGER NOT NULL DEFAULT 0")
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npLeft REAL NOT NULL DEFAULT 0.1")
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npRight REAL NOT NULL DEFAULT 0.1")
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npTop REAL NOT NULL DEFAULT 0.1")
+            database.execSQL("ALTER TABLE highlightRules ADD COLUMN npBottom REAL NOT NULL DEFAULT 0.1")
+        }
+    }
 }
 }

+ 7 - 0
app/src/main/java/io/legado/app/data/entities/HighlightRule.kt

@@ -26,6 +26,12 @@ data class HighlightRule(
     var bgImageScale: Float = 1f,
     var bgImageScale: Float = 1f,
     var configName: String? = null,
     var configName: String? = null,
     var fontPath: String? = null,
     var fontPath: String? = null,
+    var fontWeight: Int = 400,
+    var isItalic: Boolean = false,
+    var npLeft: Float = 0.1f,
+    var npRight: Float = 0.1f,
+    var npTop: Float = 0.1f,
+    var npBottom: Float = 0.1f,
 ) {
 ) {
 
 
     fun styleSummary(): String {
     fun styleSummary(): String {
@@ -54,6 +60,7 @@ data class HighlightRule(
                 when (bgImageFit) {
                 when (bgImageFit) {
                     1 -> "背景图(拉伸)"
                     1 -> "背景图(拉伸)"
                     2 -> "背景图(裁剪)"
                     2 -> "背景图(裁剪)"
+                    3 -> "背景图(九宫格)"
                     else -> "背景图(平铺)"
                     else -> "背景图(平铺)"
                 }
                 }
             )
             )

+ 7 - 1
app/src/main/java/io/legado/app/data/repository/HighlightRuleRepository.kt

@@ -170,10 +170,16 @@ class HighlightRuleRepository(
                 .coerceIn(0f, 20f),
                 .coerceIn(0f, 20f),
             underlineSvgPath = runCatching { rule.underlineSvgPath }.getOrNull(),
             underlineSvgPath = runCatching { rule.underlineSvgPath }.getOrNull(),
             bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
             bgImage = runCatching { rule.bgImage }.getOrNull()?.takeIf { it.isNotBlank() },
-            bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 2),
+            bgImageFit = runCatching { rule.bgImageFit }.getOrDefault(0).coerceIn(0, 3),
             bgImageScale = runCatching { rule.bgImageScale }.getOrDefault(1f).coerceIn(0.1f, 5f),
             bgImageScale = runCatching { rule.bgImageScale }.getOrDefault(1f).coerceIn(0.1f, 5f),
             configName = runCatching { rule.configName }.getOrNull()?.takeIf { it.isNotBlank() },
             configName = runCatching { rule.configName }.getOrNull()?.takeIf { it.isNotBlank() },
             fontPath = runCatching { rule.fontPath }.getOrNull()?.takeIf { it.isNotBlank() },
             fontPath = runCatching { rule.fontPath }.getOrNull()?.takeIf { it.isNotBlank() },
+            fontWeight = runCatching { rule.fontWeight }.getOrDefault(400).coerceIn(300, 700),
+            isItalic = runCatching { rule.isItalic }.getOrDefault(false),
+            npLeft = runCatching { rule.npLeft }.getOrDefault(0.1f).coerceIn(0f, 0.5f),
+            npRight = runCatching { rule.npRight }.getOrDefault(0.1f).coerceIn(0f, 0.5f),
+            npTop = runCatching { rule.npTop }.getOrDefault(0.1f).coerceIn(0f, 0.5f),
+            npBottom = runCatching { rule.npBottom }.getOrDefault(0.1f).coerceIn(0f, 0.5f),
         )
         )
     }
     }
 
 

+ 166 - 7
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt

@@ -63,6 +63,7 @@ data class TextLine(
     val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
     val chapterIndices: IntRange get() = chapterPosition..chapterPosition + charSize
     val height: Float inline get() = lineBottom - lineTop
     val height: Float inline get() = lineBottom - lineTop
     val canvasRecorder = CanvasRecorderFactory.create()
     val canvasRecorder = CanvasRecorderFactory.create()
+    private var nineSliceFrames: List<NineSliceFrameData> = emptyList()
     var searchResultColumnCount = 0
     var searchResultColumnCount = 0
     var isReadAloud: Boolean = false
     var isReadAloud: Boolean = false
         set(value) {
         set(value) {
@@ -82,7 +83,7 @@ data class TextLine(
     fun addColumn(column: BaseColumn) {
     fun addColumn(column: BaseColumn) {
         if (column !is TextColumn) {
         if (column !is TextColumn) {
             onlyTextColumn = false
             onlyTextColumn = false
-        } else if (column.textColor != null || column.bgColor != null || column.underlineMode != 0 || column.bgImage.isNotEmpty() || column.fontPath.isNotEmpty()) {
+        } else if (column.textColor != null || column.bgColor != null || column.underlineMode != 0 || column.bgImage.isNotEmpty() || column.fontPath.isNotEmpty() || column.fontWeight != 400 || column.isItalic) {
             onlyTextColumn = false
             onlyTextColumn = false
         }
         }
         column.textLine = this
         column.textLine = this
@@ -167,10 +168,15 @@ data class TextLine(
         } else {
         } else {
             drawTextLine(view, canvas)
             drawTextLine(view, canvas)
         }
         }
+        // Draw nine-slice frames on the real canvas (not the line recorder)
+        // to avoid clipping by the recorder's height bounds
+        drawNineSliceFrames(canvas, nineSliceFrames)
     }
     }
 
 
     private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
     private fun drawTextLine(view: ContentTextView, canvas: Canvas) {
-        drawStyledBackgrounds(canvas)
+        val frames = mutableListOf<NineSliceFrameData>()
+        drawStyledBackgrounds(canvas, frames)
+        nineSliceFrames = frames
         drawBgColors(canvas)
         drawBgColors(canvas)
         if (checkFastDraw()) {
         if (checkFastDraw()) {
             fastDrawTextLine(view, canvas)
             fastDrawTextLine(view, canvas)
@@ -267,7 +273,7 @@ data class TextLine(
     /**
     /**
      * 绘制高亮规则匹配文本的背景图
      * 绘制高亮规则匹配文本的背景图
      */
      */
-    private fun drawStyledBackgrounds(canvas: Canvas) {
+    private fun drawStyledBackgrounds(canvas: Canvas, nineSliceFrames: MutableList<NineSliceFrameData>) {
         if (isImage || columns.isEmpty()) return
         if (isImage || columns.isEmpty()) return
         if (columns.none { (it as? TextBaseColumn)?.bgImage?.isNotEmpty() == true }) return
         if (columns.none { (it as? TextBaseColumn)?.bgImage?.isNotEmpty() == true }) return
         var rangeStart = 0f
         var rangeStart = 0f
@@ -275,15 +281,23 @@ data class TextLine(
         var currentBgImage = ""
         var currentBgImage = ""
         var currentBgImageFit = 0
         var currentBgImageFit = 0
         var currentBgImageScale = 1f
         var currentBgImageScale = 1f
+        var currentNpLeft = 0.1f
+        var currentNpRight = 0.1f
+        var currentNpTop = 0.1f
+        var currentNpBottom = 0.1f
         var active = false
         var active = false
         columns.forEachIndexed { index, column ->
         columns.forEachIndexed { index, column ->
             val textColumn = column as? TextBaseColumn
             val textColumn = column as? TextBaseColumn
             val bgImage = textColumn?.bgImage ?: ""
             val bgImage = textColumn?.bgImage ?: ""
             val bgImageFit = textColumn?.bgImageFit ?: 0
             val bgImageFit = textColumn?.bgImageFit ?: 0
             val bgImageScale = textColumn?.bgImageScale ?: 1f
             val bgImageScale = textColumn?.bgImageScale ?: 1f
+            val npLeft = textColumn?.npLeft ?: 0.1f
+            val npRight = textColumn?.npRight ?: 0.1f
+            val npTop = textColumn?.npTop ?: 0.1f
+            val npBottom = textColumn?.npBottom ?: 0.1f
             when {
             when {
                 bgImage.isEmpty() && active -> {
                 bgImage.isEmpty() && active -> {
-                    drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale)
+                    drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale, nineSliceFrames, currentNpLeft, currentNpRight, currentNpTop, currentNpBottom)
                     active = false
                     active = false
                 }
                 }
                 bgImage.isNotEmpty() && !active -> {
                 bgImage.isNotEmpty() && !active -> {
@@ -292,22 +306,30 @@ data class TextLine(
                     currentBgImage = bgImage
                     currentBgImage = bgImage
                     currentBgImageFit = bgImageFit
                     currentBgImageFit = bgImageFit
                     currentBgImageScale = bgImageScale
                     currentBgImageScale = bgImageScale
+                    currentNpLeft = npLeft
+                    currentNpRight = npRight
+                    currentNpTop = npTop
+                    currentNpBottom = npBottom
                     active = true
                     active = true
                 }
                 }
-                bgImage.isNotEmpty() && bgImage == currentBgImage && bgImageFit == currentBgImageFit && bgImageScale == currentBgImageScale -> {
+                bgImage.isNotEmpty() && bgImage == currentBgImage && bgImageFit == currentBgImageFit && bgImageScale == currentBgImageScale && npLeft == currentNpLeft && npRight == currentNpRight && npTop == currentNpTop && npBottom == currentNpBottom -> {
                     rangeEnd = textColumn!!.end
                     rangeEnd = textColumn!!.end
                 }
                 }
                 bgImage.isNotEmpty() -> {
                 bgImage.isNotEmpty() -> {
-                    drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale)
+                    drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale, nineSliceFrames, currentNpLeft, currentNpRight, currentNpTop, currentNpBottom)
                     rangeStart = textColumn!!.start
                     rangeStart = textColumn!!.start
                     rangeEnd = textColumn.end
                     rangeEnd = textColumn.end
                     currentBgImage = bgImage
                     currentBgImage = bgImage
                     currentBgImageFit = bgImageFit
                     currentBgImageFit = bgImageFit
                     currentBgImageScale = bgImageScale
                     currentBgImageScale = bgImageScale
+                    currentNpLeft = npLeft
+                    currentNpRight = npRight
+                    currentNpTop = npTop
+                    currentNpBottom = npBottom
                 }
                 }
             }
             }
             if (active && index == columns.lastIndex) {
             if (active && index == columns.lastIndex) {
-                drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale)
+                drawBgImageSegment(canvas, rangeStart, rangeEnd, currentBgImage, currentBgImageFit, currentBgImageScale, nineSliceFrames, currentNpLeft, currentNpRight, currentNpTop, currentNpBottom)
             }
             }
         }
         }
     }
     }
@@ -511,6 +533,11 @@ data class TextLine(
         bgImage: String,
         bgImage: String,
         bgImageFit: Int,
         bgImageFit: Int,
         bgImageScale: Float,
         bgImageScale: Float,
+        nineSliceFrames: MutableList<NineSliceFrameData> = mutableListOf(),
+        npLeft: Float = 0.1f,
+        npRight: Float = 0.1f,
+        npTop: Float = 0.1f,
+        npBottom: Float = 0.1f,
     ) {
     ) {
         val bitmap = getBgBitmap(bgImage) ?: return
         val bitmap = getBgBitmap(bgImage) ?: return
         val paint = PaintPool.obtain()
         val paint = PaintPool.obtain()
@@ -549,6 +576,9 @@ data class TextLine(
                 canvas.drawBitmap(bitmap, null, android.graphics.RectF(dx, dy, dx + scaledW, dy + scaledH), paint)
                 canvas.drawBitmap(bitmap, null, android.graphics.RectF(dx, dy, dx + scaledW, dy + scaledH), paint)
                 canvas.restore()
                 canvas.restore()
             }
             }
+            3 -> {
+                drawNineSliceCenter(canvas, bitmap, startX, endX, npLeft, npRight, npTop, npBottom, paint, nineSliceFrames)
+            }
             else -> {
             else -> {
                 val tileBitmap = if (scale != 1f) {
                 val tileBitmap = if (scale != 1f) {
                     val sw = (bitmap.width * scale).toInt().coerceAtLeast(1)
                     val sw = (bitmap.width * scale).toInt().coerceAtLeast(1)
@@ -569,6 +599,119 @@ data class TextLine(
         PaintPool.recycle(paint)
         PaintPool.recycle(paint)
     }
     }
 
 
+    private fun drawNineSliceCenter(
+        canvas: Canvas,
+        bitmap: Bitmap,
+        startX: Float,
+        endX: Float,
+        npLeft: Float,
+        npRight: Float,
+        npTop: Float,
+        npBottom: Float,
+        paint: Paint,
+        nineSliceFrames: MutableList<NineSliceFrameData>,
+    ) {
+        val bw = bitmap.width.toFloat()
+        val bh = bitmap.height.toFloat()
+        val leftPx = (bw * npLeft).toInt().coerceAtLeast(0)
+        val rightPx = (bw * (1f - npRight)).toInt().coerceAtMost(bitmap.width)
+        val topPx = (bh * npTop).toInt().coerceAtLeast(0)
+        val bottomPx = (bh * (1f - npBottom)).toInt().coerceAtMost(bitmap.height)
+
+        // 中心(5)覆盖全部文字高度
+        canvas.drawBitmap(
+            bitmap,
+            android.graphics.Rect(leftPx, topPx, rightPx, bottomPx),
+            android.graphics.RectF(startX, 0f, endX, height),
+            paint,
+        )
+
+        nineSliceFrames.add(
+            NineSliceFrameData(
+                bitmap = bitmap,
+                startX = startX,
+                endX = endX,
+                frameStartX = startX - leftPx.toFloat(),
+                frameEndX = endX + (bw - rightPx.toFloat()),
+                leftPx = leftPx,
+                rightPx = rightPx,
+                topPx = topPx,
+                bottomPx = bottomPx,
+                lineH = height,
+            )
+        )
+    }
+
+    /**
+     * 绘制九宫格边框(1,2,3,4,6,7,8,9),在文字之上。
+     * 上下边框延伸到行外,但根据行间距自适应缩放避免多行重叠。
+     */
+    private fun drawNineSliceFrames(canvas: Canvas, frames: List<NineSliceFrameData>) {
+        if (frames.isEmpty()) return
+        val paint = PaintPool.obtain()
+        paint.style = Paint.Style.FILL
+        paint.isAntiAlias = true
+        paint.isFilterBitmap = true
+
+        // 行间距可用空间:每行上下各分得一半间距
+        val lineH = frames.first().lineH
+        val gap = (ChapterProvider.lineSpacingExtra - 1f).coerceAtLeast(0f) * lineH
+        val halfGap = gap * 0.5f
+
+        for (frame in frames) {
+            val bitmap = frame.bitmap
+            val startX = frame.startX
+            val endX = frame.endX
+            val frameStartX = frame.frameStartX
+            val frameEndX = frame.frameEndX
+            val leftPx = frame.leftPx
+            val rightPx = frame.rightPx
+            val topPx = frame.topPx
+            val bottomPx = frame.bottomPx
+
+            val rawTopH = topPx.toFloat()
+            val rawBotH = (bitmap.height - bottomPx).toFloat()
+
+            // 上下边框等比例缩放以适应行间距,避免多行高亮时重叠
+            val maxFrameH = maxOf(rawTopH, rawBotH).coerceAtLeast(0.1f)
+            val overflowScale = (halfGap / maxFrameH).coerceIn(0f, 1f)
+            val topH = rawTopH * overflowScale
+            val botH = rawBotH * overflowScale
+
+            // Row 1: 上方边框 (1, 2, 3) - 向上延伸到文字区域之上
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(0, 0, leftPx, topPx),
+                android.graphics.RectF(frameStartX, -topH, startX, 0f), paint)
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(leftPx, 0, rightPx, topPx),
+                android.graphics.RectF(startX, -topH, endX, 0f), paint)
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(rightPx, 0, bitmap.width, topPx),
+                android.graphics.RectF(endX, -topH, frameEndX, 0f), paint)
+
+            // Row 2: 同行边框 (4, 6) - 全高
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(0, topPx, leftPx, bottomPx),
+                android.graphics.RectF(frameStartX, 0f, startX, lineH), paint)
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(rightPx, topPx, bitmap.width, bottomPx),
+                android.graphics.RectF(endX, 0f, frameEndX, lineH), paint)
+
+            // Row 3: 下方边框 (7, 8, 9) - 向下延伸到文字区域之下
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(0, bottomPx, leftPx, bitmap.height),
+                android.graphics.RectF(frameStartX, lineH, startX, lineH + botH), paint)
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(leftPx, bottomPx, rightPx, bitmap.height),
+                android.graphics.RectF(startX, lineH, endX, lineH + botH), paint)
+            canvas.drawBitmap(bitmap,
+                android.graphics.Rect(rightPx, bottomPx, bitmap.width, bitmap.height),
+                android.graphics.RectF(endX, lineH, frameEndX, lineH + botH), paint)
+        }
+
+        PaintPool.recycle(paint)
+    }
+
     private fun drawNinePatchBitmap(
     private fun drawNinePatchBitmap(
         canvas: Canvas,
         canvas: Canvas,
         bitmap: Bitmap,
         bitmap: Bitmap,
@@ -710,6 +853,22 @@ data class TextLine(
         canvasRecorder.recycle()
         canvasRecorder.recycle()
     }
     }
 
 
+    /**
+     * 九宫格边框数据,用于在文字之上绘制 1,2,3,4,6,7,8,9
+     */
+    private class NineSliceFrameData(
+        val bitmap: Bitmap,
+        val startX: Float,
+        val endX: Float,
+        val frameStartX: Float,
+        val frameEndX: Float,
+        val leftPx: Int,
+        val rightPx: Int,
+        val topPx: Int,
+        val bottomPx: Int,
+        val lineH: Float,
+    )
+
     @SuppressLint("NewApi")
     @SuppressLint("NewApi")
     companion object {
     companion object {
         val emptyTextLine = TextLine()
         val emptyTextLine = TextLine()

+ 6 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextBaseColumn.kt

@@ -18,6 +18,12 @@ interface TextBaseColumn : BaseColumn {
     val bgImageFit: Int
     val bgImageFit: Int
     val bgImageScale: Float
     val bgImageScale: Float
     val fontPath: String
     val fontPath: String
+    val fontWeight: Int
+    val isItalic: Boolean
+    val npLeft: Float
+    val npRight: Float
+    val npTop: Float
+    val npBottom: Float
     var selected: Boolean
     var selected: Boolean
     var isSearchResult: Boolean
     var isSearchResult: Boolean
 }
 }

+ 23 - 4
app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextColumn.kt

@@ -31,6 +31,12 @@ data class TextColumn(
     override val bgImageFit: Int = 0,
     override val bgImageFit: Int = 0,
     override val bgImageScale: Float = 1f,
     override val bgImageScale: Float = 1f,
     override val fontPath: String = "",
     override val fontPath: String = "",
+    override val fontWeight: Int = 400,
+    override val isItalic: Boolean = false,
+    override val npLeft: Float = 0.1f,
+    override val npRight: Float = 0.1f,
+    override val npTop: Float = 0.1f,
+    override val npBottom: Float = 0.1f,
 ) : TextBaseColumn {
 ) : TextBaseColumn {
 
 
     override var textLine: TextLine = emptyTextLine
     override var textLine: TextLine = emptyTextLine
@@ -113,21 +119,34 @@ data class TextColumn(
     }
     }
 
 
     private fun getCustomTypeface(): Typeface? {
     private fun getCustomTypeface(): Typeface? {
-        return getTypeface(fontPath)
+        return getTypeface(fontPath, fontWeight, isItalic)
     }
     }
 
 
     companion object {
     companion object {
         private val typefaceCache = HashMap<String, Typeface>()
         private val typefaceCache = HashMap<String, Typeface>()
         private val failedTypefaceLoads = ResourceLoadFailureCache<String>()
         private val failedTypefaceLoads = ResourceLoadFailureCache<String>()
 
 
-        internal fun getTypeface(fontPath: String): Typeface? {
-            if (fontPath.isEmpty()) return null
-            typefaceCache[fontPath]?.let { return it }
+        internal fun getTypeface(fontPath: String, fontWeight: Int = 400, isItalic: Boolean = false): Typeface? {
+            if (fontPath.isEmpty()) {
+                return applyStyleTypeface(null, fontWeight, isItalic)
+            }
+            typefaceCache[fontPath]?.let { return applyStyleTypeface(it, fontWeight, isItalic) }
             return failedTypefaceLoads.load(fontPath) {
             return failedTypefaceLoads.load(fontPath) {
                 loadTypeface(fontPath)?.also { typeface ->
                 loadTypeface(fontPath)?.also { typeface ->
                     typefaceCache[fontPath] = typeface
                     typefaceCache[fontPath] = typeface
                 }
                 }
+            }?.let { applyStyleTypeface(it, fontWeight, isItalic) }
+        }
+
+        private fun applyStyleTypeface(typeface: Typeface?, fontWeight: Int, isItalic: Boolean): Typeface? {
+            if (fontWeight == 400 && !isItalic) return typeface
+            val style = when {
+                isItalic && fontWeight == 700 -> Typeface.BOLD_ITALIC
+                isItalic -> Typeface.ITALIC
+                fontWeight == 700 -> Typeface.BOLD
+                else -> Typeface.NORMAL // 300 (Light) falls back to NORMAL via Typeface.create
             }
             }
+            return Typeface.create(typeface ?: Typeface.DEFAULT, style)
         }
         }
 
 
         private fun loadTypeface(fontPath: String): Typeface? {
         private fun loadTypeface(fontPath: String): Typeface? {

+ 6 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/column/TextHtmlColumn.kt

@@ -31,6 +31,12 @@ data class TextHtmlColumn(
     override val bgImageFit: Int = 0,
     override val bgImageFit: Int = 0,
     override val bgImageScale: Float = 1f,
     override val bgImageScale: Float = 1f,
     override val fontPath: String = "",
     override val fontPath: String = "",
+    override val fontWeight: Int = 400,
+    override val isItalic: Boolean = false,
+    override val npLeft: Float = 0.1f,
+    override val npRight: Float = 0.1f,
+    override val npTop: Float = 0.1f,
+    override val npBottom: Float = 0.1f,
 ) : TextBaseColumn {
 ) : TextBaseColumn {
 
 
     override val textColor: Int? get() = mTextColor
     override val textColor: Int? get() = mTextColor

+ 7 - 1
app/src/main/java/io/legado/app/ui/book/read/page/provider/CharStyle.kt

@@ -15,7 +15,13 @@ data class CharStyle(
     val bgImageFit: Int = 0,
     val bgImageFit: Int = 0,
     val bgImageScale: Float = 1f,
     val bgImageScale: Float = 1f,
     val fontPath: String = "",
     val fontPath: String = "",
+    val fontWeight: Int = 400,
+    val isItalic: Boolean = false,
+    val npLeft: Float = 0.1f,
+    val npRight: Float = 0.1f,
+    val npTop: Float = 0.1f,
+    val npBottom: Float = 0.1f,
 ) {
 ) {
     val hasStyle: Boolean
     val hasStyle: Boolean
-        get() = textColor != null || bgColor != null || underlineMode != 0 || bgImage.isNotEmpty() || fontPath.isNotEmpty()
+        get() = textColor != null || bgColor != null || underlineMode != 0 || bgImage.isNotEmpty() || fontPath.isNotEmpty() || fontWeight != 400 || isItalic
 }
 }

+ 192 - 29
app/src/main/java/io/legado/app/ui/book/read/page/provider/TextChapterLayout.kt

@@ -1,5 +1,6 @@
-package io.legado.app.ui.book.read.page.provider
+package io.legado.app.ui.book.read.page.provider
 
 
+import android.graphics.BitmapFactory
 import android.graphics.Paint
 import android.graphics.Paint
 import android.os.Build
 import android.os.Build
 import android.text.Layout
 import android.text.Layout
@@ -83,7 +84,11 @@ class TextChapterLayout(
         fun invalidateRegexCache() {
         fun invalidateRegexCache() {
             cachedHighlightRules = null
             cachedHighlightRules = null
             cachedHighlightRulesConfigName = null
             cachedHighlightRulesConfigName = null
+            bitmapDimsCache.clear()
         }
         }
+
+        /** 九宫格图片尺寸缓存 (width, height),避免重复读取文件 */
+        private val bitmapDimsCache = mutableMapOf<String, Pair<Int, Int>?>()
     }
     }
 
 
     private val compiledHighlightRules: List<CompiledHighlightRule>
     private val compiledHighlightRules: List<CompiledHighlightRule>
@@ -1011,9 +1016,10 @@ class TextChapterLayout(
                     }
                     }
                     addCharsToLineNatural(
                     addCharsToLineNatural(
                         book, absStartX, textLine, words,
                         book, absStartX, textLine, words,
-                        startX, !isTitle && lineIndex == 0, widths, srcList, clickList, charStyles, lineStart
+                        startX, !isTitle && lineIndex == 0, widths, srcList, clickList, charStyles, lineStart, textPaint,
                     )
                     )
                 }
                 }
+
                 else -> {
                 else -> {
                     if (
                     if (
                         isTitle &&
                         isTitle &&
@@ -1023,7 +1029,7 @@ class TextChapterLayout(
                         val startX = (visibleWidth - desiredWidth) / 2
                         val startX = (visibleWidth - desiredWidth) / 2
                         addCharsToLineNatural(
                         addCharsToLineNatural(
                             book, absStartX, textLine, words,
                             book, absStartX, textLine, words,
-                            startX, false, widths, srcList, clickList, charStyles, lineStart
+                            startX, false, widths, srcList, clickList, charStyles, lineStart, textPaint,
                         )
                         )
                     } else {
                     } else {
                         addCharsToLineMiddle(
                         addCharsToLineMiddle(
@@ -1090,7 +1096,7 @@ class TextChapterLayout(
         if (!textFullJustify) {
         if (!textFullJustify) {
             addCharsToLineNatural(
             addCharsToLineNatural(
                 book, absStartX, textLine, words,
                 book, absStartX, textLine, words,
-                x, true, textWidths, srcList, clickList, charStyles, lineStart
+                x, true, textWidths, srcList, clickList, charStyles, lineStart, textPaint,
             )
             )
             return
             return
         }
         }
@@ -1139,20 +1145,56 @@ class TextChapterLayout(
         if (!textFullJustify) {
         if (!textFullJustify) {
             addCharsToLineNatural(
             addCharsToLineNatural(
                 book, absStartX, textLine, words,
                 book, absStartX, textLine, words,
-                startX, false, textWidths, srcList, clickList, charStyles, lineStart
+                startX, false, textWidths, srcList, clickList, charStyles, lineStart, textPaint,
             )
             )
             return
             return
         }
         }
         val residualWidth = visibleWidth - desiredWidth
         val residualWidth = visibleWidth - desiredWidth
         val spaceSize = words.count { it == " " }
         val spaceSize = words.count { it == " " }
         textLine.startX = absStartX + startX
         textLine.startX = absStartX + startX
+
+        // 九宫格状态追踪
+        val continuingFromPrevLine = lineStart > 0 && charStyles?.getOrNull(lineStart - 1)?.bgImageFit == 3
+        var inNineSlice = false
+        var nsBgImage = ""
+        var nsNpLeft = 0.1f
+        var nsNpRight = 0.1f
+
         if (spaceSize > 1) {
         if (spaceSize > 1) {
             val d = residualWidth / spaceSize
             val d = residualWidth / spaceSize
             textLine.wordSpacing = d
             textLine.wordSpacing = d
             var x = startX
             var x = startX
+            // 跨行延续:添加 margin-left
+            if (continuingFromPrevLine) {
+                val prevStyle = charStyles.getOrNull(lineStart - 1)
+                if (prevStyle != null) {
+                    nsBgImage = prevStyle.bgImage
+                    nsNpLeft = prevStyle.npLeft
+                    nsNpRight = prevStyle.npRight
+                    val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginLeft
+                    inNineSlice = true
+                }
+            }
             for (index in words.indices) {
             for (index in words.indices) {
                 val char = words[index]
                 val char = words[index]
                 val cw = textWidths[index]
                 val cw = textWidths[index]
+                val style = charStyles?.getOrNull(lineStart + index)
+                val isNineSlice = style?.bgImageFit == 3
+
+                if (isNineSlice && !inNineSlice) {
+                    nsBgImage = style.bgImage
+                    nsNpLeft = style.npLeft
+                    nsNpRight = style.npRight
+                    val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginLeft
+                    inNineSlice = true
+                } else if (!isNineSlice && inNineSlice) {
+                    val (_, marginRight) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginRight
+                    inNineSlice = false
+                }
+
                 val x1 = if (char == " ") {
                 val x1 = if (char == " ") {
                     if (index != words.lastIndex) (x + cw + d) else (x + cw)
                     if (index != words.lastIndex) (x + cw + d) else (x + cw)
                 } else {
                 } else {
@@ -1171,9 +1213,37 @@ class TextChapterLayout(
             textLine.extraLetterSpacingOffsetX = -d / 2
             textLine.extraLetterSpacingOffsetX = -d / 2
             textLine.extraLetterSpacing = d / textPaint.textSize
             textLine.extraLetterSpacing = d / textPaint.textSize
             var x = startX
             var x = startX
+            // 跨行延续:添加 margin-left
+            if (continuingFromPrevLine) {
+                val prevStyle = charStyles.getOrNull(lineStart - 1)
+                if (prevStyle != null) {
+                    nsBgImage = prevStyle.bgImage
+                    nsNpLeft = prevStyle.npLeft
+                    nsNpRight = prevStyle.npRight
+                    val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginLeft
+                    inNineSlice = true
+                }
+            }
             for (index in words.indices) {
             for (index in words.indices) {
                 val char = words[index]
                 val char = words[index]
                 val cw = textWidths[index]
                 val cw = textWidths[index]
+                val style = charStyles?.getOrNull(lineStart + index)
+                val isNineSlice = style?.bgImageFit == 3
+
+                if (isNineSlice && !inNineSlice) {
+                    nsBgImage = style.bgImage
+                    nsNpLeft = style.npLeft
+                    nsNpRight = style.npRight
+                    val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginLeft
+                    inNineSlice = true
+                } else if (!isNineSlice && inNineSlice) {
+                    val (_, marginRight) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                    x += marginRight
+                    inNineSlice = false
+                }
+
                 val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
                 val x1 = if (index != words.lastIndex) (x + cw + d) else (x + cw)
                 addCharToLine(
                 addCharToLine(
                     book, absStartX, textLine, char,
                     book, absStartX, textLine, char,
@@ -1183,7 +1253,38 @@ class TextChapterLayout(
                 x = x1
                 x = x1
             }
             }
         }
         }
-        exceed(absStartX, textLine, words)
+        // 行末处理:传递 margin-right 给 exceed
+        var extraRightMargin = 0f
+        if (inNineSlice) {
+            val (_, marginRight) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+            extraRightMargin = marginRight
+        }
+        exceed(absStartX, textLine, words, extraRightMargin)
+    }
+
+    /**
+     * 计算九宫格左右 margin(即边4/6的真实渲染宽度),与渲染层 drawNineSliceCenter 保持一致
+     */
+    private fun calcNineSliceMargin(
+        bgImage: String,
+        npLeft: Float,
+        npRight: Float,
+    ): Pair<Float, Float> {
+        val dims = bitmapDimsCache.getOrPut(bgImage) {
+            val opts = BitmapFactory.Options().apply { inJustDecodeBounds = true }
+            BitmapFactory.decodeFile(bgImage, opts)
+            if (opts.outWidth > 0 && opts.outHeight > 0) {
+                opts.outWidth to opts.outHeight
+            } else null
+        } ?: return 0f to 0f
+
+        val (bw, _) = dims
+        val leftPx = (bw * npLeft).toInt().coerceAtLeast(0)
+        val rightPx = (bw * (1f - npRight)).toInt().coerceAtMost(bw)
+
+        val marginLeft = leftPx.toFloat()
+        val marginRight = (bw - rightPx).toFloat()
+        return marginLeft to marginRight
     }
     }
 
 
     /**
     /**
@@ -1200,34 +1301,72 @@ class TextChapterLayout(
         srcList: LinkedList<String>?,
         srcList: LinkedList<String>?,
         clickList: LinkedList<String?>?,
         clickList: LinkedList<String?>?,
         charStyles: Array<CharStyle?>?,
         charStyles: Array<CharStyle?>?,
-        lineStart: Int
+        lineStart: Int,
+        textPaint: android.graphics.Paint,
     ) {
     ) {
         val indentLength = paragraphIndent.length
         val indentLength = paragraphIndent.length
         var x = startX
         var x = startX
         textLine.startX = absStartX + startX
         textLine.startX = absStartX + startX
+
+        // 检查是否从前一行延续九宫格段落
+        val continuingFromPrevLine = lineStart > 0 && charStyles?.getOrNull(lineStart - 1)?.bgImageFit == 3
+
+        var inNineSlice = false
+        var nsBgImage = ""
+        var nsNpLeft = 0.1f
+        var nsNpRight = 0.1f
+
+        // 跨行延续:在新行开头添加 margin-left(边4宽度)
+        if (continuingFromPrevLine) {
+            val prevStyle = charStyles.getOrNull(lineStart - 1)
+            if (prevStyle != null) {
+                nsBgImage = prevStyle.bgImage
+                nsNpLeft = prevStyle.npLeft
+                nsNpRight = prevStyle.npRight
+                val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                x += marginLeft
+                inNineSlice = true
+            }
+        }
+
         for (index in words.indices) {
         for (index in words.indices) {
             val char = words[index]
             val char = words[index]
             val cw = textWidths[index]
             val cw = textWidths[index]
+            val style = charStyles?.getOrNull(lineStart + index)
+            val isNineSlice = style?.bgImageFit == 3
+
+            if (isNineSlice && !inNineSlice) {
+                // 进入九宫格段落:添加 margin-left(边4宽度)
+                nsBgImage = style.bgImage
+                nsNpLeft = style.npLeft
+                nsNpRight = style.npRight
+                val (marginLeft, _) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                x += marginLeft
+                inNineSlice = true
+            } else if (!isNineSlice && inNineSlice) {
+                // 离开九宫格段落:添加 margin-right(边6宽度)
+                val (_, marginRight) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+                x += marginRight
+                inNineSlice = false
+            }
+
             val x1 = x + cw
             val x1 = x + cw
             addCharToLine(
             addCharToLine(
-                book,
-                absStartX,
-                textLine,
-                char,
-                x,
-                x1,
-                index + 1 == words.size,
-                srcList,
-                clickList,
-                charStyles,
-                lineStart + index
+                book, absStartX, textLine, char, x, x1,
+                index + 1 == words.size, srcList, clickList, charStyles, lineStart + index,
             )
             )
             x = x1
             x = x1
             if (hasIndent && index == indentLength - 1) {
             if (hasIndent && index == indentLength - 1) {
                 textLine.indentWidth = x
                 textLine.indentWidth = x
             }
             }
         }
         }
-        exceed(absStartX, textLine, words)
+        // 行末处理:计算 margin-right 并传递给 exceed,确保右侧边框有足够空间
+        var extraRightMargin = 0f
+        if (inNineSlice) {
+            val (_, marginRight) = calcNineSliceMargin(nsBgImage, nsNpLeft, nsNpRight)
+            extraRightMargin = marginRight
+        }
+        exceed(absStartX, textLine, words, extraRightMargin)
     }
     }
 
 
     /**
     /**
@@ -1275,7 +1414,13 @@ class TextChapterLayout(
                     bgImage = style?.bgImage ?: "",
                     bgImage = style?.bgImage ?: "",
                     bgImageFit = style?.bgImageFit ?: 0,
                     bgImageFit = style?.bgImageFit ?: 0,
                     bgImageScale = style?.bgImageScale ?: 1f,
                     bgImageScale = style?.bgImageScale ?: 1f,
-                    fontPath = style?.fontPath ?: ""
+                    fontPath = style?.fontPath ?: "",
+                    fontWeight = style?.fontWeight ?: 400,
+                    isItalic = style?.isItalic ?: false,
+                    npLeft = style?.npLeft ?: 0.1f,
+                    npRight = style?.npRight ?: 0.1f,
+                    npTop = style?.npTop ?: 0.1f,
+                    npBottom = style?.npBottom ?: 0.1f,
                 )
                 )
             }
             }
         }
         }
@@ -1285,7 +1430,12 @@ class TextChapterLayout(
     /**
     /**
      * 超出边界处理
      * 超出边界处理
      */
      */
-    private fun exceed(absStartX: Int, textLine: TextLine, words: List<String>) {
+    private fun exceed(
+        absStartX: Int,
+        textLine: TextLine,
+        words: List<String>,
+        extraRightMargin: Float = 0f,
+    ) {
         var size = words.size
         var size = words.size
         if (size < 2) return
         if (size < 2) return
         val visibleEnd = absStartX + visibleWidth
         val visibleEnd = absStartX + visibleWidth
@@ -1298,10 +1448,10 @@ class TextChapterLayout(
         } else {
         } else {
             columns.last()
             columns.last()
         }
         }
-        val endX = endColumn.end.roundToInt()
-        if (endX > visibleEnd) {
+        val effectiveEndX = endColumn.end.roundToInt() + extraRightMargin.roundToInt()
+        if (effectiveEndX > visibleEnd) {
             textLine.exceed = true
             textLine.exceed = true
-            val cc = (endX - visibleEnd) / size
+            val cc = (effectiveEndX - visibleEnd) / size
             for (i in 0..<size) {
             for (i in 0..<size) {
                 textLine.getColumnReverseAt(i, offset).let {
                 textLine.getColumnReverseAt(i, offset).let {
                     val py = cc * (size - i)
                     val py = cc * (size - i)
@@ -1389,14 +1539,21 @@ class TextChapterLayout(
         val measurePaint = TextPaint(textPaint)
         val measurePaint = TextPaint(textPaint)
         var i = 0
         var i = 0
         while (i < text.length) {
         while (i < text.length) {
-            val fontPath = charStyles[i]?.fontPath.orEmpty()
+            val style = charStyles[i]
+            val fontPath = style?.fontPath.orEmpty()
             if (fontPath.isEmpty()) { i++; continue }
             if (fontPath.isEmpty()) { i++; continue }
-            // 找连续使用同一字体的区间
+            val fontWeight = style?.fontWeight ?: 400
+            val isItalic = style?.isItalic ?: false
+            // 找连续使用同一字体且同一字重/斜体的区间
             val segStart = i
             val segStart = i
             i++
             i++
-            while (i < text.length && charStyles[i]?.fontPath.orEmpty() == fontPath) i++
+            while (i < text.length) {
+                val s = charStyles[i]
+                if (s?.fontPath.orEmpty() != fontPath || (s?.fontWeight ?: 400) != fontWeight || (s?.isItalic ?: false) != isItalic) break
+                i++
+            }
             val segEnd = i
             val segEnd = i
-            val typeface = TextColumn.getTypeface(fontPath) ?: continue
+            val typeface = TextColumn.getTypeface(fontPath, fontWeight, isItalic) ?: continue
             measurePaint.typeface = typeface
             measurePaint.typeface = typeface
             val segLen = segEnd - segStart
             val segLen = segEnd - segStart
             val segWidths = FloatArray(segLen)
             val segWidths = FloatArray(segLen)
@@ -1527,7 +1684,13 @@ class TextChapterLayout(
             bgImage = bgImage.orEmpty(),
             bgImage = bgImage.orEmpty(),
             bgImageFit = bgImageFit,
             bgImageFit = bgImageFit,
             bgImageScale = bgImageScale,
             bgImageScale = bgImageScale,
-            fontPath = fontPath.orEmpty()
+            fontPath = fontPath.orEmpty(),
+            fontWeight = fontWeight,
+            isItalic = isItalic,
+            npLeft = npLeft,
+            npRight = npRight,
+            npTop = npTop,
+            npBottom = npBottom,
         )
         )
     }
     }
 
 

+ 207 - 15
app/src/main/java/io/legado/app/ui/book/read/sheet/HighlightRuleEditSheet.kt

@@ -1,15 +1,19 @@
 package io.legado.app.ui.book.read.sheet
 package io.legado.app.ui.book.read.sheet
 
 
 import android.content.Intent
 import android.content.Intent
+import android.graphics.BitmapFactory
 import android.net.Uri
 import android.net.Uri
 import android.provider.OpenableColumns
 import android.provider.OpenableColumns
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.compose.rememberLauncherForActivityResult
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.animation.AnimatedVisibility
 import androidx.compose.foundation.Canvas
 import androidx.compose.foundation.Canvas
+import androidx.compose.foundation.background
 import androidx.compose.foundation.layout.Arrangement
 import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Column
 import androidx.compose.foundation.layout.Spacer
 import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.fillMaxWidth
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.height
 import androidx.compose.foundation.layout.padding
 import androidx.compose.foundation.layout.padding
@@ -32,6 +36,7 @@ import androidx.compose.ui.Modifier
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.geometry.Offset
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.Color
 import androidx.compose.ui.graphics.StrokeCap
 import androidx.compose.ui.graphics.StrokeCap
+import androidx.compose.ui.graphics.asImageBitmap
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.graphics.drawscope.Stroke
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.platform.LocalContext
 import androidx.compose.ui.res.stringArrayResource
 import androidx.compose.ui.res.stringArrayResource
@@ -124,6 +129,17 @@ fun HighlightRuleEditSheet(
     var bgImageScale by remember(show, rule) { mutableFloatStateOf(initial.bgImageScale) }
     var bgImageScale by remember(show, rule) { mutableFloatStateOf(initial.bgImageScale) }
     var hasBgImage by remember(show, rule) { mutableStateOf(initial.bgImage?.isNotBlank() == true) }
     var hasBgImage by remember(show, rule) { mutableStateOf(initial.bgImage?.isNotBlank() == true) }
 
 
+    // Font weight state
+    var fontWeight by remember(show, rule) { mutableIntStateOf(initial.fontWeight) }
+    var isItalic by remember(show, rule) { mutableStateOf(initial.isItalic) }
+
+    // Nine-slice state
+    var npLeft by remember(show, rule) { mutableFloatStateOf(initial.npLeft) }
+    var npRight by remember(show, rule) { mutableFloatStateOf(initial.npRight) }
+    var npTop by remember(show, rule) { mutableFloatStateOf(initial.npTop) }
+    var npBottom by remember(show, rule) { mutableFloatStateOf(initial.npBottom) }
+    var showNinePatchEditor by remember(show, rule) { mutableStateOf(false) }
+
     // Config binding state — empty set = global (applies to all configs)
     // Config binding state — empty set = global (applies to all configs)
     var configNames by remember(show, rule) {
     var configNames by remember(show, rule) {
         mutableStateOf(initial.configName.orEmpty().configNames().toSet())
         mutableStateOf(initial.configName.orEmpty().configNames().toSet())
@@ -142,9 +158,9 @@ fun HighlightRuleEditSheet(
     // Validation
     // Validation
     var patternError by remember(show, rule) { mutableStateOf<String?>(null) }
     var patternError by remember(show, rule) { mutableStateOf<String?>(null) }
 
 
-    // System photo picker, with the contract's built-in fallback on older devices.
+    // File picker for background images (uses OpenDocument to avoid MediaStore transcoding)
     val imagePicker = rememberLauncherForActivityResult(
     val imagePicker = rememberLauncherForActivityResult(
-        ActivityResultContracts.GetContent()
+        ActivityResultContracts.OpenDocument()
     ) { uri: Uri? ->
     ) { uri: Uri? ->
         if (uri != null) {
         if (uri != null) {
             coroutineScope.launch {
             coroutineScope.launch {
@@ -228,6 +244,12 @@ fun HighlightRuleEditSheet(
                             bgImageScale = bgImageScale,
                             bgImageScale = bgImageScale,
                             configName = if (configNames.isEmpty()) null else configNames.toList().toJsonArray(),
                             configName = if (configNames.isEmpty()) null else configNames.toList().toJsonArray(),
                             fontPath = if (hasFont) fontPath.ifBlank { null } else null,
                             fontPath = if (hasFont) fontPath.ifBlank { null } else null,
+                            fontWeight = fontWeight,
+                            isItalic = isItalic,
+                            npLeft = npLeft,
+                            npRight = npRight,
+                            npTop = npTop,
+                            npBottom = npBottom,
                         )
                         )
                     )
                     )
                 },
                 },
@@ -245,6 +267,16 @@ fun HighlightRuleEditSheet(
             // === Section 1: Rule Info ===
             // === Section 1: Rule Info ===
             SectionTitle(stringResource(R.string.rule_info))
             SectionTitle(stringResource(R.string.rule_info))
 
 
+            AppTextField(
+                value = name,
+                onValueChange = { name = it },
+                label = stringResource(R.string.rule_name),
+                singleLine = true,
+                modifier = Modifier.fillMaxWidth(),
+            )
+
+            Spacer(Modifier.height(8.dp))
+
             AppTextField(
             AppTextField(
                 value = pattern,
                 value = pattern,
                 onValueChange = {
                 onValueChange = {
@@ -262,16 +294,6 @@ fun HighlightRuleEditSheet(
 
 
             Spacer(Modifier.height(8.dp))
             Spacer(Modifier.height(8.dp))
 
 
-            AppTextField(
-                value = name,
-                onValueChange = { name = it },
-                label = stringResource(R.string.rule_name),
-                singleLine = true,
-                modifier = Modifier.fillMaxWidth(),
-            )
-
-            Spacer(Modifier.height(8.dp))
-
             val scopeEntries = arrayOf(
             val scopeEntries = arrayOf(
                 stringResource(R.string.target_all),
                 stringResource(R.string.target_all),
                 stringResource(R.string.target_title),
                 stringResource(R.string.target_title),
@@ -313,6 +335,23 @@ fun HighlightRuleEditSheet(
                 )
                 )
             }
             }
 
 
+            // Font weight — three options: Regular(400), Bold(700), Light(300)
+            val weightEntries = stringArrayResource(R.array.text_font_weight)
+            TinyDropdownSettingItem(
+                title = stringResource(R.string.font_weight_text),
+                selectedValue = fontWeight.toString(),
+                displayEntries = weightEntries,
+                entryValues = arrayOf("400", "700", "300"),
+                onValueChange = { fontWeight = it.toIntOrNull() ?: 400 },
+            )
+
+            // Italic
+            TinySwitchSettingItem(
+                title = stringResource(R.string.read_config_italic),
+                checked = isItalic,
+                onCheckedChange = { isItalic = it },
+            )
+
             // Underline
             // Underline
             TinySwitchSettingItem(
             TinySwitchSettingItem(
                 title = stringResource(R.string.underline_style),
                 title = stringResource(R.string.underline_style),
@@ -402,7 +441,7 @@ fun HighlightRuleEditSheet(
                     title = stringResource(R.string.highlight_bg_image),
                     title = stringResource(R.string.highlight_bg_image),
                     description = bgImage.ifBlank { null }?.let { File(it).name },
                     description = bgImage.ifBlank { null }?.let { File(it).name },
                     onClick = {
                     onClick = {
-                        imagePicker.launch("image/*")
+                        imagePicker.launch(arrayOf("image/*"))
                     },
                     },
                 )
                 )
             }
             }
@@ -412,14 +451,21 @@ fun HighlightRuleEditSheet(
                         stringResource(R.string.bg_fit_tile),
                         stringResource(R.string.bg_fit_tile),
                         stringResource(R.string.bg_fit_stretch),
                         stringResource(R.string.bg_fit_stretch),
                         stringResource(R.string.bg_fit_crop),
                         stringResource(R.string.bg_fit_crop),
+                        stringResource(R.string.bg_fit_nine_patch),
                     )
                     )
-                    val fitValues = arrayOf("0", "1", "2")
+                    val fitValues = arrayOf("0", "1", "2", "3")
                     TinyDropdownSettingItem(
                     TinyDropdownSettingItem(
                         title = stringResource(R.string.bg_image_fit),
                         title = stringResource(R.string.bg_image_fit),
                         selectedValue = bgImageFit.toString(),
                         selectedValue = bgImageFit.toString(),
                         displayEntries = fitEntries,
                         displayEntries = fitEntries,
                         entryValues = fitValues,
                         entryValues = fitValues,
-                        onValueChange = { bgImageFit = it.toIntOrNull() ?: 0 },
+                        onValueChange = {
+                            val newFit = it.toIntOrNull() ?: 0
+                            bgImageFit = newFit
+                            if (newFit == 3) {
+                                showNinePatchEditor = true
+                            }
+                        },
                     )
                     )
 
 
                     TinySliderSettingItem(
                     TinySliderSettingItem(
@@ -554,6 +600,24 @@ fun HighlightRuleEditSheet(
         },
         },
     )
     )
 
 
+    // Nine-patch editor
+    NinePatchEditorDialog(
+        show = showNinePatchEditor,
+        imagePath = bgImage,
+        initialLeft = npLeft,
+        initialRight = npRight,
+        initialTop = npTop,
+        initialBottom = npBottom,
+        onDismissRequest = { showNinePatchEditor = false },
+        onSave = { left, right, top, bottom ->
+            npLeft = left
+            npRight = right
+            npTop = top
+            npBottom = bottom
+            showNinePatchEditor = false
+        },
+    )
+
     // Font selector
     // Font selector
     val readSettingsRepository: ReadSettingsRepository = org.koin.compose.koinInject()
     val readSettingsRepository: ReadSettingsRepository = org.koin.compose.koinInject()
     val fontSelectScope = rememberCoroutineScope()
     val fontSelectScope = rememberCoroutineScope()
@@ -695,3 +759,131 @@ private fun HighlightRulePreview(
         }
         }
     }
     }
 }
 }
+
+@Composable
+private fun NinePatchEditorDialog(
+    show: Boolean,
+    imagePath: String,
+    initialLeft: Float,
+    initialRight: Float,
+    initialTop: Float,
+    initialBottom: Float,
+    onDismissRequest: () -> Unit,
+    onSave: (left: Float, right: Float, top: Float, bottom: Float) -> Unit,
+) {
+    var left by remember(show, imagePath) { mutableFloatStateOf(initialLeft) }
+    var right by remember(show, imagePath) { mutableFloatStateOf(initialRight) }
+    var top by remember(show, imagePath) { mutableFloatStateOf(initialTop) }
+    var bottom by remember(show, imagePath) { mutableFloatStateOf(initialBottom) }
+
+    val bitmap = remember(imagePath) {
+        runCatching {
+            val file = File(imagePath)
+            if (file.exists()) {
+                BitmapFactory.decodeFile(imagePath)
+            } else null
+        }.getOrNull()
+    }
+
+    AppModalBottomSheet(
+        show = show,
+        onDismissRequest = onDismissRequest,
+        title = stringResource(R.string.nine_patch_editor),
+        endAction = {
+            MediumTonalButton(
+                onClick = { onSave(left, right, top, bottom) },
+                icon = Icons.Default.Done,
+                contentDescription = stringResource(R.string.save),
+            )
+        },
+    ) {
+        Column(
+            modifier = Modifier
+                .fillMaxWidth()
+                .padding(bottom = 16.dp)
+                .verticalScroll(rememberScrollState()),
+        ) {
+            // Image preview with split lines — use single Canvas to avoid coordinate mismatch
+            Box(
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .height(250.dp)
+                    .padding(8.dp)
+                    .background(MaterialTheme.colorScheme.surfaceContainerLow),
+            ) {
+                if (bitmap != null) {
+                    Canvas(modifier = Modifier.fillMaxSize()) {
+                        val canvasWidth = size.width
+                        val canvasHeight = size.height
+                        val bw = bitmap.width.toFloat()
+                        val bh = bitmap.height.toFloat()
+                        val imageAspect = bw / bh
+                        val canvasAspect = canvasWidth / canvasHeight
+                        val (imageW, imageH, offsetX, offsetY) = if (imageAspect > canvasAspect) {
+                            val w = canvasWidth
+                            val h = canvasWidth / imageAspect
+                            listOf(w, h, 0f, (canvasHeight - h) / 2f)
+                        } else {
+                            val h = canvasHeight
+                            val w = canvasHeight * imageAspect
+                            listOf(w, h, (canvasWidth - w) / 2f, 0f)
+                        }
+
+                        // Draw bitmap
+                        drawImage(
+                            image = bitmap.asImageBitmap(),
+                            dstOffset = androidx.compose.ui.unit.IntOffset(offsetX.toInt(), offsetY.toInt()),
+                            dstSize = androidx.compose.ui.unit.IntSize(imageW.toInt(), imageH.toInt()),
+                        )
+
+                        val lineColor = Color.Red
+                        val lineWidth = 2.dp.toPx()
+
+                        // Left line
+                        val lx = offsetX + imageW * left
+                        drawLine(lineColor, Offset(lx, offsetY), Offset(lx, offsetY + imageH), lineWidth)
+                        // Right line
+                        val rx = offsetX + imageW * (1f - right)
+                        drawLine(lineColor, Offset(rx, offsetY), Offset(rx, offsetY + imageH), lineWidth)
+                        // Top line
+                        val ty = offsetY + imageH * top
+                        drawLine(lineColor, Offset(offsetX, ty), Offset(offsetX + imageW, ty), lineWidth)
+                        // Bottom line
+                        val by = offsetY + imageH * (1f - bottom)
+                        drawLine(lineColor, Offset(offsetX, by), Offset(offsetX + imageW, by), lineWidth)
+                    }
+                }
+            }
+
+            // Sliders
+            TinySliderSettingItem(
+                title = stringResource(R.string.nine_patch_split_left),
+                value = left,
+                valueRange = 0f..0.5f,
+                description = String.format("%.0f%%", left * 100),
+                onValueChange = { left = (it * 100).toInt() / 100f },
+            )
+            TinySliderSettingItem(
+                title = stringResource(R.string.nine_patch_split_right),
+                value = right,
+                valueRange = 0f..0.5f,
+                description = String.format("%.0f%%", right * 100),
+                onValueChange = { right = (it * 100).toInt() / 100f },
+            )
+            TinySliderSettingItem(
+                title = stringResource(R.string.nine_patch_split_top),
+                value = top,
+                valueRange = 0f..0.5f,
+                description = String.format("%.0f%%", top * 100),
+                onValueChange = { top = (it * 100).toInt() / 100f },
+            )
+            TinySliderSettingItem(
+                title = stringResource(R.string.nine_patch_split_bottom),
+                value = bottom,
+                valueRange = 0f..0.5f,
+                description = String.format("%.0f%%", bottom * 100),
+                onValueChange = { bottom = (it * 100).toInt() / 100f },
+            )
+        }
+    }
+}

+ 7 - 0
app/src/main/res/values-zh-rCN/strings.xml

@@ -784,6 +784,12 @@
     <string name="bg_fit_tile">平铺</string>
     <string name="bg_fit_tile">平铺</string>
     <string name="bg_fit_stretch">拉伸填充</string>
     <string name="bg_fit_stretch">拉伸填充</string>
     <string name="bg_fit_crop">居中裁剪</string>
     <string name="bg_fit_crop">居中裁剪</string>
+    <string name="bg_fit_nine_patch">九宫格</string>
+    <string name="nine_patch_editor">九宫格切图</string>
+    <string name="nine_patch_split_left">左切分线</string>
+    <string name="nine_patch_split_right">右切分线</string>
+    <string name="nine_patch_split_top">上切分线</string>
+    <string name="nine_patch_split_bottom">下切分线</string>
     <string name="preview_effect">预览效果</string>
     <string name="preview_effect">预览效果</string>
     <string name="sample_text">示例文本</string>
     <string name="sample_text">示例文本</string>
     <string name="sure_delete">确定要删除吗?</string>
     <string name="sure_delete">确定要删除吗?</string>
@@ -1404,6 +1410,7 @@
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
+    <string name="font_weight">字重</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行,拒绝后不再弹出。如有需要可至高级设置中再次申请。</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行,拒绝后不再弹出。如有需要可至高级设置中再次申请。</string>

+ 7 - 0
app/src/main/res/values-zh-rHK/strings.xml

@@ -1228,6 +1228,7 @@
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
+    <string name="font_weight">字重</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
@@ -1586,6 +1587,12 @@
     <string name="backup_error">備份失敗</string>
     <string name="backup_error">備份失敗</string>
     <string name="be_swiped">頁面仍可滑動</string>
     <string name="be_swiped">頁面仍可滑動</string>
     <string name="bg_fit_crop">中央裁剪</string>
     <string name="bg_fit_crop">中央裁剪</string>
+    <string name="bg_fit_nine_patch">九宮格</string>
+    <string name="nine_patch_editor">九宮格切圖</string>
+    <string name="nine_patch_split_left">左切分線</string>
+    <string name="nine_patch_split_right">右切分線</string>
+    <string name="nine_patch_split_top">上切分線</string>
+    <string name="nine_patch_split_bottom">下切分線</string>
     <string name="bg_fit_stretch">拉伸</string>
     <string name="bg_fit_stretch">拉伸</string>
     <string name="bg_fit_tile">平鋪</string>
     <string name="bg_fit_tile">平鋪</string>
     <string name="bg_image_fit">背景適配</string>
     <string name="bg_image_fit">背景適配</string>

+ 7 - 0
app/src/main/res/values-zh-rTW/strings.xml

@@ -1232,6 +1232,7 @@
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_s">当应用发生OOM崩溃时保存堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="record_heap_dump_t">记录堆转储</string>
     <string name="font_weight_text">字重</string>
     <string name="font_weight_text">字重</string>
+    <string name="font_weight">字重</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="keep_swipe_tip">继续滑动以加载下一章…</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="enable_optimize_render">启用绘制优化</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
     <string name="ignore_battery_permission_rationale">阅读需要请求后台权限以保持服务正常运行</string>
@@ -1521,6 +1522,12 @@
     <string name="bg_fit_tile">並排</string>
     <string name="bg_fit_tile">並排</string>
     <string name="bg_fit_stretch">延伸</string>
     <string name="bg_fit_stretch">延伸</string>
     <string name="bg_fit_crop">置中裁切</string>
     <string name="bg_fit_crop">置中裁切</string>
+    <string name="bg_fit_nine_patch">九宮格</string>
+    <string name="nine_patch_editor">九宮格切圖</string>
+    <string name="nine_patch_split_left">左切分線</string>
+    <string name="nine_patch_split_right">右切分線</string>
+    <string name="nine_patch_split_top">上切分線</string>
+    <string name="nine_patch_split_bottom">下切分線</string>
     <string name="preview_effect">預覽</string>
     <string name="preview_effect">預覽</string>
     <string name="sample_text">範例文字</string>
     <string name="sample_text">範例文字</string>
     <string name="sure_delete">確定要刪除嗎?</string>
     <string name="sure_delete">確定要刪除嗎?</string>

+ 7 - 0
app/src/main/res/values/strings.xml

@@ -819,6 +819,12 @@
     <string name="bg_fit_tile">Tile</string>
     <string name="bg_fit_tile">Tile</string>
     <string name="bg_fit_stretch">Stretch</string>
     <string name="bg_fit_stretch">Stretch</string>
     <string name="bg_fit_crop">Center Crop</string>
     <string name="bg_fit_crop">Center Crop</string>
+    <string name="bg_fit_nine_patch">9-Slice</string>
+    <string name="nine_patch_editor">9-Slice Editor</string>
+    <string name="nine_patch_split_left">Left Split</string>
+    <string name="nine_patch_split_right">Right Split</string>
+    <string name="nine_patch_split_top">Top Split</string>
+    <string name="nine_patch_split_bottom">Bottom Split</string>
     <string name="preview_effect">Preview</string>
     <string name="preview_effect">Preview</string>
     <string name="sample_text">Sample Text</string>
     <string name="sample_text">Sample Text</string>
     <string name="sure_delete">Are you sure you want to delete?</string>
     <string name="sure_delete">Are you sure you want to delete?</string>
@@ -1469,6 +1475,7 @@
     <string name="record_heap_dump_s">Saving a heap dump when the application crashes by OOM</string>
     <string name="record_heap_dump_s">Saving a heap dump when the application crashes by OOM</string>
     <string name="record_heap_dump_t">Capture heap dump</string>
     <string name="record_heap_dump_t">Capture heap dump</string>
     <string name="font_weight_text">N/B/L</string>
     <string name="font_weight_text">N/B/L</string>
+    <string name="font_weight">Font Weight</string>
     <string name="keep_swipe_tip">Keep swiping to load the next chapter…</string>
     <string name="keep_swipe_tip">Keep swiping to load the next chapter…</string>
     <string name="enable_optimize_render">Enable Render Optimization</string>
     <string name="enable_optimize_render">Enable Render Optimization</string>
     <string name="ignore_battery_permission_rationale">Reading requires requesting background permission to keep the service running properly</string>
     <string name="ignore_battery_permission_rationale">Reading requires requesting background permission to keep the service running properly</string>