Book.kt 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421
  1. package io.legato.kazusa.data.entities
  2. import android.os.Parcelable
  3. import androidx.room.ColumnInfo
  4. import androidx.room.Entity
  5. import androidx.room.Ignore
  6. import androidx.room.Index
  7. import androidx.room.PrimaryKey
  8. import androidx.room.TypeConverter
  9. import androidx.room.TypeConverters
  10. import io.legato.kazusa.constant.AppPattern
  11. import io.legato.kazusa.constant.BookType
  12. import io.legato.kazusa.constant.PageAnim
  13. import io.legato.kazusa.data.appDb
  14. import io.legato.kazusa.help.book.BookHelp
  15. import io.legato.kazusa.help.book.ContentProcessor
  16. import io.legato.kazusa.help.book.getFolderNameNoCache
  17. import io.legato.kazusa.help.book.isEpub
  18. import io.legato.kazusa.help.book.isImage
  19. import io.legato.kazusa.help.book.simulatedTotalChapterNum
  20. import io.legato.kazusa.help.config.AppConfig
  21. import io.legato.kazusa.help.config.ReadBookConfig
  22. import io.legato.kazusa.model.ReadBook
  23. import io.legato.kazusa.utils.GSON
  24. import io.legato.kazusa.utils.fromJsonObject
  25. import kotlinx.parcelize.IgnoredOnParcel
  26. import kotlinx.parcelize.Parcelize
  27. import java.nio.charset.Charset
  28. import java.time.LocalDate
  29. import kotlin.math.max
  30. @Parcelize
  31. @TypeConverters(Book.Converters::class)
  32. @Entity(
  33. tableName = "books",
  34. indices = [Index(value = ["name", "author"], unique = true)]
  35. )
  36. data class Book(
  37. // 详情页Url(本地书源存储完整文件路径)
  38. @PrimaryKey
  39. @ColumnInfo(defaultValue = "")
  40. override var bookUrl: String = "",
  41. // 目录页Url (toc=table of Contents)
  42. @ColumnInfo(defaultValue = "")
  43. var tocUrl: String = "",
  44. // 书源URL(默认BookType.local)
  45. @ColumnInfo(defaultValue = BookType.localTag)
  46. var origin: String = BookType.localTag,
  47. //书源名称 or 本地书籍文件名
  48. @ColumnInfo(defaultValue = "")
  49. var originName: String = "",
  50. // 书籍名称(书源获取)
  51. @ColumnInfo(defaultValue = "")
  52. override var name: String = "",
  53. // 作者名称(书源获取)
  54. @ColumnInfo(defaultValue = "")
  55. override var author: String = "",
  56. // 分类信息(书源获取)
  57. override var kind: String? = null,
  58. // 分类信息(用户修改)
  59. var customTag: String? = null,
  60. // 封面Url(书源获取)
  61. var coverUrl: String? = null,
  62. // 封面Url(用户修改)
  63. var customCoverUrl: String? = null,
  64. // 简介内容(书源获取)
  65. var intro: String? = null,
  66. // 简介内容(用户修改)
  67. var customIntro: String? = null,
  68. // 自定义字符集名称(仅适用于本地书籍)
  69. var charset: String? = null,
  70. // 类型,详见BookType
  71. @ColumnInfo(defaultValue = "0")
  72. var type: Int = BookType.text,
  73. // 自定义分组索引号
  74. @ColumnInfo(defaultValue = "0")
  75. var group: Long = 0,
  76. // 最新章节标题
  77. var latestChapterTitle: String? = null,
  78. // 最新章节标题更新时间
  79. @ColumnInfo(defaultValue = "0")
  80. var latestChapterTime: Long = System.currentTimeMillis(),
  81. // 最近一次更新书籍信息的时间
  82. @ColumnInfo(defaultValue = "0")
  83. var lastCheckTime: Long = System.currentTimeMillis(),
  84. // 最近一次发现新章节的数量
  85. @ColumnInfo(defaultValue = "0")
  86. var lastCheckCount: Int = 0,
  87. // 书籍目录总数
  88. @ColumnInfo(defaultValue = "0")
  89. var totalChapterNum: Int = 0,
  90. // 当前章节名称
  91. var durChapterTitle: String? = null,
  92. // 当前章节索引
  93. @ColumnInfo(defaultValue = "0")
  94. var durChapterIndex: Int = 0,
  95. // 当前阅读的进度(首行字符的索引位置)
  96. @ColumnInfo(defaultValue = "0")
  97. var durChapterPos: Int = 0,
  98. // 最近一次阅读书籍的时间(打开正文的时间)
  99. @ColumnInfo(defaultValue = "0")
  100. var durChapterTime: Long = System.currentTimeMillis(),
  101. //字数
  102. override var wordCount: String? = null,
  103. // 刷新书架时更新书籍信息
  104. @ColumnInfo(defaultValue = "1")
  105. var canUpdate: Boolean = true,
  106. // 手动排序
  107. @ColumnInfo(defaultValue = "0")
  108. var order: Int = 0,
  109. //书源排序
  110. @ColumnInfo(defaultValue = "0")
  111. var originOrder: Int = 0,
  112. // 自定义书籍变量信息(用于书源规则检索书籍信息)
  113. override var variable: String? = null,
  114. //阅读设置
  115. var readConfig: ReadConfig? = null,
  116. //同步时间
  117. @ColumnInfo(defaultValue = "0")
  118. var syncTime: Long = 0L
  119. ) : Parcelable, BaseBook {
  120. override fun equals(other: Any?): Boolean {
  121. if (other is Book) {
  122. return other.bookUrl == bookUrl
  123. }
  124. return false
  125. }
  126. override fun hashCode(): Int {
  127. return bookUrl.hashCode()
  128. }
  129. @delegate:Transient
  130. @delegate:Ignore
  131. @IgnoredOnParcel
  132. override val variableMap: HashMap<String, String> by lazy {
  133. GSON.fromJsonObject<HashMap<String, String>>(variable).getOrNull() ?: hashMapOf()
  134. }
  135. @Ignore
  136. @IgnoredOnParcel
  137. override var infoHtml: String? = null
  138. @Ignore
  139. @IgnoredOnParcel
  140. override var tocHtml: String? = null
  141. @Ignore
  142. @IgnoredOnParcel
  143. var downloadUrls: List<String>? = null
  144. @Ignore
  145. @IgnoredOnParcel
  146. private var folderName: String? = null
  147. @get:Ignore
  148. @IgnoredOnParcel
  149. val lastChapterIndex get() = totalChapterNum - 1
  150. fun getRealAuthor() = author.replace(AppPattern.authorRegex, "")
  151. fun getUnreadChapterNum() = max(simulatedTotalChapterNum() - durChapterIndex - 1, 0)
  152. fun getDisplayCover() = if (customCoverUrl.isNullOrEmpty()) coverUrl else customCoverUrl
  153. fun getDisplayIntro() = if (customIntro.isNullOrEmpty()) intro else customIntro
  154. //自定义简介有自动更新的需求时,可通过更新intro再调用upCustomIntro()完成
  155. @Suppress("unused")
  156. fun upCustomIntro() {
  157. customIntro = intro
  158. }
  159. fun fileCharset(): Charset {
  160. return charset(charset ?: "UTF-8")
  161. }
  162. @IgnoredOnParcel
  163. val config: ReadConfig
  164. get() {
  165. if (readConfig == null) {
  166. readConfig = ReadConfig()
  167. }
  168. return readConfig!!
  169. }
  170. fun setReverseToc(reverseToc: Boolean) {
  171. config.reverseToc = reverseToc
  172. }
  173. fun getReverseToc(): Boolean {
  174. return config.reverseToc
  175. }
  176. fun setUseReplaceRule(useReplaceRule: Boolean) {
  177. config.useReplaceRule = useReplaceRule
  178. }
  179. fun getUseReplaceRule(): Boolean {
  180. val useReplaceRule = config.useReplaceRule
  181. if (useReplaceRule != null) {
  182. return useReplaceRule
  183. }
  184. //图片类书源 epub本地 默认关闭净化
  185. if (isImage || isEpub) {
  186. return false
  187. }
  188. return AppConfig.replaceEnableDefault
  189. }
  190. fun setReSegment(reSegment: Boolean) {
  191. config.reSegment = reSegment
  192. }
  193. fun getReSegment(): Boolean {
  194. return config.reSegment
  195. }
  196. fun setPageAnim(pageAnim: Int?) {
  197. config.pageAnim = pageAnim
  198. }
  199. fun getPageAnim(): Int {
  200. var pageAnim = config.pageAnim
  201. ?: if (type and BookType.image > 0) PageAnim.scrollPageAnim else ReadBookConfig.pageAnim
  202. if (pageAnim < 0) {
  203. pageAnim = ReadBookConfig.pageAnim
  204. }
  205. return pageAnim
  206. }
  207. fun setImageStyle(imageStyle: String?) {
  208. config.imageStyle = imageStyle
  209. }
  210. fun getImageStyle(): String? {
  211. return config.imageStyle
  212. }
  213. fun setTtsEngine(ttsEngine: String?) {
  214. config.ttsEngine = ttsEngine
  215. }
  216. fun getTtsEngine(): String? {
  217. return config.ttsEngine
  218. }
  219. fun setSplitLongChapter(limitLongContent: Boolean) {
  220. config.splitLongChapter = limitLongContent
  221. }
  222. fun getSplitLongChapter(): Boolean {
  223. return config.splitLongChapter
  224. }
  225. // readSimulating 的 setter 和 getter
  226. fun setReadSimulating(readSimulating: Boolean) {
  227. config.readSimulating = readSimulating
  228. }
  229. fun getReadSimulating(): Boolean {
  230. return config.readSimulating
  231. }
  232. // startDate 的 setter 和 getter
  233. fun setStartDate(startDate: LocalDate?) {
  234. config.startDate = startDate
  235. }
  236. fun getStartDate(): LocalDate? {
  237. if (!config.readSimulating || config.startDate == null) {
  238. return LocalDate.now()
  239. }
  240. return config.startDate
  241. }
  242. // startChapter 的 setter 和 getter
  243. fun setStartChapter(startChapter: Int) {
  244. config.startChapter = startChapter
  245. }
  246. fun getStartChapter(): Int {
  247. if (config.readSimulating) return config.startChapter ?: 0
  248. return this.durChapterIndex
  249. }
  250. // dailyChapters 的 setter 和 getter
  251. fun setDailyChapters(dailyChapters: Int) {
  252. config.dailyChapters = dailyChapters
  253. }
  254. fun getDailyChapters(): Int {
  255. return config.dailyChapters
  256. }
  257. fun getDelTag(tag: Long): Boolean {
  258. return config.delTag and tag == tag
  259. }
  260. fun addDelTag(tag: Long) {
  261. config.delTag = config.delTag and tag
  262. }
  263. fun removeDelTag(tag: Long) {
  264. config.delTag = config.delTag and tag.inv()
  265. }
  266. fun getFolderName(): String {
  267. folderName?.let {
  268. return it
  269. }
  270. //防止书名过长,只取9位
  271. folderName = getFolderNameNoCache()
  272. return folderName!!
  273. }
  274. fun toSearchBook() = SearchBook(
  275. name = name,
  276. author = author,
  277. kind = kind,
  278. bookUrl = bookUrl,
  279. origin = origin,
  280. originName = originName,
  281. type = type,
  282. wordCount = wordCount,
  283. latestChapterTitle = latestChapterTitle,
  284. coverUrl = coverUrl,
  285. intro = intro,
  286. tocUrl = tocUrl,
  287. originOrder = originOrder,
  288. variable = variable
  289. ).apply {
  290. this.infoHtml = this@Book.infoHtml
  291. this.tocHtml = this@Book.tocHtml
  292. }
  293. /**
  294. * 迁移旧的书籍的一些信息到新的书籍中
  295. */
  296. fun migrateTo(newBook: Book, toc: List<BookChapter>): Book {
  297. newBook.durChapterIndex = BookHelp
  298. .getDurChapter(durChapterIndex, durChapterTitle, toc, totalChapterNum)
  299. newBook.durChapterTitle = toc[newBook.durChapterIndex].getDisplayTitle(
  300. ContentProcessor.get(newBook.name, newBook.origin).getTitleReplaceRules(),
  301. getUseReplaceRule()
  302. )
  303. newBook.durChapterPos = durChapterPos
  304. newBook.durChapterTime = durChapterTime
  305. newBook.group = group
  306. newBook.order = order
  307. newBook.customCoverUrl = customCoverUrl
  308. newBook.customIntro = customIntro
  309. newBook.customTag = customTag
  310. newBook.canUpdate = canUpdate
  311. newBook.readConfig = readConfig
  312. return newBook
  313. }
  314. fun createBookMark(): Bookmark {
  315. return Bookmark(
  316. bookName = name,
  317. bookAuthor = author,
  318. )
  319. }
  320. fun save() {
  321. if (appDb.bookDao.has(bookUrl)) {
  322. appDb.bookDao.update(this)
  323. } else {
  324. appDb.bookDao.insert(this)
  325. }
  326. }
  327. fun delete() {
  328. if (ReadBook.book?.bookUrl == bookUrl) {
  329. ReadBook.book = null
  330. }
  331. appDb.bookDao.delete(this)
  332. }
  333. @Suppress("ConstPropertyName")
  334. companion object {
  335. const val hTag = 2L
  336. const val rubyTag = 4L
  337. const val imgStyleDefault = "DEFAULT"
  338. const val imgStyleFull = "FULL"
  339. const val imgStyleText = "TEXT"
  340. const val imgStyleSingle = "SINGLE"
  341. }
  342. @Parcelize
  343. data class ReadConfig(
  344. var reverseToc: Boolean = false,
  345. var pageAnim: Int? = null,
  346. var reSegment: Boolean = false,
  347. var imageStyle: String? = null,
  348. var useReplaceRule: Boolean? = null,// 正文使用净化替换规则
  349. var delTag: Long = 0L,//去除标签
  350. var ttsEngine: String? = null,
  351. var splitLongChapter: Boolean = true,
  352. var readSimulating: Boolean = false,
  353. var startDate: LocalDate? = null,
  354. var startChapter: Int? = null, // 用户设置的起始章节
  355. var dailyChapters: Int = 3, // 用户设置的每日更新章节数
  356. val mangaColorFilter: String? = null,
  357. var mangaScrollMode: Int? = null,
  358. var webtoonSidePaddingDp: Int? = null
  359. ) : Parcelable
  360. class Converters {
  361. @TypeConverter
  362. fun readConfigToString(config: ReadConfig?): String = GSON.toJson(config)
  363. @TypeConverter
  364. fun stringToReadConfig(json: String?) = GSON.fromJsonObject<ReadConfig>(json).getOrNull()
  365. }
  366. }