فهرست منبع

Merge branch 'master' of https://github.com/Xie-Jason/legado

XieJC 4 سال پیش
والد
کامیت
dfeae9e633
35فایلهای تغییر یافته به همراه2313 افزوده شده و 145 حذف شده
  1. 1692 0
      app/schemas/io.legado.app.data.AppDatabase/53.json
  2. 2 2
      app/src/main/assets/updateLog.md
  3. 1 0
      app/src/main/java/io/legado/app/constant/PreferKey.kt
  4. 3 2
      app/src/main/java/io/legado/app/data/AppDatabase.kt
  5. 3 0
      app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt
  6. 15 0
      app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt
  7. 21 2
      app/src/main/java/io/legado/app/data/entities/BookSource.kt
  8. 20 0
      app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt
  9. 50 18
      app/src/main/java/io/legado/app/help/JsExtensions.kt
  10. 11 1
      app/src/main/java/io/legado/app/help/SourceAnalyzer.kt
  11. 6 0
      app/src/main/java/io/legado/app/help/config/AppConfig.kt
  12. 1 1
      app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt
  13. 7 0
      app/src/main/java/io/legado/app/model/webBook/WebBook.kt
  14. 6 0
      app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt
  15. 143 40
      app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt
  16. 58 3
      app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt
  17. 29 16
      app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt
  18. 5 1
      app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt
  19. 3 0
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt
  20. 5 2
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextColumn.kt
  21. 7 3
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextLine.kt
  22. 9 1
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt
  23. 3 0
      app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt
  24. 35 18
      app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt
  25. 52 13
      app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt
  26. 24 4
      app/src/main/res/layout/activity_book_source_edit.xml
  27. 7 0
      app/src/main/res/menu/book_read.xml
  28. 11 0
      app/src/main/res/values-es-rES/strings.xml
  29. 11 0
      app/src/main/res/values-ja-rJP/strings.xml
  30. 11 0
      app/src/main/res/values-pt-rBR/strings.xml
  31. 11 0
      app/src/main/res/values-zh-rHK/strings.xml
  32. 11 0
      app/src/main/res/values-zh-rTW/strings.xml
  33. 11 0
      app/src/main/res/values-zh/strings.xml
  34. 18 18
      app/src/main/res/values/non_translat.xml
  35. 11 0
      app/src/main/res/values/strings.xml

+ 1692 - 0
app/schemas/io.legado.app.data.AppDatabase/53.json

@@ -0,0 +1,1692 @@
+{
+  "formatVersion": 1,
+  "database": {
+    "version": 53,
+    "identityHash": "c55dac79aa9cc3dcdb72e91da4282005",
+    "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 '', `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, `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, 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": "''"
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customTag",
+            "columnName": "customTag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "coverUrl",
+            "columnName": "coverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customCoverUrl",
+            "columnName": "customCoverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "intro",
+            "columnName": "intro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customIntro",
+            "columnName": "customIntro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "charset",
+            "columnName": "charset",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "readConfig",
+            "columnName": "readConfig",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_books_name_author",
+            "unique": true,
+            "columnNames": [
+              "name",
+              "author"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_books_name_author` ON `${TABLE_NAME}` (`name`, `author`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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, `show` INTEGER NOT NULL, 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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "show",
+            "columnName": "show",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "groupId"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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, `enabled` INTEGER NOT NULL, `enabledExplore` INTEGER NOT NULL, `enabledReview` INTEGER, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `bookSourceComment` TEXT, `variableComment` TEXT, `lastUpdateTime` INTEGER NOT NULL, `respondTime` INTEGER NOT NULL, `weight` INTEGER NOT NULL, `exploreUrl` TEXT, `ruleExplore` TEXT, `searchUrl` TEXT, `ruleSearch` TEXT, `ruleBookInfo` TEXT, `ruleToc` TEXT, `ruleContent` TEXT, `ruleReview` 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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "bookSourceType",
+            "columnName": "bookSourceType",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrlPattern",
+            "columnName": "bookUrlPattern",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "customOrder",
+            "columnName": "customOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabledExplore",
+            "columnName": "enabledExplore",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enabledReview",
+            "columnName": "enabledReview",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "bookSourceComment",
+            "columnName": "bookSourceComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "variableComment",
+            "columnName": "variableComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleExplore",
+            "columnName": "ruleExplore",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "searchUrl",
+            "columnName": "searchUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleSearch",
+            "columnName": "ruleSearch",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleBookInfo",
+            "columnName": "ruleBookInfo",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleToc",
+            "columnName": "ruleToc",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleContent",
+            "columnName": "ruleContent",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleReview",
+            "columnName": "ruleReview",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookSourceUrl"
+          ],
+          "autoGenerate": false
+        },
+        "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`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "chapters",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`url` TEXT NOT NULL, `title` TEXT NOT NULL, `isVolume` INTEGER NOT NULL, `baseUrl` TEXT NOT NULL, `bookUrl` TEXT NOT NULL, `index` INTEGER NOT NULL, `isVip` INTEGER NOT NULL, `isPay` INTEGER NOT NULL, `resourceUrl` TEXT, `tag` TEXT, `start` INTEGER, `end` INTEGER, `startFragmentId` TEXT, `endFragmentId` TEXT, `variable` TEXT, PRIMARY KEY(`url`, `bookUrl`), FOREIGN KEY(`bookUrl`) REFERENCES `books`(`bookUrl`) ON UPDATE NO ACTION ON DELETE CASCADE )",
+        "fields": [
+          {
+            "fieldPath": "url",
+            "columnName": "url",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "title",
+            "columnName": "title",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isVolume",
+            "columnName": "isVolume",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "baseUrl",
+            "columnName": "baseUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookUrl",
+            "columnName": "bookUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "index",
+            "columnName": "index",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isVip",
+            "columnName": "isVip",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "isPay",
+            "columnName": "isPay",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "resourceUrl",
+            "columnName": "resourceUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tag",
+            "columnName": "tag",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "start",
+            "columnName": "start",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "end",
+            "columnName": "end",
+            "affinity": "INTEGER",
+            "notNull": false
+          },
+          {
+            "fieldPath": "startFragmentId",
+            "columnName": "startFragmentId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "endFragmentId",
+            "columnName": "endFragmentId",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "url",
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "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, `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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "pattern",
+            "columnName": "pattern",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "replacement",
+            "columnName": "replacement",
+            "affinity": "TEXT",
+            "notNull": true,
+            "defaultValue": "''"
+          },
+          {
+            "fieldPath": "scope",
+            "columnName": "scope",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "scopeTitle",
+            "columnName": "scopeTitle",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "scopeContent",
+            "columnName": "scopeContent",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "1"
+          },
+          {
+            "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": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": true
+        },
+        "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`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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, 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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "coverUrl",
+            "columnName": "coverUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "intro",
+            "columnName": "intro",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "wordCount",
+            "columnName": "wordCount",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "latestChapterTitle",
+            "columnName": "latestChapterTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "tocUrl",
+            "columnName": "tocUrl",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "time",
+            "columnName": "time",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "originOrder",
+            "columnName": "originOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "bookUrl"
+          ],
+          "autoGenerate": false
+        },
+        "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": {
+          "columnNames": [
+            "word"
+          ],
+          "autoGenerate": false
+        },
+        "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`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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": {
+          "columnNames": [
+            "url"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_cookies_url",
+            "unique": true,
+            "columnNames": [
+              "url"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_cookies_url` ON `${TABLE_NAME}` (`url`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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, `enabledCookieJar` INTEGER DEFAULT 0, `concurrentRate` TEXT, `header` TEXT, `loginUrl` TEXT, `loginUi` TEXT, `loginCheckJs` TEXT, `sortUrl` TEXT, `singleUrl` INTEGER NOT NULL, `articleStyle` INTEGER NOT NULL, `ruleArticles` TEXT, `ruleNextPage` TEXT, `ruleTitle` TEXT, `rulePubDate` TEXT, `ruleDescription` TEXT, `ruleImage` TEXT, `ruleLink` TEXT, `ruleContent` TEXT, `style` TEXT, `enableJs` INTEGER NOT NULL, `loadWithBaseUrl` INTEGER NOT NULL, `customOrder` INTEGER NOT NULL, 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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sourceComment",
+            "columnName": "sourceComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabled",
+            "columnName": "enabled",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "variableComment",
+            "columnName": "variableComment",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "sortUrl",
+            "columnName": "sortUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "singleUrl",
+            "columnName": "singleUrl",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "articleStyle",
+            "columnName": "articleStyle",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "ruleArticles",
+            "columnName": "ruleArticles",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleNextPage",
+            "columnName": "ruleNextPage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleTitle",
+            "columnName": "ruleTitle",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "rulePubDate",
+            "columnName": "rulePubDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleDescription",
+            "columnName": "ruleDescription",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleImage",
+            "columnName": "ruleImage",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleLink",
+            "columnName": "ruleLink",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "ruleContent",
+            "columnName": "ruleContent",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "style",
+            "columnName": "style",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enableJs",
+            "columnName": "enableJs",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "loadWithBaseUrl",
+            "columnName": "loadWithBaseUrl",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "customOrder",
+            "columnName": "customOrder",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "sourceUrl"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_rssSources_sourceUrl",
+            "unique": false,
+            "columnNames": [
+              "sourceUrl"
+            ],
+            "orders": [],
+            "createSql": "CREATE INDEX IF NOT EXISTS `index_rssSources_sourceUrl` ON `${TABLE_NAME}` (`sourceUrl`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "bookmarks",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`time` INTEGER NOT NULL, `bookName` TEXT NOT NULL, `bookAuthor` TEXT NOT NULL, `chapterIndex` INTEGER NOT NULL, `chapterPos` INTEGER NOT NULL, `chapterName` TEXT NOT NULL, `bookText` TEXT NOT NULL, `content` TEXT NOT NULL, PRIMARY KEY(`time`))",
+        "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
+          },
+          {
+            "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": {
+          "columnNames": [
+            "time"
+          ],
+          "autoGenerate": false
+        },
+        "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`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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, `read` INTEGER NOT NULL, `variable` TEXT, 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": "order",
+            "columnName": "order",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "link",
+            "columnName": "link",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "pubDate",
+            "columnName": "pubDate",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "image",
+            "columnName": "image",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "origin",
+            "link"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "rssReadRecords",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`record` TEXT NOT NULL, `read` INTEGER NOT NULL, PRIMARY KEY(`record`))",
+        "fields": [
+          {
+            "fieldPath": "record",
+            "columnName": "record",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "read",
+            "columnName": "read",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "record"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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, `variable` TEXT, 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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "description",
+            "columnName": "description",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "content",
+            "columnName": "content",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "image",
+            "columnName": "image",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "variable",
+            "columnName": "variable",
+            "affinity": "TEXT",
+            "notNull": false
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "origin",
+            "link"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "serialNumber",
+            "columnName": "serialNumber",
+            "affinity": "INTEGER",
+            "notNull": true
+          },
+          {
+            "fieldPath": "enable",
+            "columnName": "enable",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "tableName": "readRecord",
+        "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`deviceId` TEXT NOT NULL, `bookName` TEXT NOT NULL, `readTime` INTEGER NOT NULL DEFAULT 0, `lastRead` INTEGER NOT NULL DEFAULT 0, PRIMARY KEY(`deviceId`, `bookName`))",
+        "fields": [
+          {
+            "fieldPath": "deviceId",
+            "columnName": "deviceId",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "bookName",
+            "columnName": "bookName",
+            "affinity": "TEXT",
+            "notNull": true
+          },
+          {
+            "fieldPath": "readTime",
+            "columnName": "readTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "lastRead",
+            "columnName": "lastRead",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "deviceId",
+            "bookName"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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, `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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "concurrentRate",
+            "columnName": "concurrentRate",
+            "affinity": "TEXT",
+            "notNull": false,
+            "defaultValue": "'0'"
+          },
+          {
+            "fieldPath": "loginUrl",
+            "columnName": "loginUrl",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "loginUi",
+            "columnName": "loginUi",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "header",
+            "columnName": "header",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "enabledCookieJar",
+            "columnName": "enabledCookieJar",
+            "affinity": "INTEGER",
+            "notNull": false,
+            "defaultValue": "0"
+          },
+          {
+            "fieldPath": "loginCheckJs",
+            "columnName": "loginCheckJs",
+            "affinity": "TEXT",
+            "notNull": false
+          },
+          {
+            "fieldPath": "lastUpdateTime",
+            "columnName": "lastUpdateTime",
+            "affinity": "INTEGER",
+            "notNull": true,
+            "defaultValue": "0"
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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",
+            "notNull": false
+          },
+          {
+            "fieldPath": "deadline",
+            "columnName": "deadline",
+            "affinity": "INTEGER",
+            "notNull": true
+          }
+        ],
+        "primaryKey": {
+          "columnNames": [
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [
+          {
+            "name": "index_caches_key",
+            "unique": true,
+            "columnNames": [
+              "key"
+            ],
+            "orders": [],
+            "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_caches_key` ON `${TABLE_NAME}` (`key`)"
+          }
+        ],
+        "foreignKeys": []
+      },
+      {
+        "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": {
+          "columnNames": [
+            "id"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      },
+      {
+        "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": {
+          "columnNames": [
+            "type",
+            "key"
+          ],
+          "autoGenerate": false
+        },
+        "indices": [],
+        "foreignKeys": []
+      }
+    ],
+    "views": [],
+    "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, 'c55dac79aa9cc3dcdb72e91da4282005')"
+    ]
+  }
+}

+ 2 - 2
app/src/main/assets/updateLog.md

@@ -11,11 +11,11 @@
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 正文出现缺字漏字、内容缺失、排版错乱等情况,有可能是净化规则或简繁转换出现问题。
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 * 漫画源看书显示乱码,**阅读与其他软件的源并不通用**,请导入阅读的支持的漫画源!
 
 
-**2022/09/03**
+**2022/09/07**
 
 
 * 更新cronet: 105.0.5195.79
 * 更新cronet: 105.0.5195.79
-* 更新cronet: 105.0.5195.77
 * 更新web端书源编辑 by jgckM
 * 更新web端书源编辑 by jgckM
+* 修复仿真翻页点击时翻页动画异常,化仿真翻页点击翻页效果 by 821938089
 
 
 **2022/08/31**
 **2022/08/31**
 
 

+ 1 - 0
app/src/main/java/io/legado/app/constant/PreferKey.kt

@@ -32,6 +32,7 @@ object PreferKey {
     const val prevKeys = "prevKeyCodes"
     const val prevKeys = "prevKeyCodes"
     const val nextKeys = "nextKeyCodes"
     const val nextKeys = "nextKeyCodes"
     const val showDiscovery = "showDiscovery"
     const val showDiscovery = "showDiscovery"
+    const val enableReview = "enableReview"
     const val showRss = "showRss"
     const val showRss = "showRss"
     const val bookshelfLayout = "bookshelfLayout"
     const val bookshelfLayout = "bookshelfLayout"
     const val bookshelfSort = "bookshelfSort"
     const val bookshelfSort = "bookshelfSort"

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

@@ -20,7 +20,7 @@ val appDb by lazy {
 }
 }
 
 
 @Database(
 @Database(
-    version = 52,
+    version = 53,
     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,
@@ -36,7 +36,8 @@ val appDb by lazy {
         AutoMigration(from = 48, to = 49),
         AutoMigration(from = 48, to = 49),
         AutoMigration(from = 49, to = 50),
         AutoMigration(from = 49, to = 50),
         AutoMigration(from = 50, to = 51),
         AutoMigration(from = 50, to = 51),
-        AutoMigration(from = 51, to = 52)
+        AutoMigration(from = 51, to = 52),
+        AutoMigration(from = 52, to = 53)
     ]
     ]
 )
 )
 abstract class AppDatabase : RoomDatabase() {
 abstract class AppDatabase : RoomDatabase() {

+ 3 - 0
app/src/main/java/io/legado/app/data/dao/BookSourceDao.kt

@@ -50,6 +50,9 @@ interface BookSourceDao {
     @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
     @Query("select * from book_sources where enabledExplore = 1 and trim(exploreUrl) <> '' order by customOrder asc")
     fun flowExplore(): Flow<List<BookSource>>
     fun flowExplore(): Flow<List<BookSource>>
 
 
+//    @Query("select * from book_sources where enabledReview = 1 order by customOrder asc")
+//    fun flowReview(): Flow<List<BookSource>>
+
     @Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
     @Query("select * from book_sources where loginUrl is not null and loginUrl != ''")
     fun flowLogin(): Flow<List<BookSource>>
     fun flowLogin(): Flow<List<BookSource>>
 
 

+ 15 - 0
app/src/main/java/io/legado/app/data/entities/BookChapterReview.kt

@@ -0,0 +1,15 @@
+package io.legado.app.data.entities
+
+import android.os.Parcelable
+import androidx.room.ColumnInfo
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+class BookChapterReview(
+    @ColumnInfo(defaultValue = "0")
+    var bookId: Long = 0,
+    var chapterId: Long = 0,
+    var summaryUrl: String = "",
+): Parcelable {
+
+}

+ 21 - 2
app/src/main/java/io/legado/app/data/entities/BookSource.kt

@@ -37,7 +37,9 @@ data class BookSource(
     // 是否启用
     // 是否启用
     var enabled: Boolean = true,
     var enabled: Boolean = true,
     // 启用发现
     // 启用发现
-    var enabledExplore: Boolean = true,
+    var enabledExplore: Boolean = false,
+    // 启用段评
+    var enabledReview: Boolean? = false,
     // 启用okhttp CookieJAr 自动保存每次请求的cookie
     // 启用okhttp CookieJAr 自动保存每次请求的cookie
     @ColumnInfo(defaultValue = "0")
     @ColumnInfo(defaultValue = "0")
     override var enabledCookieJar: Boolean? = false,
     override var enabledCookieJar: Boolean? = false,
@@ -74,7 +76,9 @@ data class BookSource(
     // 目录页规则
     // 目录页规则
     var ruleToc: TocRule? = null,
     var ruleToc: TocRule? = null,
     // 正文页规则
     // 正文页规则
-    var ruleContent: ContentRule? = null
+    var ruleContent: ContentRule? = null,
+    // 段评规则
+    var ruleReview: ReviewRule? = null
 ) : Parcelable, BaseSource {
 ) : Parcelable, BaseSource {
 
 
     override fun getTag(): String {
     override fun getTag(): String {
@@ -170,6 +174,13 @@ data class BookSource(
         return rule
         return rule
     }
     }
 
 
+    fun getReviewRule(): ReviewRule {
+        ruleReview?.let { return it }
+        val rule = ReviewRule()
+        ruleReview = rule
+        return rule
+    }
+
     fun getDisPlayNameGroup(): String {
     fun getDisPlayNameGroup(): String {
         return if (bookSourceGroup.isNullOrBlank()) {
         return if (bookSourceGroup.isNullOrBlank()) {
             bookSourceName
             bookSourceName
@@ -303,5 +314,13 @@ data class BookSource(
         fun stringToContentRule(json: String?) =
         fun stringToContentRule(json: String?) =
             GSON.fromJsonObject<ContentRule>(json).getOrNull()
             GSON.fromJsonObject<ContentRule>(json).getOrNull()
 
 
+        @TypeConverter
+        fun stringToReviewRule(json: String?) =
+            GSON.fromJsonObject<ReviewRule>(json).getOrNull()
+
+        @TypeConverter
+        fun reviewRuleToString(reviewRule: ReviewRule?): String =
+            GSON.toJson(reviewRule)
+
     }
     }
 }
 }

+ 20 - 0
app/src/main/java/io/legado/app/data/entities/rule/ReviewRule.kt

@@ -0,0 +1,20 @@
+package io.legado.app.data.entities.rule
+
+import android.os.Parcelable
+import kotlinx.parcelize.Parcelize
+
+@Parcelize
+data class ReviewRule(
+    var reviewUrl: String? = null,          // 段评URL
+    var avatarRule: String? = null,         // 段评发布者头像
+    var contentRule: String? = null,        // 段评内容
+    var postTimeRule: String? = null,       // 段评发布时间
+    var reviewQuoteUrl: String? = null,     // 获取段评回复URL
+
+    // 这些功能将在以上功能完成以后实现
+    var voteUpUrl: String? = null,          // 点赞URL
+    var voteDownUrl: String? = null,        // 点踩URL
+    var postReviewUrl: String? = null,      // 发送回复URL
+    var postQuoteUrl: String? = null,       // 发送回复段评URL
+    var deleteUrl: String? = null,          // 删除段评URL
+): Parcelable

+ 50 - 18
app/src/main/java/io/legado/app/help/JsExtensions.kt

@@ -27,6 +27,7 @@ import splitties.init.appCtx
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayInputStream
 import java.io.ByteArrayOutputStream
 import java.io.ByteArrayOutputStream
 import java.io.File
 import java.io.File
+import java.io.FileOutputStream
 import java.net.URLEncoder
 import java.net.URLEncoder
 import java.nio.charset.Charset
 import java.nio.charset.Charset
 import java.text.SimpleDateFormat
 import java.text.SimpleDateFormat
@@ -125,7 +126,7 @@ interface JsExtensions {
                 html = html,
                 html = html,
                 javaScript = js,
                 javaScript = js,
                 headerMap = getSource()?.getHeaderMap(true),
                 headerMap = getSource()?.getHeaderMap(true),
-                tag =  getSource()?.getKey()
+                tag = getSource()?.getKey()
             ).getStrResponse().body
             ).getStrResponse().body
         }
         }
     }
     }
@@ -143,15 +144,18 @@ interface JsExtensions {
      * 使用内置浏览器打开链接,并等待网页结果
      * 使用内置浏览器打开链接,并等待网页结果
      */
      */
     fun startBrowserAwait(url: String, title: String): StrResponse {
     fun startBrowserAwait(url: String, title: String): StrResponse {
-        return StrResponse(url, SourceVerificationHelp.getVerificationResult(getSource(), url, title, true))
+        return StrResponse(
+            url,
+            SourceVerificationHelp.getVerificationResult(getSource(), url, title, true)
+        )
     }
     }
 
 
     /**
     /**
      * 打开图片验证码对话框,等待返回验证结果
      * 打开图片验证码对话框,等待返回验证结果
      */
      */
-     fun getVerificationCode(imageUrl: String): String {
-         return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
-     }
+    fun getVerificationCode(imageUrl: String): String {
+        return SourceVerificationHelp.getVerificationResult(getSource(), imageUrl, "", false)
+    }
 
 
     /**
     /**
      * 可从网络,本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本
      * 可从网络,本地文件(阅读私有缓存目录和书籍保存位置支持相对路径)导入JavaScript脚本
@@ -203,6 +207,29 @@ interface JsExtensions {
         }
         }
     }
     }
 
 
+    /**
+     * 下载文件
+     * @param url 下载地址:可带参数type,文件后缀,不带默认zip
+     * @return 下载的文件相对路径
+     */
+    fun downloadFile(url: String): String {
+        val analyzeUrl = AnalyzeUrl(url, source = getSource())
+        val type = analyzeUrl.type ?: "zip"
+        val path = FileUtils.getPath(
+            FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
+            "${MD5Utils.md5Encode16(url)}.${type}"
+        )
+        FileUtils.delete(path)
+        analyzeUrl.getInputStream().use { iStream ->
+            val file = FileUtils.createFileIfNotExist(path)
+            FileOutputStream(file).use { oStream ->
+                iStream.copyTo(oStream)
+            }
+        }
+        return path.substring(FileUtils.getCachePath().length)
+    }
+
+
     /**
     /**
      * 实现16进制字符串转文件
      * 实现16进制字符串转文件
      * @param content 需要转成文件的16进制字符串
      * @param content 需要转成文件的16进制字符串
@@ -211,18 +238,18 @@ interface JsExtensions {
      */
      */
     fun downloadFile(content: String, url: String): String {
     fun downloadFile(content: String, url: String): String {
         val type = AnalyzeUrl(url, source = getSource()).type ?: return ""
         val type = AnalyzeUrl(url, source = getSource()).type ?: return ""
-        val zipPath = FileUtils.getPath(
+        val path = FileUtils.getPath(
             FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
             FileUtils.createFolderIfNotExist(FileUtils.getCachePath()),
             "${MD5Utils.md5Encode16(url)}.${type}"
             "${MD5Utils.md5Encode16(url)}.${type}"
         )
         )
-        FileUtils.delete(zipPath)
-        val zipFile = FileUtils.createFileIfNotExist(zipPath)
+        FileUtils.delete(path)
+        val file = FileUtils.createFileIfNotExist(path)
         StringUtils.hexStringToByte(content).let {
         StringUtils.hexStringToByte(content).let {
             if (it.isNotEmpty()) {
             if (it.isNotEmpty()) {
-                zipFile.writeBytes(it)
+                file.writeBytes(it)
             }
             }
         }
         }
-        return zipPath.substring(FileUtils.getCachePath().length)
+        return path.substring(FileUtils.getCachePath().length)
     }
     }
 
 
     /**
     /**
@@ -433,13 +460,15 @@ interface JsExtensions {
     }
     }
 
 
     /**
     /**
-     * js实现文件夹内所有文件读取
+     * js实现文件夹内所有文本文件读取
+     * @param path 文件夹相对路径
+     * @return 所有文件字符串换行连接
      */
      */
-    fun getTxtInFolder(unzipPath: String): String {
-        if (unzipPath.isEmpty()) return ""
-        val unzipFolder = getFile(unzipPath)
+    fun getTxtInFolder(path: String): String {
+        if (path.isEmpty()) return ""
+        val folder = getFile(path)
         val contents = StringBuilder()
         val contents = StringBuilder()
-        unzipFolder.listFiles().let {
+        folder.listFiles().let {
             if (it != null) {
             if (it != null) {
                 for (f in it) {
                 for (f in it) {
                     val charsetName = EncodingDetect.getEncode(f)
                     val charsetName = EncodingDetect.getEncode(f)
@@ -449,7 +478,7 @@ interface JsExtensions {
                 contents.deleteCharAt(contents.length - 1)
                 contents.deleteCharAt(contents.length - 1)
             }
             }
         }
         }
-        FileUtils.delete(unzipFolder.absolutePath)
+        FileUtils.delete(folder.absolutePath)
         return contents.toString()
         return contents.toString()
     }
     }
 
 
@@ -833,7 +862,7 @@ interface JsExtensions {
     }
     }
 
 
     fun desBase64DecodeToString(
     fun desBase64DecodeToString(
-       data: String, key: String, transformation: String, iv: String
+        data: String, key: String, transformation: String, iv: String
     ): String? {
     ): String? {
         return EncoderUtils.decryptBase64DES(
         return EncoderUtils.decryptBase64DES(
             data.encodeToByteArray(),
             data.encodeToByteArray(),
@@ -1028,7 +1057,10 @@ interface JsExtensions {
         algorithm: String,
         algorithm: String,
         key: String
         key: String
     ): String {
     ): String {
-        return Base64.encodeToString(HMac(algorithm, key.toByteArray()).digest(data), Base64.NO_WRAP)
+        return Base64.encodeToString(
+            HMac(algorithm, key.toByteArray()).digest(data),
+            Base64.NO_WRAP
+        )
     }
     }
 
 
     fun md5Encode(str: String): String {
     fun md5Encode(str: String): String {

+ 11 - 1
app/src/main/java/io/legado/app/help/SourceAnalyzer.kt

@@ -151,6 +151,7 @@ object SourceAnalyzer {
                 source.enabled = sourceAny.enabled
                 source.enabled = sourceAny.enabled
                 source.enabledExplore = sourceAny.enabledExplore
                 source.enabledExplore = sourceAny.enabledExplore
                 source.enabledCookieJar = sourceAny.enabledCookieJar
                 source.enabledCookieJar = sourceAny.enabledCookieJar
+                source.enabledReview = sourceAny.enabledReview
                 source.concurrentRate = sourceAny.concurrentRate
                 source.concurrentRate = sourceAny.concurrentRate
                 source.header = sourceAny.header
                 source.header = sourceAny.header
                 source.loginUrl = when (sourceAny.loginUrl) {
                 source.loginUrl = when (sourceAny.loginUrl) {
@@ -206,6 +207,13 @@ object SourceAnalyzer {
                     GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent))
                     GSON.fromJsonObject<ContentRule>(GSON.toJson(sourceAny.ruleContent))
                         .getOrNull()
                         .getOrNull()
                 }
                 }
+                source.ruleReview = if (sourceAny.ruleReview is String) {
+                    GSON.fromJsonObject<ReviewRule>(sourceAny.ruleReview.toString())
+                        .getOrNull()
+                } else {
+                    GSON.fromJsonObject<ReviewRule>(GSON.toJson(sourceAny.ruleReview))
+                        .getOrNull()
+                }
             }
             }
             source
             source
         }
         }
@@ -221,6 +229,7 @@ object SourceAnalyzer {
         var customOrder: Int = 0,                       // 手动排序编号
         var customOrder: Int = 0,                       // 手动排序编号
         var enabled: Boolean = true,                    // 是否启用
         var enabled: Boolean = true,                    // 是否启用
         var enabledExplore: Boolean = true,             // 启用发现
         var enabledExplore: Boolean = true,             // 启用发现
+        var enabledReview: Boolean = false,             // 启用段评
         var enabledCookieJar: Boolean = false,          // 启用CookieJar
         var enabledCookieJar: Boolean = false,          // 启用CookieJar
         var concurrentRate: String? = null,             // 并发率
         var concurrentRate: String? = null,             // 并发率
         var header: String? = null,                     // 请求头
         var header: String? = null,                     // 请求头
@@ -238,7 +247,8 @@ object SourceAnalyzer {
         var ruleSearch: Any? = null,                    // 搜索规则
         var ruleSearch: Any? = null,                    // 搜索规则
         var ruleBookInfo: Any? = null,                  // 书籍信息页规则
         var ruleBookInfo: Any? = null,                  // 书籍信息页规则
         var ruleToc: Any? = null,                       // 目录页规则
         var ruleToc: Any? = null,                       // 目录页规则
-        var ruleContent: Any? = null                    // 正文页规则
+        var ruleContent: Any? = null,                   // 正文页规则
+        var ruleReview: Any? = null                     // 段评规则
     )
     )
 
 
     // default规则适配
     // default规则适配

+ 6 - 0
app/src/main/java/io/legado/app/help/config/AppConfig.kt

@@ -154,6 +154,12 @@ object AppConfig : SharedPreferences.OnSharedPreferenceChangeListener {
     val autoRefreshBook: Boolean
     val autoRefreshBook: Boolean
         get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
         get() = appCtx.getPrefBoolean(PreferKey.autoRefresh)
 
 
+    var enableReview: Boolean
+        get() = appCtx.getPrefBoolean(PreferKey.enableReview, false)
+        set(value) {
+            appCtx.putPrefBoolean(PreferKey.enableReview, value)
+        }
+
     var threadCount: Int
     var threadCount: Int
         get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
         get() = appCtx.getPrefInt(PreferKey.threadCount, 16)
         set(value) {
         set(value) {

+ 1 - 1
app/src/main/java/io/legado/app/help/config/ReadBookConfig.kt

@@ -449,7 +449,7 @@ object ReadBookConfig {
         var letterSpacing: Float = 0.1f,//字间距
         var letterSpacing: Float = 0.1f,//字间距
         var lineSpacingExtra: Int = 12,//行间距
         var lineSpacingExtra: Int = 12,//行间距
         var paragraphSpacing: Int = 2,//段距
         var paragraphSpacing: Int = 2,//段距
-        var titleMode: Int = 0,//标题居中
+        var titleMode: Int = 0,//标题位置 0:居左 1:居中 2:隐藏
         var titleSize: Int = 0,
         var titleSize: Int = 0,
         var titleTopSpacing: Int = 0,
         var titleTopSpacing: Int = 0,
         var titleBottomSpacing: Int = 0,
         var titleBottomSpacing: Int = 0,

+ 7 - 0
app/src/main/java/io/legado/app/model/webBook/WebBook.kt

@@ -300,6 +300,13 @@ object WebBook {
         }
         }
     }
     }
 
 
+    /**
+     * 获取段评
+     */
+    fun getReview() {
+        // TODO
+    }
+
     /**
     /**
      * 精准搜索
      * 精准搜索
      */
      */

+ 6 - 0
app/src/main/java/io/legado/app/ui/book/read/ReadBookActivity.kt

@@ -256,6 +256,7 @@ class ReadBookActivity : BaseReadBookActivity(),
                     else -> when (item.itemId) {
                     else -> when (item.itemId) {
                         R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule()
                         R.id.menu_enable_replace -> item.isChecked = book.getUseReplaceRule()
                         R.id.menu_re_segment -> item.isChecked = book.getReSegment()
                         R.id.menu_re_segment -> item.isChecked = book.getReSegment()
+                        R.id.menu_enable_review -> item.isChecked = AppConfig.enableReview
                         R.id.menu_reverse_content -> item.isVisible = onLine
                         R.id.menu_reverse_content -> item.isVisible = onLine
                     }
                     }
                 }
                 }
@@ -356,6 +357,11 @@ class ReadBookActivity : BaseReadBookActivity(),
                 menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment()
                 menu?.findItem(R.id.menu_re_segment)?.isChecked = it.getReSegment()
                 ReadBook.loadContent(false)
                 ReadBook.loadContent(false)
             }
             }
+            R.id.menu_enable_review -> {
+                AppConfig.enableReview = !AppConfig.enableReview
+                menu?.findItem(R.id.menu_enable_review)?.isChecked = AppConfig.enableReview
+                ReadBook.loadContent(false)
+            }
             R.id.menu_page_anim -> showPageAnimConfig {
             R.id.menu_page_anim -> showPageAnimConfig {
                 binding.readView.upPageAnim()
                 binding.readView.upPageAnim()
                 ReadBook.loadContent(false)
                 ReadBook.loadContent(false)

+ 143 - 40
app/src/main/java/io/legado/app/ui/book/read/page/ContentTextView.kt

@@ -4,6 +4,8 @@ import android.content.Context
 import android.graphics.Canvas
 import android.graphics.Canvas
 import android.graphics.Paint
 import android.graphics.Paint
 import android.graphics.RectF
 import android.graphics.RectF
+import android.text.StaticLayout
+import android.text.TextPaint
 import android.util.AttributeSet
 import android.util.AttributeSet
 import android.view.View
 import android.view.View
 import io.legado.app.R
 import io.legado.app.R
@@ -14,10 +16,7 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.lib.theme.accentColor
 import io.legado.app.lib.theme.accentColor
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
-import io.legado.app.ui.book.read.page.entities.TextChar
-import io.legado.app.ui.book.read.page.entities.TextLine
-import io.legado.app.ui.book.read.page.entities.TextPage
-import io.legado.app.ui.book.read.page.entities.TextPos
+import io.legado.app.ui.book.read.page.entities.*
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.ImageProvider
 import io.legado.app.ui.book.read.page.provider.ImageProvider
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.ui.book.read.page.provider.TextPageFactory
@@ -28,7 +27,7 @@ import io.legado.app.utils.toastOnUi
 import kotlin.math.min
 import kotlin.math.min
 
 
 /**
 /**
- * 阅读内容界面
+ * 阅读内容视图
  */
  */
 class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
 class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, attrs) {
     var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
     var selectAble = context.getPrefBoolean(PreferKey.textSelectAble, true)
@@ -65,12 +64,18 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         callBack = activity as CallBack
         callBack = activity as CallBack
     }
     }
 
 
+    /**
+     * 设置内容
+     */
     fun setContent(textPage: TextPage) {
     fun setContent(textPage: TextPage) {
         this.textPage = textPage
         this.textPage = textPage
         imagePaint.isAntiAlias = AppConfig.useAntiAlias
         imagePaint.isAntiAlias = AppConfig.useAntiAlias
         invalidate()
         invalidate()
     }
     }
 
 
+    /**
+     * 更新绘制区域
+     */
     fun upVisibleRect() {
     fun upVisibleRect() {
         visibleRect.set(
         visibleRect.set(
             ChapterProvider.paddingLeft.toFloat(),
             ChapterProvider.paddingLeft.toFloat(),
@@ -121,6 +126,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         }
         }
     }
     }
 
 
+    /**
+     * 绘制页面
+     */
     private fun draw(
     private fun draw(
         canvas: Canvas,
         canvas: Canvas,
         textPage: TextPage,
         textPage: TextPage,
@@ -150,15 +158,90 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
             ChapterProvider.contentPaint
             ChapterProvider.contentPaint
         }
         }
         val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
         val textColor = if (textLine.isReadAloud) context.accentColor else ReadBookConfig.textColor
+        val linePaint = Paint()
+        linePaint.strokeWidth = textPaint.textSize / 21
+        linePaint.color = textColor
+        val reviewCountPaint = TextPaint()
+        reviewCountPaint.textSize = textPaint.textSize * 0.6F
+        reviewCountPaint.color = textColor
         textLine.textChars.forEach {
         textLine.textChars.forEach {
-            if (it.isImage) {
-                drawImage(canvas, textPage, textLine, it, lineTop, lineBottom)
-            } else {
-                textPaint.color = textColor
-                if (it.isSearchResult) {
-                    textPaint.color = context.accentColor
+            when (it.style) {
+                0 -> {
+                    textPaint.color = textColor
+                    if (it.isSearchResult) {
+                        textPaint.color = context.accentColor
+                    }
+                    canvas.drawText(it.charData, it.start, lineBase, textPaint)
+                }
+                1 -> drawImage(canvas, textPage, textLine, it, lineTop, lineBottom)
+                2 -> {
+                    if (textLine.reviewCount <= 0) return@forEach
+                    canvas.drawLine(
+                        it.start,
+                        lineBase - textPaint.textSize * 2 / 5,
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize / 4,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start,
+                        lineBase - textPaint.textSize * 0.38F,
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize * 0.55F,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize / 4,
+                        it.start + textPaint.textSize / 6,
+                        lineBase,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize * 0.55F,
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize * 0.8F,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start + textPaint.textSize / 6,
+                        lineBase,
+                        it.start + textPaint.textSize * 1.6F,
+                        lineBase,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start + textPaint.textSize / 6,
+                        lineBase - textPaint.textSize * 0.8F,
+                        it.start + textPaint.textSize * 1.6F,
+                        lineBase - textPaint.textSize * 0.8F,
+                        linePaint
+                    )
+                    canvas.drawLine(
+                        it.start + textPaint.textSize * 1.6F,
+                        lineBase - textPaint.textSize * 0.8F,
+                        it.start + textPaint.textSize * 1.6F,
+                        lineBase,
+                        linePaint
+                    )
+                    if (textLine.reviewCount < 100) canvas.drawText(
+                        textLine.reviewCount.toString(),
+                        it.start + textPaint.textSize * 0.87F -
+                                StaticLayout.getDesiredWidth(
+                                    textLine.reviewCount.toString(),
+                                    reviewCountPaint
+                                ) / 2,
+                        lineBase - textPaint.textSize / 6,
+                        reviewCountPaint
+                    )
+                    else canvas.drawText(
+                        "99+",
+                        it.start + textPaint.textSize * 0.35F,
+                        lineBase - textPaint.textSize / 6,
+                        reviewCountPaint
+                    )
                 }
                 }
-                canvas.drawText(it.charData, it.start, lineBase, textPaint)
             }
             }
             if (it.selected) {
             if (it.selected) {
                 canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint)
                 canvas.drawRect(it.start, lineTop, it.end, lineBottom, selectedPaint)
@@ -174,7 +257,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         canvas: Canvas,
         canvas: Canvas,
         textPage: TextPage,
         textPage: TextPage,
         textLine: TextLine,
         textLine: TextLine,
-        textChar: TextChar,
+        textChar: TextColumn,
         lineTop: Float,
         lineTop: Float,
         lineBottom: Float
         lineBottom: Float
     ) {
     ) {
@@ -259,6 +342,9 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         invalidate()
         invalidate()
     }
     }
 
 
+    /**
+     * 重置滚动位置
+     */
     fun resetPageOffset() {
     fun resetPageOffset() {
         pageOffset = 0
         pageOffset = 0
     }
     }
@@ -269,18 +355,34 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     fun longPress(
     fun longPress(
         x: Float,
         x: Float,
         y: Float,
         y: Float,
-        select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit,
+        select: (textPos: TextPos) -> Unit,
     ) {
     ) {
-        touch(x, y) { _, relativePos, _, lineIndex, _, charIndex, textChar ->
-            if (textChar.isImage) {
-                callBack.onImageLongPress(x, y, textChar.charData)
+        touch(x, y) { _, textPos, _, _, textColumn ->
+            if (textColumn.style == 2) return@touch
+            if (textColumn.style == 1) {
+                callBack.onImageLongPress(x, y, textColumn.charData)
             } else {
             } else {
                 if (!selectAble) return@touch
                 if (!selectAble) return@touch
-                textChar.selected = true
+                textColumn.selected = true
                 invalidate()
                 invalidate()
-                select(relativePos, lineIndex, charIndex)
+                select(textPos)
+            }
+        }
+    }
+
+    /**
+     * 单击
+     * @return true:已处理, false:未处理
+     */
+    fun click(x: Float, y: Float): Boolean {
+        var handled = false
+        touch(x, y) { _, textPos, textPage, textLine, textColumn ->
+            if (textColumn.style == 2) {
+                context.toastOnUi("Button Pressed!")
+                handled = true
             }
             }
         }
         }
+        return handled
     }
     }
 
 
     /**
     /**
@@ -289,12 +391,13 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
     fun selectText(
     fun selectText(
         x: Float,
         x: Float,
         y: Float,
         y: Float,
-        select: (relativePage: Int, lineIndex: Int, charIndex: Int) -> Unit,
+        select: (textPos: TextPos) -> Unit,
     ) {
     ) {
-        touch(x, y) { _, relativePos, _, lineIndex, _, charIndex, textChar ->
-            textChar.selected = true
+        touch(x, y) { _, textPos, _, _, textColumn ->
+            if (textColumn.style == 2) return@touch
+            textColumn.selected = true
             invalidate()
             invalidate()
-            select(relativePos, lineIndex, charIndex)
+            select(textPos)
         }
         }
     }
     }
 
 
@@ -302,11 +405,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
      * 开始选择符移动
      * 开始选择符移动
      */
      */
     fun selectStartMove(x: Float, y: Float) {
     fun selectStartMove(x: Float, y: Float) {
-        touch(x, y) { relativeOffset, relativePos, _, lineIndex, textLine, charIndex, textChar ->
-            val pos = TextPos(relativePos, lineIndex, charIndex)
-            if (selectStart.compare(pos) != 0) {
-                if (pos.compare(selectEnd) <= 0) {
-                    selectStart.upData(pos = pos)
+        touch(x, y) { relativeOffset, textPos, _, textLine, textChar ->
+            if (selectStart.compare(textPos) != 0) {
+                if (textPos.compare(selectEnd) <= 0) {
+                    selectStart.upData(pos = textPos)
                     upSelectedStart(
                     upSelectedStart(
                         textChar.start,
                         textChar.start,
                         textLine.lineBottom + relativeOffset,
                         textLine.lineBottom + relativeOffset,
@@ -322,11 +424,10 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
      * 结束选择符移动
      * 结束选择符移动
      */
      */
     fun selectEndMove(x: Float, y: Float) {
     fun selectEndMove(x: Float, y: Float) {
-        touch(x, y) { relativeOffset, relativePos, _, lineIndex, textLine, charIndex, textChar ->
-            val pos = TextPos(relativePos, lineIndex, charIndex)
-            if (pos.compare(selectEnd) != 0) {
-                if (pos.compare(selectStart) >= 0) {
-                    selectEnd.upData(pos)
+        touch(x, y) { relativeOffset, textPos, _, textLine, textChar ->
+            if (textPos.compare(selectEnd) != 0) {
+                if (textPos.compare(selectStart) >= 0) {
+                    selectEnd.upData(textPos)
                     upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
                     upSelectedEnd(textChar.end, textLine.lineBottom + relativeOffset)
                     upSelectChars()
                     upSelectChars()
                 }
                 }
@@ -334,17 +435,19 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
         }
         }
     }
     }
 
 
+    /**
+     * 触碰位置信息
+     * @param touched 回调
+     */
     private fun touch(
     private fun touch(
         x: Float,
         x: Float,
         y: Float,
         y: Float,
         touched: (
         touched: (
             relativeOffset: Float,
             relativeOffset: Float,
-            relativePos: Int,
+            textPos: TextPos,
             textPage: TextPage,
             textPage: TextPage,
-            lineIndex: Int,
             textLine: TextLine,
             textLine: TextLine,
-            charIndex: Int,
-            textChar: TextChar
+            textColumn: TextColumn
         ) -> Unit
         ) -> Unit
     ) {
     ) {
         if (!visibleRect.contains(x, y)) return
         if (!visibleRect.contains(x, y)) return
@@ -363,9 +466,8 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                         if (textChar.isTouch(x)) {
                         if (textChar.isTouch(x)) {
                             touched.invoke(
                             touched.invoke(
                                 relativeOffset,
                                 relativeOffset,
-                                relativePos, textPage,
-                                lineIndex, textLine,
-                                charIndex, textChar
+                                TextPos(relativePos, lineIndex, charIndex),
+                                textPage, textLine, textChar
                             )
                             )
                             return
                             return
                         }
                         }
@@ -415,6 +517,7 @@ class ContentTextView(context: Context, attrs: AttributeSet?) : View(context, at
                 textPos.lineIndex = lineIndex
                 textPos.lineIndex = lineIndex
                 for ((charIndex, textChar) in textLine.textChars.withIndex()) {
                 for ((charIndex, textChar) in textLine.textChars.withIndex()) {
                     textPos.charIndex = charIndex
                     textPos.charIndex = charIndex
+                    if (textChar.style == 2) continue
                     textChar.selected =
                     textChar.selected =
                         textPos.compare(selectStart) >= 0 && textPos.compare(selectEnd) <= 0
                         textPos.compare(selectStart) >= 0 && textPos.compare(selectEnd) <= 0
                     textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult
                     textChar.isSearchResult = textChar.selected && callBack.isSelectingSearchResult

+ 58 - 3
app/src/main/java/io/legado/app/ui/book/read/page/PageView.kt

@@ -14,6 +14,7 @@ import io.legado.app.help.config.ReadTipConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.ReadBookActivity
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.ui.book.read.page.entities.TextPage
+import io.legado.app.ui.book.read.page.entities.TextPos
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.widget.BatteryView
 import io.legado.app.ui.widget.BatteryView
 import io.legado.app.utils.activity
 import io.legado.app.utils.activity
@@ -24,7 +25,7 @@ import splitties.views.backgroundColor
 import java.util.*
 import java.util.*
 
 
 /**
 /**
- * 阅读界面
+ * 页面视图
  */
  */
 class PageView(context: Context) : FrameLayout(context) {
 class PageView(context: Context) : FrameLayout(context) {
 
 
@@ -104,6 +105,9 @@ class PageView(context: Context) : FrameLayout(context) {
         isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true
         isGone = ReadBookConfig.hideStatusBar || readBookActivity?.isInMultiWindow == true
     }
     }
 
 
+    /**
+     * 更新阅读信息
+     */
     private fun upTipStyle() = binding.run {
     private fun upTipStyle() = binding.run {
         tvHeaderLeft.tag = null
         tvHeaderLeft.tag = null
         tvHeaderMiddle.tag = null
         tvHeaderMiddle.tag = null
@@ -189,6 +193,10 @@ class PageView(context: Context) : FrameLayout(context) {
         }
         }
     }
     }
 
 
+    /**
+     * 获取信息视图
+     * @param tip 信息类型
+     */
     private fun getTipView(tip: Int): BatteryView? = binding.run {
     private fun getTipView(tip: Int): BatteryView? = binding.run {
         return when (tip) {
         return when (tip) {
             ReadTipConfig.tipHeaderLeft -> tvHeaderLeft
             ReadTipConfig.tipHeaderLeft -> tvHeaderLeft
@@ -201,6 +209,9 @@ class PageView(context: Context) : FrameLayout(context) {
         }
         }
     }
     }
 
 
+    /**
+     * 更新背景
+     */
     fun upBg() {
     fun upBg() {
         if (ReadBookConfig.bgAlpha < 100) {
         if (ReadBookConfig.bgAlpha < 100) {
             binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor
             binding.vwRoot.backgroundColor = ReadBookConfig.bgMeanColor
@@ -211,15 +222,24 @@ class PageView(context: Context) : FrameLayout(context) {
         upBgAlpha()
         upBgAlpha()
     }
     }
 
 
+    /**
+     * 更新背景透明度
+     */
     fun upBgAlpha() {
     fun upBgAlpha() {
         binding.vwBg.alpha = ReadBookConfig.bgAlpha / 100f
         binding.vwBg.alpha = ReadBookConfig.bgAlpha / 100f
     }
     }
 
 
+    /**
+     * 更新时间信息
+     */
     fun upTime() {
     fun upTime() {
         tvTime?.text = timeFormat.format(Date(System.currentTimeMillis()))
         tvTime?.text = timeFormat.format(Date(System.currentTimeMillis()))
         upTimeBattery()
         upTimeBattery()
     }
     }
 
 
+    /**
+     * 更新电池信息
+     */
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     fun upBattery(battery: Int) {
     fun upBattery(battery: Int) {
         this.battery = battery
         this.battery = battery
@@ -228,6 +248,9 @@ class PageView(context: Context) : FrameLayout(context) {
         upTimeBattery()
         upTimeBattery()
     }
     }
 
 
+    /**
+     * 更新电池信息
+     */
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     private fun upTimeBattery() {
     private fun upTimeBattery() {
         val time = timeFormat.format(Date(System.currentTimeMillis()))
         val time = timeFormat.format(Date(System.currentTimeMillis()))
@@ -235,6 +258,9 @@ class PageView(context: Context) : FrameLayout(context) {
         tvTimeBatteryP?.text = "$time $battery%"
         tvTimeBatteryP?.text = "$time $battery%"
     }
     }
 
 
+    /**
+     * 设置内容
+     */
     fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
     fun setContent(textPage: TextPage, resetPageOffset: Boolean = true) {
         setProgress(textPage)
         setProgress(textPage)
         if (resetPageOffset) {
         if (resetPageOffset) {
@@ -243,14 +269,23 @@ class PageView(context: Context) : FrameLayout(context) {
         binding.contentTextView.setContent(textPage)
         binding.contentTextView.setContent(textPage)
     }
     }
 
 
+    /**
+     * 设置无障碍文本
+     */
     fun setContentDescription(content: String) {
     fun setContentDescription(content: String) {
         binding.contentTextView.contentDescription = content
         binding.contentTextView.contentDescription = content
     }
     }
 
 
+    /**
+     * 重置滚动位置
+     */
     fun resetPageOffset() {
     fun resetPageOffset() {
         binding.contentTextView.resetPageOffset()
         binding.contentTextView.resetPageOffset()
     }
     }
 
 
+    /**
+     * 设置进度
+     */
     @SuppressLint("SetTextI18n")
     @SuppressLint("SetTextI18n")
     fun setProgress(textPage: TextPage) = textPage.apply {
     fun setProgress(textPage: TextPage) = textPage.apply {
         tvBookName?.text = ReadBook.book?.name
         tvBookName?.text = ReadBook.book?.name
@@ -260,24 +295,44 @@ class PageView(context: Context) : FrameLayout(context) {
         tvPageAndTotal?.text = "${index.plus(1)}/$pageSize  $readProgress"
         tvPageAndTotal?.text = "${index.plus(1)}/$pageSize  $readProgress"
     }
     }
 
 
+    /**
+     * 滚动事件
+     */
     fun scroll(offset: Int) {
     fun scroll(offset: Int) {
         binding.contentTextView.scroll(offset)
         binding.contentTextView.scroll(offset)
     }
     }
 
 
+    /**
+     * 更新是否开启选择功能
+     */
     fun upSelectAble(selectAble: Boolean) {
     fun upSelectAble(selectAble: Boolean) {
         binding.contentTextView.selectAble = selectAble
         binding.contentTextView.selectAble = selectAble
     }
     }
 
 
+    /**
+     * 优先处理页面内单击
+     * @return true:已处理, false:未处理
+     */
+    fun onClick(x: Float, y: Float): Boolean {
+        return binding.contentTextView.click(x, y - headerHeight)
+    }
+
+    /**
+     * 长按事件
+     */
     fun longPress(
     fun longPress(
         x: Float, y: Float,
         x: Float, y: Float,
-        select: (relativePagePos: Int, lineIndex: Int, charIndex: Int) -> Unit,
+        select: (textPos: TextPos) -> Unit,
     ) {
     ) {
         return binding.contentTextView.longPress(x, y - headerHeight, select)
         return binding.contentTextView.longPress(x, y - headerHeight, select)
     }
     }
 
 
+    /**
+     * 选择文本
+     */
     fun selectText(
     fun selectText(
         x: Float, y: Float,
         x: Float, y: Float,
-        select: (relativePagePos: Int, lineIndex: Int, charIndex: Int) -> Unit,
+        select: (textPos: TextPos) -> Unit,
     ) {
     ) {
         return binding.contentTextView.selectText(x, y - headerHeight, select)
         return binding.contentTextView.selectText(x, y - headerHeight, select)
     }
     }

+ 29 - 16
app/src/main/java/io/legado/app/ui/book/read/page/ReadView.kt

@@ -28,11 +28,14 @@ import io.legado.app.ui.book.read.page.provider.TextPageFactory
 import io.legado.app.utils.activity
 import io.legado.app.utils.activity
 import io.legado.app.utils.invisible
 import io.legado.app.utils.invisible
 import io.legado.app.utils.screenshot
 import io.legado.app.utils.screenshot
+import io.legado.app.utils.toastOnUi
 import java.text.BreakIterator
 import java.text.BreakIterator
 import java.util.*
 import java.util.*
 import kotlin.math.abs
 import kotlin.math.abs
 
 
-
+/**
+ * 阅读视图
+ */
 class ReadView(context: Context, attrs: AttributeSet) :
 class ReadView(context: Context, attrs: AttributeSet) :
     FrameLayout(context, attrs),
     FrameLayout(context, attrs),
     DataSource {
     DataSource {
@@ -222,7 +225,9 @@ class ReadView(context: Context, attrs: AttributeSet) :
                 pressDown = false
                 pressDown = false
                 if (!isPageMove) {
                 if (!isPageMove) {
                     if (!longPressed && !pressOnTextSelected) {
                     if (!longPressed && !pressOnTextSelected) {
-                        onSingleTapUp()
+                        if (!curPage.onClick(startX, startY)) {
+                            onSingleTapUp()
+                        }
                         return true
                         return true
                     }
                     }
                 }
                 }
@@ -292,18 +297,18 @@ class ReadView(context: Context, attrs: AttributeSet) :
      */
      */
     private fun onLongPress() {
     private fun onLongPress() {
         kotlin.runCatching {
         kotlin.runCatching {
-            curPage.longPress(startX, startY) { relativePos, lineIndex, charIndex ->
+            curPage.longPress(startX, startY) { textPos: TextPos ->
                 isTextSelected = true
                 isTextSelected = true
                 pressOnTextSelected = true
                 pressOnTextSelected = true
-                initialTextPos.upData(relativePos, lineIndex, charIndex)
-                val startPos = TextPos(relativePos, lineIndex, charIndex)
-                val endPos = TextPos(relativePos, lineIndex, charIndex)
-                val page = curPage.relativePage(relativePos)
+                initialTextPos.upData(textPos)
+                val startPos = textPos.copy()
+                val endPos = textPos.copy()
+                val page = curPage.relativePage(textPos.relativePagePos)
                 val stringBuilder = StringBuilder()
                 val stringBuilder = StringBuilder()
-                var cIndex = charIndex
-                var lineStart = lineIndex
-                var lineEnd = lineIndex
-                for (index in lineIndex - 1 downTo 0) {
+                var cIndex = textPos.charIndex
+                var lineStart = textPos.lineIndex
+                var lineEnd = textPos.lineIndex
+                for (index in textPos.lineIndex - 1 downTo 0) {
                     val textLine = page.getLine(index)
                     val textLine = page.getLine(index)
                     if (textLine.isParagraphEnd) {
                     if (textLine.isParagraphEnd) {
                         break
                         break
@@ -313,7 +318,7 @@ class ReadView(context: Context, attrs: AttributeSet) :
                         cIndex += textLine.charSize
                         cIndex += textLine.charSize
                     }
                     }
                 }
                 }
-                for (index in lineIndex until page.lineSize) {
+                for (index in textPos.lineIndex until page.lineSize) {
                     val textLine = page.getLine(index)
                     val textLine = page.getLine(index)
                     stringBuilder.append(textLine.text)
                     stringBuilder.append(textLine.text)
                     lineEnd += 1
                     lineEnd += 1
@@ -419,11 +424,15 @@ class ReadView(context: Context, attrs: AttributeSet) :
      * 选择文本
      * 选择文本
      */
      */
     private fun selectText(x: Float, y: Float) {
     private fun selectText(x: Float, y: Float) {
-        curPage.selectText(x, y) { relativePagePos, lineIndex, charIndex ->
-            val compare = initialTextPos.compare(relativePagePos, lineIndex, charIndex)
+        curPage.selectText(x, y) { textPos ->
+            val compare = initialTextPos.compare(textPos)
             when {
             when {
                 compare >= 0 -> {
                 compare >= 0 -> {
-                    curPage.selectStartMoveIndex(relativePagePos, lineIndex, charIndex)
+                    curPage.selectStartMoveIndex(
+                        textPos.relativePagePos,
+                        textPos.lineIndex,
+                        textPos.charIndex
+                    )
                     curPage.selectEndMoveIndex(
                     curPage.selectEndMoveIndex(
                         initialTextPos.relativePagePos,
                         initialTextPos.relativePagePos,
                         initialTextPos.lineIndex,
                         initialTextPos.lineIndex,
@@ -436,7 +445,11 @@ class ReadView(context: Context, attrs: AttributeSet) :
                         initialTextPos.lineIndex,
                         initialTextPos.lineIndex,
                         initialTextPos.charIndex
                         initialTextPos.charIndex
                     )
                     )
-                    curPage.selectEndMoveIndex(relativePagePos, lineIndex, charIndex)
+                    curPage.selectEndMoveIndex(
+                        textPos.relativePagePos,
+                        textPos.lineIndex,
+                        textPos.charIndex
+                    )
                 }
                 }
             }
             }
         }
         }

+ 5 - 1
app/src/main/java/io/legado/app/ui/book/read/page/delegate/HorizontalPageDelegate.kt

@@ -119,7 +119,11 @@ abstract class HorizontalPageDelegate(readView: ReadView) : PageDelegate(readVie
         abortAnim()
         abortAnim()
         if (!hasNext()) return
         if (!hasNext()) return
         setDirection(PageDirection.NEXT)
         setDirection(PageDirection.NEXT)
-        readView.setStartPoint(viewWidth.toFloat(), 1f, false)
+        val y = when {
+            viewHeight / 2 < startY -> viewHeight.toFloat() * 0.9f
+            else -> 1f
+        }
+        readView.setStartPoint(viewWidth.toFloat() * 0.9f, y, false)
         onAnimStart(animationSpeed)
         onAnimStart(animationSpeed)
     }
     }
 
 

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChapter.kt

@@ -2,6 +2,9 @@ package io.legado.app.ui.book.read.page.entities
 
 
 import kotlin.math.min
 import kotlin.math.min
 
 
+/**
+ * 章节信息
+ */
 @Suppress("unused")
 @Suppress("unused")
 data class TextChapter(
 data class TextChapter(
     val position: Int,
     val position: Int,

+ 5 - 2
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextChar.kt → app/src/main/java/io/legado/app/ui/book/read/page/entities/TextColumn.kt

@@ -1,11 +1,14 @@
 package io.legado.app.ui.book.read.page.entities
 package io.legado.app.ui.book.read.page.entities
 
 
-data class TextChar(
+/**
+ * 字符信息
+ */
+data class TextColumn(
     val charData: String,
     val charData: String,
     var start: Float,
     var start: Float,
     var end: Float,
     var end: Float,
+    val style: Int = 0, //0:文字,1:图片,2:按钮
     var selected: Boolean = false,
     var selected: Boolean = false,
-    var isImage: Boolean = false,
     var isSearchResult: Boolean = false
     var isSearchResult: Boolean = false
 ) {
 ) {
 
 

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

@@ -4,10 +4,14 @@ import android.text.TextPaint
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.ui.book.read.page.provider.ChapterProvider
 import io.legado.app.utils.textHeight
 import io.legado.app.utils.textHeight
 
 
+/**
+ * 行信息
+ */
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 data class TextLine(
 data class TextLine(
     var text: String = "",
     var text: String = "",
-    val textChars: ArrayList<TextChar> = arrayListOf(),
+    val textChars: ArrayList<TextColumn> = arrayListOf(),
+    val reviewCount: Int = 0,
     var lineTop: Float = 0f,
     var lineTop: Float = 0f,
     var lineBase: Float = 0f,
     var lineBase: Float = 0f,
     var lineBottom: Float = 0f,
     var lineBottom: Float = 0f,
@@ -27,13 +31,13 @@ data class TextLine(
         lineBase = lineBottom - textPaint.fontMetrics.descent
         lineBase = lineBottom - textPaint.fontMetrics.descent
     }
     }
 
 
-    fun getTextChar(index: Int): TextChar {
+    fun getTextChar(index: Int): TextColumn {
         return textChars.getOrElse(index) {
         return textChars.getOrElse(index) {
             textChars.last()
             textChars.last()
         }
         }
     }
     }
 
 
-    fun getTextCharReverseAt(index: Int): TextChar {
+    fun getTextCharReverseAt(index: Int): TextColumn {
         return textChars[textChars.lastIndex - index]
         return textChars[textChars.lastIndex - index]
     }
     }
 
 

+ 9 - 1
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPage.kt

@@ -11,6 +11,9 @@ import splitties.init.appCtx
 import java.text.DecimalFormat
 import java.text.DecimalFormat
 import kotlin.math.min
 import kotlin.math.min
 
 
+/**
+ * 页面信息
+ */
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 @Suppress("unused", "MemberVisibilityCanBePrivate")
 data class TextPage(
 data class TextPage(
     var index: Int = 0,
     var index: Int = 0,
@@ -111,7 +114,12 @@ data class TextPage(
                     val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
                     val cw = StaticLayout.getDesiredWidth(char, ChapterProvider.contentPaint)
                     val x1 = x + cw
                     val x1 = x + cw
                     textLine.textChars.add(
                     textLine.textChars.add(
-                        TextChar(char, start = x, end = x1)
+                        TextColumn(
+                            char,
+                            start = x,
+                            end = x1,
+                            style = if (textLine.text.length - 1 == index && char == "\uD83D\uDCAC") 2 else 0
+                        )
                     )
                     )
                     x = x1
                     x = x1
                 }
                 }

+ 3 - 0
app/src/main/java/io/legado/app/ui/book/read/page/entities/TextPos.kt

@@ -1,5 +1,8 @@
 package io.legado.app.ui.book.read.page.entities
 package io.legado.app.ui.book.read.page.entities
 
 
+/**
+ * 位置信息
+ */
 data class TextPos(
 data class TextPos(
     var relativePagePos: Int,
     var relativePagePos: Int,
     var lineIndex: Int,
     var lineIndex: Int,

+ 35 - 18
app/src/main/java/io/legado/app/ui/book/read/page/provider/ChapterProvider.kt

@@ -14,7 +14,7 @@ import io.legado.app.help.config.AppConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.help.config.ReadBookConfig
 import io.legado.app.model.ReadBook
 import io.legado.app.model.ReadBook
 import io.legado.app.ui.book.read.page.entities.TextChapter
 import io.legado.app.ui.book.read.page.entities.TextChapter
-import io.legado.app.ui.book.read.page.entities.TextChar
+import io.legado.app.ui.book.read.page.entities.TextColumn
 import io.legado.app.ui.book.read.page.entities.TextLine
 import io.legado.app.ui.book.read.page.entities.TextLine
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.ui.book.read.page.entities.TextPage
 import io.legado.app.utils.*
 import io.legado.app.utils.*
@@ -116,7 +116,13 @@ object ChapterProvider {
         if (ReadBookConfig.titleMode != 2) {
         if (ReadBookConfig.titleMode != 2) {
             displayTitle.splitNotBlank("\n").forEach { text ->
             displayTitle.splitNotBlank("\n").forEach { text ->
                 setTypeText(
                 setTypeText(
-                    book, absStartX, durY, text, textPages, stringBuilder, titlePaint,
+                    book,
+                    absStartX,
+                    durY,
+                    if (AppConfig.enableReview) text + "\ud83d\udcac" else text,
+                    textPages,
+                    stringBuilder,
+                    titlePaint,
                     isTitle = true,
                     isTitle = true,
                     isTitleWithNoContent = contents.isEmpty(),
                     isTitleWithNoContent = contents.isEmpty(),
                     isVolumeTitle = bookChapter.isVolume
                     isVolumeTitle = bookChapter.isVolume
@@ -171,7 +177,13 @@ object ChapterProvider {
                     val text = content.substring(start, content.length)
                     val text = content.substring(start, content.length)
                     if (text.isNotBlank()) {
                     if (text.isNotBlank()) {
                         setTypeText(
                         setTypeText(
-                            book, absStartX, durY, text, textPages, stringBuilder, contentPaint
+                            book,
+                            absStartX,
+                            durY,
+                            if (AppConfig.enableReview) text + "\ud83d\udcac" else text,
+                            textPages,
+                            stringBuilder,
+                            contentPaint
                         ).let {
                         ).let {
                             absStartX = it.first
                             absStartX = it.first
                             durY = it.second
                             durY = it.second
@@ -252,7 +264,7 @@ object ChapterProvider {
                 Pair(0f, width.toFloat())
                 Pair(0f, width.toFloat())
             }
             }
             textLine.textChars.add(
             textLine.textChars.add(
-                TextChar(charData = src, start = x + start, end = x + end, isImage = true)
+                TextColumn(charData = src, start = x + start, end = x + end, style = 1)
             )
             )
             textPages.last().textLines.add(textLine)
             textPages.last().textLines.add(textLine)
         }
         }
@@ -276,9 +288,8 @@ object ChapterProvider {
         srcList: LinkedList<String>? = null
         srcList: LinkedList<String>? = null
     ): Pair<Int, Float> {
     ): Pair<Int, Float> {
         var absStartX = x
         var absStartX = x
-        val layout = if (ReadBookConfig.useZhLayout) {
-            ZhLayout(text, textPaint, visibleWidth)
-        } else StaticLayout(
+        val layout = if (ReadBookConfig.useZhLayout) ZhLayout(text, textPaint, visibleWidth)
+        else StaticLayout(
             text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true
             text, textPaint, visibleWidth, Layout.Alignment.ALIGN_NORMAL, 0f, 0f, true
         )
         )
         var durY = when {
         var durY = when {
@@ -398,10 +409,14 @@ object ChapterProvider {
         }
         }
         val bodyIndent = ReadBookConfig.paragraphIndent
         val bodyIndent = ReadBookConfig.paragraphIndent
         val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
         val icw = StaticLayout.getDesiredWidth(bodyIndent, textPaint) / bodyIndent.length
-        bodyIndent.toStringArray().forEach { char ->
+        for (char in bodyIndent.toStringArray()) {
             val x1 = x + icw
             val x1 = x + icw
             textLine.textChars.add(
             textLine.textChars.add(
-                TextChar(charData = char, start = absStartX + x, end = absStartX + x1)
+                TextColumn(
+                    charData = char,
+                    start = absStartX + x,
+                    end = absStartX + x1
+                )
             )
             )
             x = x1
             x = x1
         }
         }
@@ -436,7 +451,7 @@ object ChapterProvider {
         words.forEachIndexed { index, char ->
         words.forEachIndexed { index, char ->
             val cw = StaticLayout.getDesiredWidth(char, textPaint)
             val cw = StaticLayout.getDesiredWidth(char, textPaint)
             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(book, absStartX, textLine, char, x, x1, srcList)
+            addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
             x = x1
             x = x1
         }
         }
         exceed(absStartX, textLine, words)
         exceed(absStartX, textLine, words)
@@ -455,10 +470,10 @@ object ChapterProvider {
         srcList: LinkedList<String>?
         srcList: LinkedList<String>?
     ) {
     ) {
         var x = startX
         var x = startX
-        words.forEach { char ->
+        words.forEachIndexed { index, char ->
             val cw = StaticLayout.getDesiredWidth(char, textPaint)
             val cw = StaticLayout.getDesiredWidth(char, textPaint)
             val x1 = x + cw
             val x1 = x + cw
-            addCharToLine(book, absStartX, textLine, char, x, x1, srcList)
+            addCharToLine(book, absStartX, textLine, char, x, x1, index + 1 == words.size, srcList)
             x = x1
             x = x1
         }
         }
         exceed(absStartX, textLine, words)
         exceed(absStartX, textLine, words)
@@ -474,25 +489,27 @@ object ChapterProvider {
         char: String,
         char: String,
         xStart: Float,
         xStart: Float,
         xEnd: Float,
         xEnd: Float,
+        isLineEnd: Boolean,
         srcList: LinkedList<String>?
         srcList: LinkedList<String>?
     ) {
     ) {
         if (srcList != null && char == srcReplaceChar) {
         if (srcList != null && char == srcReplaceChar) {
             val src = srcList.removeFirst()
             val src = srcList.removeFirst()
             ImageProvider.cacheImage(book, src, ReadBook.bookSource)
             ImageProvider.cacheImage(book, src, ReadBook.bookSource)
             textLine.textChars.add(
             textLine.textChars.add(
-                TextChar(
+                TextColumn(
                     charData = src,
                     charData = src,
                     start = absStartX + xStart,
                     start = absStartX + xStart,
                     end = absStartX + xEnd,
                     end = absStartX + xEnd,
-                    isImage = true
+                    style = 1
                 )
                 )
             )
             )
         } else {
         } else {
             textLine.textChars.add(
             textLine.textChars.add(
-                TextChar(
+                TextColumn(
                     charData = char,
                     charData = char,
                     start = absStartX + xStart,
                     start = absStartX + xStart,
-                    end = absStartX + xEnd
+                    end = absStartX + xEnd,
+                    style = if (isLineEnd && char == "\uD83D\uDCAC") 2 else 0
                 )
                 )
             )
             )
         }
         }
@@ -612,12 +629,12 @@ object ChapterProvider {
      * 更新绘制尺寸
      * 更新绘制尺寸
      */
      */
     fun upLayout() {
     fun upLayout() {
-        when(AppConfig.doublePageHorizontal){
+        when (AppConfig.doublePageHorizontal) {
             "0" -> doublePage = false
             "0" -> doublePage = false
             "1" -> doublePage = true
             "1" -> doublePage = true
             "2" -> {
             "2" -> {
                 doublePage = (viewWidth > viewHeight)
                 doublePage = (viewWidth > viewHeight)
-                    && ReadBook.pageAnim() != 3
+                        && ReadBook.pageAnim() != 3
             }
             }
         }
         }
 
 

+ 52 - 13
app/src/main/java/io/legado/app/ui/book/source/edit/BookSourceEditActivity.kt

@@ -41,10 +41,11 @@ class BookSourceEditActivity :
     private val adapter by lazy { BookSourceEditAdapter() }
     private val adapter by lazy { BookSourceEditAdapter() }
     private val sourceEntities: ArrayList<EditEntity> = ArrayList()
     private val sourceEntities: ArrayList<EditEntity> = ArrayList()
     private val searchEntities: ArrayList<EditEntity> = ArrayList()
     private val searchEntities: ArrayList<EditEntity> = ArrayList()
-    private val findEntities: ArrayList<EditEntity> = ArrayList()
+    private val exploreEntities: ArrayList<EditEntity> = ArrayList()
     private val infoEntities: ArrayList<EditEntity> = ArrayList()
     private val infoEntities: ArrayList<EditEntity> = ArrayList()
     private val tocEntities: ArrayList<EditEntity> = ArrayList()
     private val tocEntities: ArrayList<EditEntity> = ArrayList()
     private val contentEntities: ArrayList<EditEntity> = ArrayList()
     private val contentEntities: ArrayList<EditEntity> = ArrayList()
+    private val reviewEntities: ArrayList<EditEntity> = ArrayList()
     private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
     private val qrCodeResult = registerForActivityResult(QrCodeResult()) {
         it ?: return@registerForActivityResult
         it ?: return@registerForActivityResult
         viewModel.importSource(it) { source ->
         viewModel.importSource(it) { source ->
@@ -180,10 +181,11 @@ class BookSourceEditActivity :
     private fun setEditEntities(tabPosition: Int?) {
     private fun setEditEntities(tabPosition: Int?) {
         when (tabPosition) {
         when (tabPosition) {
             1 -> adapter.editEntities = searchEntities
             1 -> adapter.editEntities = searchEntities
-            2 -> adapter.editEntities = findEntities
+            2 -> adapter.editEntities = exploreEntities
             3 -> adapter.editEntities = infoEntities
             3 -> adapter.editEntities = infoEntities
             4 -> adapter.editEntities = tocEntities
             4 -> adapter.editEntities = tocEntities
             5 -> adapter.editEntities = contentEntities
             5 -> adapter.editEntities = contentEntities
+            6 -> adapter.editEntities = reviewEntities
             else -> adapter.editEntities = sourceEntities
             else -> adapter.editEntities = sourceEntities
         }
         }
         binding.recyclerView.scrollToPosition(0)
         binding.recyclerView.scrollToPosition(0)
@@ -192,8 +194,9 @@ class BookSourceEditActivity :
     private fun upSourceView(source: BookSource? = viewModel.bookSource) {
     private fun upSourceView(source: BookSource? = viewModel.bookSource) {
         source?.let {
         source?.let {
             binding.cbIsEnable.isChecked = it.enabled
             binding.cbIsEnable.isChecked = it.enabled
-            binding.cbIsEnableFind.isChecked = it.enabledExplore
+            binding.cbIsEnableExplore.isChecked = it.enabledExplore
             binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
             binding.cbIsEnableCookie.isChecked = it.enabledCookieJar ?: false
+            binding.cbIsEnableReview.isChecked = it.enabledReview ?: false
             binding.spType.setSelection(
             binding.spType.setSelection(
                 when (it.bookSourceType) {
                 when (it.bookSourceType) {
                     BookType.file -> 3
                     BookType.file -> 3
@@ -203,7 +206,7 @@ class BookSourceEditActivity :
                 }
                 }
             )
             )
         }
         }
-        //基本信息
+        // 基本信息
         sourceEntities.clear()
         sourceEntities.clear()
         sourceEntities.apply {
         sourceEntities.apply {
             add(EditEntity("bookSourceUrl", source?.bookSourceUrl, R.string.source_url))
             add(EditEntity("bookSourceUrl", source?.bookSourceUrl, R.string.source_url))
@@ -218,7 +221,7 @@ class BookSourceEditActivity :
             add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment))
             add(EditEntity("variableComment", source?.variableComment, R.string.variable_comment))
             add(EditEntity("concurrentRate", source?.concurrentRate, R.string.concurrent_rate))
             add(EditEntity("concurrentRate", source?.concurrentRate, R.string.concurrent_rate))
         }
         }
-        //搜索
+        // 搜索
         val sr = source?.getSearchRule()
         val sr = source?.getSearchRule()
         searchEntities.clear()
         searchEntities.clear()
         searchEntities.apply {
         searchEntities.apply {
@@ -234,10 +237,10 @@ class BookSourceEditActivity :
             add(EditEntity("coverUrl", sr?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("coverUrl", sr?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("bookUrl", sr?.bookUrl, R.string.r_book_url))
             add(EditEntity("bookUrl", sr?.bookUrl, R.string.r_book_url))
         }
         }
-        //发现
+        // 发现
         val er = source?.getExploreRule()
         val er = source?.getExploreRule()
-        findEntities.clear()
-        findEntities.apply {
+        exploreEntities.clear()
+        exploreEntities.apply {
             add(EditEntity("exploreUrl", source?.exploreUrl, R.string.r_find_url))
             add(EditEntity("exploreUrl", source?.exploreUrl, R.string.r_find_url))
             add(EditEntity("bookList", er?.bookList, R.string.r_book_list))
             add(EditEntity("bookList", er?.bookList, R.string.r_book_list))
             add(EditEntity("name", er?.name, R.string.r_book_name))
             add(EditEntity("name", er?.name, R.string.r_book_name))
@@ -249,7 +252,7 @@ class BookSourceEditActivity :
             add(EditEntity("coverUrl", er?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("coverUrl", er?.coverUrl, R.string.rule_cover_url))
             add(EditEntity("bookUrl", er?.bookUrl, R.string.r_book_url))
             add(EditEntity("bookUrl", er?.bookUrl, R.string.r_book_url))
         }
         }
-        //详情页
+        // 详情页
         val ir = source?.getBookInfoRule()
         val ir = source?.getBookInfoRule()
         infoEntities.clear()
         infoEntities.clear()
         infoEntities.apply {
         infoEntities.apply {
@@ -265,7 +268,7 @@ class BookSourceEditActivity :
             add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
             add(EditEntity("canReName", ir?.canReName, R.string.rule_can_re_name))
             add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
             add(EditEntity("downloadUrls", ir?.downloadUrls, R.string.download_url_rule))
         }
         }
-        //目录页
+        // 目录页
         val tr = source?.getTocRule()
         val tr = source?.getTocRule()
         tocEntities.clear()
         tocEntities.clear()
         tocEntities.apply {
         tocEntities.apply {
@@ -279,7 +282,7 @@ class BookSourceEditActivity :
             add(EditEntity("isPay", tr?.isPay, R.string.rule_is_pay))
             add(EditEntity("isPay", tr?.isPay, R.string.rule_is_pay))
             add(EditEntity("nextTocUrl", tr?.nextTocUrl, R.string.rule_next_toc_url))
             add(EditEntity("nextTocUrl", tr?.nextTocUrl, R.string.rule_next_toc_url))
         }
         }
-        //正文页
+        // 正文页
         val cr = source?.getContentRule()
         val cr = source?.getContentRule()
         contentEntities.clear()
         contentEntities.clear()
         contentEntities.apply {
         contentEntities.apply {
@@ -291,6 +294,21 @@ class BookSourceEditActivity :
             add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style))
             add(EditEntity("imageStyle", cr?.imageStyle, R.string.rule_image_style))
             add(EditEntity("payAction", cr?.payAction, R.string.rule_pay_action))
             add(EditEntity("payAction", cr?.payAction, R.string.rule_pay_action))
         }
         }
+        // 段评
+        val rr = source?.getReviewRule()
+        reviewEntities.clear()
+        reviewEntities.apply {
+            add(EditEntity("reviewUrl", rr?.reviewUrl, R.string.rule_review_url))
+            add(EditEntity("avatarRule", rr?.avatarRule, R.string.rule_avatar))
+            add(EditEntity("contentRule", rr?.contentRule, R.string.rule_review_content))
+            add(EditEntity("postTimeRule", rr?.postTimeRule, R.string.rule_post_time))
+            add(EditEntity("reviewQuoteUrl", rr?.reviewQuoteUrl, R.string.rule_review_quote))
+            add(EditEntity("voteUpUrl", rr?.voteUpUrl, R.string.review_vote_up))
+            add(EditEntity("voteDownUrl", rr?.voteDownUrl, R.string.review_vote_down))
+            add(EditEntity("postReviewUrl", rr?.postReviewUrl, R.string.post_review_url))
+            add(EditEntity("postQuoteUrl", rr?.postQuoteUrl, R.string.post_quote_url))
+            add(EditEntity("deleteUrl", rr?.deleteUrl, R.string.delete_review_url))
+        }
         binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
         binding.tabLayout.selectTab(binding.tabLayout.getTabAt(0))
         setEditEntities(0)
         setEditEntities(0)
     }
     }
@@ -298,8 +316,9 @@ class BookSourceEditActivity :
     private fun getSource(): BookSource {
     private fun getSource(): BookSource {
         val source = viewModel.bookSource?.copy() ?: BookSource()
         val source = viewModel.bookSource?.copy() ?: BookSource()
         source.enabled = binding.cbIsEnable.isChecked
         source.enabled = binding.cbIsEnable.isChecked
-        source.enabledExplore = binding.cbIsEnableFind.isChecked
+        source.enabledExplore = binding.cbIsEnableExplore.isChecked
         source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
         source.enabledCookieJar = binding.cbIsEnableCookie.isChecked
+        source.enabledReview = binding.cbIsEnableReview.isChecked
         source.bookSourceType = when (binding.spType.selectedItemPosition) {
         source.bookSourceType = when (binding.spType.selectedItemPosition) {
             3 -> BookType.file
             3 -> BookType.file
             2 -> BookType.image
             2 -> BookType.image
@@ -311,6 +330,7 @@ class BookSourceEditActivity :
         val bookInfoRule = BookInfoRule()
         val bookInfoRule = BookInfoRule()
         val tocRule = TocRule()
         val tocRule = TocRule()
         val contentRule = ContentRule()
         val contentRule = ContentRule()
+        val reviewRule = ReviewRule()
         sourceEntities.forEach {
         sourceEntities.forEach {
             when (it.key) {
             when (it.key) {
                 "bookSourceUrl" -> source.bookSourceUrl = it.value ?: ""
                 "bookSourceUrl" -> source.bookSourceUrl = it.value ?: ""
@@ -351,7 +371,7 @@ class BookSourceEditActivity :
                     viewModel.ruleComplete(it.value, searchRule.bookList, 2)
                     viewModel.ruleComplete(it.value, searchRule.bookList, 2)
             }
             }
         }
         }
-        findEntities.forEach {
+        exploreEntities.forEach {
             when (it.key) {
             when (it.key) {
                 "exploreUrl" -> source.exploreUrl = it.value
                 "exploreUrl" -> source.exploreUrl = it.value
                 "bookList" -> exploreRule.bookList = it.value
                 "bookList" -> exploreRule.bookList = it.value
@@ -429,11 +449,30 @@ class BookSourceEditActivity :
                 "payAction" -> contentRule.payAction = it.value
                 "payAction" -> contentRule.payAction = it.value
             }
             }
         }
         }
+        reviewEntities.forEach {
+            when (it.key) {
+                "reviewUrl" -> reviewRule.reviewUrl = it.value
+                "avatarRule" -> reviewRule.avatarRule =
+                    viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 3)
+                "contentRule" -> reviewRule.contentRule =
+                    viewModel.ruleComplete(it.value, reviewRule.reviewUrl)
+                "postTimeRule" -> reviewRule.postTimeRule =
+                    viewModel.ruleComplete(it.value, reviewRule.reviewUrl)
+                "reviewQuoteUrl" -> reviewRule.reviewQuoteUrl =
+                    viewModel.ruleComplete(it.value, reviewRule.reviewUrl, 2)
+                "voteUpUrl" -> reviewRule.voteUpUrl = it.value
+                "voteDownUrl" -> reviewRule.voteDownUrl = it.value
+                "postReviewUrl" -> reviewRule.postReviewUrl = it.value
+                "postQuoteUrl" -> reviewRule.postQuoteUrl = it.value
+                "deleteUrl" -> reviewRule.deleteUrl =it.value
+            }
+        }
         source.ruleSearch = searchRule
         source.ruleSearch = searchRule
         source.ruleExplore = exploreRule
         source.ruleExplore = exploreRule
         source.ruleBookInfo = bookInfoRule
         source.ruleBookInfo = bookInfoRule
         source.ruleToc = tocRule
         source.ruleToc = tocRule
         source.ruleContent = contentRule
         source.ruleContent = contentRule
+        source.ruleReview = reviewRule
         return source
         return source
     }
     }
 
 

+ 24 - 4
app/src/main/res/layout/activity_book_source_edit.xml

@@ -27,7 +27,7 @@
             android:text="@string/is_enable" />
             android:text="@string/is_enable" />
 
 
         <io.legado.app.lib.theme.view.ThemeCheckBox
         <io.legado.app.lib.theme.view.ThemeCheckBox
-            android:id="@+id/cb_is_enable_find"
+            android:id="@+id/cb_is_enable_explore"
             android:layout_width="wrap_content"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:checked="true"
             android:checked="true"
@@ -40,18 +40,33 @@
             android:checked="true"
             android:checked="true"
             android:text="@string/auto_save_cookie" />
             android:text="@string/auto_save_cookie" />
 
 
-        <TextView
+        <io.legado.app.lib.theme.view.ThemeCheckBox
+            android:id="@+id/cb_is_enable_review"
             android:layout_width="wrap_content"
             android:layout_width="wrap_content"
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
+            android:checked="true"
+            android:text="@string/review" />
+
+    </LinearLayout>
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:orientation="horizontal">
+
+        <TextView
+            android:height="30dp"
+            android:gravity="center"
+            android:layout_width="wrap_content"
+            android:layout_height="match_parent"
             android:text="@string/book_type"
             android:text="@string/book_type"
-            android:layout_marginLeft="12dp"
+            android:layout_marginLeft="8dp"
             android:layout_marginRight="3dp"
             android:layout_marginRight="3dp"
             tools:ignore="RtlHardcoded" />
             tools:ignore="RtlHardcoded" />
 
 
         <androidx.appcompat.widget.AppCompatSpinner
         <androidx.appcompat.widget.AppCompatSpinner
             android:id="@+id/sp_type"
             android:id="@+id/sp_type"
             android:layout_width="wrap_content"
             android:layout_width="wrap_content"
-            android:layout_height="wrap_content"
+            android:layout_height="match_parent"
             android:entries="@array/book_type"
             android:entries="@array/book_type"
             app:theme="@style/Spinner" />
             app:theme="@style/Spinner" />
 
 
@@ -94,6 +109,11 @@
             android:layout_height="wrap_content"
             android:layout_height="wrap_content"
             android:text="@string/source_tab_content" />
             android:text="@string/source_tab_content" />
 
 
+        <com.google.android.material.tabs.TabItem
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:text="@string/review" />
+
     </com.google.android.material.tabs.TabLayout>
     </com.google.android.material.tabs.TabLayout>
 
 
     <androidx.recyclerview.widget.RecyclerView
     <androidx.recyclerview.widget.RecyclerView

+ 7 - 0
app/src/main/res/menu/book_read.xml

@@ -84,6 +84,13 @@
         android:checked="false"
         android:checked="false"
         app:showAsAction="never" />
         app:showAsAction="never" />
 
 
+    <item
+        android:id="@+id/menu_enable_review"
+        android:title="@string/review"
+        android:checkable="true"
+        android:checked="false"
+        app:showAsAction="never" />
+
     <item
     <item
         android:id="@+id/menu_image_style"
         android:id="@+id/menu_image_style"
         android:title="@string/image_style"
         android:title="@string/image_style"

+ 11 - 0
app/src/main/res/values-es-rES/strings.xml

@@ -1010,6 +1010,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">Review</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">show read title addition area</string>
     <string name="show_read_title_addition">show read title addition area</string>

+ 11 - 0
app/src/main/res/values-ja-rJP/strings.xml

@@ -1013,6 +1013,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">Review</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">show read title addition area</string>
     <string name="show_read_title_addition">show read title addition area</string>

+ 11 - 0
app/src/main/res/values-pt-rBR/strings.xml

@@ -1013,6 +1013,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">Review</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">show read title addition area</string>
     <string name="show_read_title_addition">show read title addition area</string>

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

@@ -1010,6 +1010,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">段评</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

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

@@ -1012,6 +1012,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">段评</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

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

@@ -1012,6 +1012,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">段评</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>
     <string name="show_read_title_addition">展示顶部工具栏附加区域</string>

+ 18 - 18
app/src/main/res/values/non_translat.xml

@@ -1,25 +1,25 @@
 <?xml version="1.0" encoding="utf-8"?>
 <?xml version="1.0" encoding="utf-8"?>
 <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
 <resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="MissingTranslation">
-    <string name="pk_bookshelf_px">bookshelf_px</string>
-    <string name="legado_gzh">开源阅读</string>
-    <string name="email">kunfei.ge@gmail.com</string>
+    <string name="pk_bookshelf_px" translatable="false">bookshelf_px</string>
+    <string name="legado_gzh" translatable="false">开源阅读</string>
+    <string name="email" translatable="false">kunfei.ge@gmail.com</string>
 
 
-    <string name="source_rule_url">https://alanskycn.gitee.io/teachme/</string>
-    <string name="this_github_url">https://github.com/gedoor/legado</string>
-    <string name="contributors_url">https://github.com/gedoor/legado/graphs/contributors</string>
-    <string name="home_page_url">https://gedoor.github.io</string>
-    <string name="license_url">https://github.com/gedoor/legado/blob/master/LICENSE</string>
-    <string name="latest_release_url">https://github.com/gedoor/legado/releases/latest</string>
-    <string name="latest_release_api">https://api.github.com/repos/gedoor/legado/releases/latest</string>
-    <string name="tg_url">https://t.me/legado_channels</string>
-    <string name="discord_url">https://discord.gg/qDE52P5xGW</string>
+    <string name="source_rule_url" translatable="false">https://alanskycn.gitee.io/teachme/</string>
+    <string name="this_github_url" translatable="false">https://github.com/gedoor/legado</string>
+    <string name="contributors_url" translatable="false">https://github.com/gedoor/legado/graphs/contributors</string>
+    <string name="home_page_url" translatable="false">https://gedoor.github.io</string>
+    <string name="license_url" translatable="false">https://github.com/gedoor/legado/blob/master/LICENSE</string>
+    <string name="latest_release_url" translatable="false">https://github.com/gedoor/legado/releases/latest</string>
+    <string name="latest_release_api" translatable="false">https://api.github.com/repos/gedoor/legado/releases/latest</string>
+    <string name="tg_url" translatable="false">https://t.me/legado_channels</string>
+    <string name="discord_url" translatable="false">https://discord.gg/qDE52P5xGW</string>
 
 
-    <string name="http_ip">http://%1$s:%2$d</string>
-    <string name="git_hub">GitHub</string>
-    <string name="diy_edit_source_group_title">【%s】</string>
-    <string name="vip_title">🔒%s</string>
-    <string name="payed_title">🔓%s</string>
+    <string name="http_ip" translatable="false">http://%1$s:%2$d</string>
+    <string name="git_hub" translatable="false">GitHub</string>
+    <string name="diy_edit_source_group_title" translatable="false">【%s】</string>
+    <string name="vip_title" translatable="false">🔒%s</string>
+    <string name="payed_title" translatable="false">🔓%s</string>
 
 
-    <string name="separator">丨</string>
+    <string name="separator" translatable="false">丨</string>
 
 
 </resources>
 </resources>

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

@@ -1013,6 +1013,17 @@
     <string name="check_selected_interval">选中所选区间</string>
     <string name="check_selected_interval">选中所选区间</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_title">返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
     <string name="show_add_to_shelf_alert_summary">阅读未放入书架的书籍在返回时提示放入书架</string>
+    <string name="review">Review</string>
+    <string name="rule_avatar">段评发布者头像(avatarRule)</string>
+    <string name="rule_review_url">段评URL(reviewUrl)</string>
+    <string name="rule_review_content">段评内容(contentRule)</string>
+    <string name="rule_post_time">段评发布时间(postTimeRule)</string>
+    <string name="rule_review_quote">段评回复URL(reviewQuoteUrl)</string>
+    <string name="review_vote_down">点踩URL(voteUpUrl)</string>
+    <string name="review_vote_up">点赞URL(voteDownUrl)</string>
+    <string name="post_review_url">发送回复URL(postReviewUrl)</string>
+    <string name="post_quote_url">发送回复段评URL(postQuoteUrl)</string>
+    <string name="delete_review_url">删除段评URL(deleteUrl)</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_enabled">标志:发现已启用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="tag_explore_disabled">标志:发现已禁用</string>
     <string name="show_read_title_addition">show read title addition area</string>
     <string name="show_read_title_addition">show read title addition area</string>