mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
475 lines
20 KiB
Swift
475 lines
20 KiB
Swift
import Foundation
|
|
|
|
public struct Book: Codable, Hashable, Identifiable, Sendable {
|
|
public var bookUrl: String
|
|
public var tocUrl: String
|
|
public var origin: String
|
|
public var originName: String
|
|
public var name: String
|
|
public var author: String
|
|
public var kind: String?
|
|
public var customTag: String?
|
|
public var coverUrl: String?
|
|
public var customCoverUrl: String?
|
|
public var intro: String?
|
|
public var customIntro: String?
|
|
public var charset: String?
|
|
public var type: Int
|
|
public var group: Int64
|
|
public var latestChapterTitle: String?
|
|
public var latestChapterTime: Int64
|
|
public var lastCheckTime: Int64
|
|
public var lastCheckCount: Int
|
|
public var totalChapterNum: Int
|
|
public var durChapterTitle: String?
|
|
public var durChapterIndex: Int
|
|
public var durVolumeIndex: Int
|
|
public var chapterInVolumeIndex: Int
|
|
public var durChapterPos: Int
|
|
public var durChapterTime: Int64
|
|
public var wordCount: String?
|
|
public var canUpdate: Bool
|
|
public var order: Int
|
|
public var originOrder: Int
|
|
public var variable: String?
|
|
public var readConfig: BookReadConfig?
|
|
public var syncTime: Int64
|
|
|
|
public var id: String { bookUrl }
|
|
|
|
public init(bookUrl: String, name: String, author: String = "", origin: String = "local") {
|
|
self.bookUrl = bookUrl
|
|
tocUrl = ""
|
|
self.origin = origin
|
|
originName = ""
|
|
self.name = name
|
|
self.author = author
|
|
kind = nil
|
|
customTag = nil
|
|
coverUrl = nil
|
|
customCoverUrl = nil
|
|
intro = nil
|
|
customIntro = nil
|
|
charset = nil
|
|
type = 0
|
|
group = 0
|
|
latestChapterTitle = nil
|
|
latestChapterTime = 0
|
|
lastCheckTime = 0
|
|
lastCheckCount = 0
|
|
totalChapterNum = 0
|
|
durChapterTitle = nil
|
|
durChapterIndex = 0
|
|
durVolumeIndex = 0
|
|
chapterInVolumeIndex = 0
|
|
durChapterPos = 0
|
|
durChapterTime = 0
|
|
wordCount = nil
|
|
canUpdate = true
|
|
order = 0
|
|
originOrder = 0
|
|
variable = nil
|
|
readConfig = nil
|
|
syncTime = 0
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case bookUrl, tocUrl, origin, originName, name, author, kind, customTag, coverUrl, customCoverUrl
|
|
case intro, customIntro, charset, type, group, latestChapterTitle, latestChapterTime, lastCheckTime
|
|
case lastCheckCount, totalChapterNum, durChapterTitle, durChapterIndex, durVolumeIndex
|
|
case chapterInVolumeIndex, durChapterPos, durChapterTime, wordCount, canUpdate, order, originOrder
|
|
case variable, readConfig, syncTime
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let value = try decoder.container(keyedBy: CodingKeys.self)
|
|
bookUrl = try value.decodeIfPresent(String.self, forKey: .bookUrl) ?? ""
|
|
tocUrl = try value.decodeIfPresent(String.self, forKey: .tocUrl) ?? ""
|
|
origin = try value.decodeIfPresent(String.self, forKey: .origin) ?? "local"
|
|
originName = try value.decodeIfPresent(String.self, forKey: .originName) ?? ""
|
|
name = try value.decodeIfPresent(String.self, forKey: .name) ?? ""
|
|
author = try value.decodeIfPresent(String.self, forKey: .author) ?? ""
|
|
kind = try value.decodeIfPresent(String.self, forKey: .kind)
|
|
customTag = try value.decodeIfPresent(String.self, forKey: .customTag)
|
|
coverUrl = try value.decodeIfPresent(String.self, forKey: .coverUrl)
|
|
customCoverUrl = try value.decodeIfPresent(String.self, forKey: .customCoverUrl)
|
|
intro = try value.decodeIfPresent(String.self, forKey: .intro)
|
|
customIntro = try value.decodeIfPresent(String.self, forKey: .customIntro)
|
|
charset = try value.decodeIfPresent(String.self, forKey: .charset)
|
|
type = try value.decodeIfPresent(Int.self, forKey: .type) ?? 0
|
|
group = try value.decodeIfPresent(Int64.self, forKey: .group) ?? 0
|
|
latestChapterTitle = try value.decodeIfPresent(String.self, forKey: .latestChapterTitle)
|
|
latestChapterTime = try value.decodeIfPresent(Int64.self, forKey: .latestChapterTime) ?? 0
|
|
lastCheckTime = try value.decodeIfPresent(Int64.self, forKey: .lastCheckTime) ?? 0
|
|
lastCheckCount = try value.decodeIfPresent(Int.self, forKey: .lastCheckCount) ?? 0
|
|
totalChapterNum = try value.decodeIfPresent(Int.self, forKey: .totalChapterNum) ?? 0
|
|
durChapterTitle = try value.decodeIfPresent(String.self, forKey: .durChapterTitle)
|
|
durChapterIndex = try value.decodeIfPresent(Int.self, forKey: .durChapterIndex) ?? 0
|
|
durVolumeIndex = try value.decodeIfPresent(Int.self, forKey: .durVolumeIndex) ?? 0
|
|
chapterInVolumeIndex = try value.decodeIfPresent(Int.self, forKey: .chapterInVolumeIndex) ?? 0
|
|
durChapterPos = try value.decodeIfPresent(Int.self, forKey: .durChapterPos) ?? 0
|
|
durChapterTime = try value.decodeIfPresent(Int64.self, forKey: .durChapterTime) ?? 0
|
|
wordCount = try value.decodeIfPresent(String.self, forKey: .wordCount)
|
|
canUpdate = try value.decodeIfPresent(Bool.self, forKey: .canUpdate) ?? true
|
|
order = try value.decodeIfPresent(Int.self, forKey: .order) ?? 0
|
|
originOrder = try value.decodeIfPresent(Int.self, forKey: .originOrder) ?? 0
|
|
variable = try value.decodeIfPresent(String.self, forKey: .variable)
|
|
readConfig = try value.decodeIfPresent(BookReadConfig.self, forKey: .readConfig)
|
|
syncTime = try value.decodeIfPresent(Int64.self, forKey: .syncTime) ?? 0
|
|
}
|
|
}
|
|
|
|
public struct BookReadConfig: Codable, Hashable, Sendable {
|
|
public var reverseToc = false
|
|
public var pageAnim: Int?
|
|
public var reSegment = false
|
|
public var imageStyle: String?
|
|
public var useReplaceRule: Bool?
|
|
public var delTag: Int64 = 0
|
|
public var ttsEngine: String?
|
|
public var splitLongChapter = true
|
|
public var readSimulating = false
|
|
public var startDate: String?
|
|
public var startChapter: Int?
|
|
public var dailyChapters = 3
|
|
public var openCredits = 0
|
|
public var closeCredits = 0
|
|
public var playMode = 0
|
|
public var playSpeed: Float = 1
|
|
|
|
public init() {}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case reverseToc, pageAnim, reSegment, imageStyle, useReplaceRule, delTag, ttsEngine
|
|
case splitLongChapter, readSimulating, startDate, startChapter, dailyChapters
|
|
case openCredits, closeCredits, playMode, playSpeed
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let value = try decoder.container(keyedBy: CodingKeys.self)
|
|
reverseToc = try value.decodeIfPresent(Bool.self, forKey: .reverseToc) ?? false
|
|
pageAnim = try value.decodeIfPresent(Int.self, forKey: .pageAnim)
|
|
reSegment = try value.decodeIfPresent(Bool.self, forKey: .reSegment) ?? false
|
|
imageStyle = try value.decodeIfPresent(String.self, forKey: .imageStyle)
|
|
useReplaceRule = try value.decodeIfPresent(Bool.self, forKey: .useReplaceRule)
|
|
delTag = try value.decodeIfPresent(Int64.self, forKey: .delTag) ?? 0
|
|
ttsEngine = try value.decodeIfPresent(String.self, forKey: .ttsEngine)
|
|
splitLongChapter = try value.decodeIfPresent(Bool.self, forKey: .splitLongChapter) ?? true
|
|
readSimulating = try value.decodeIfPresent(Bool.self, forKey: .readSimulating) ?? false
|
|
startDate = try value.decodeIfPresent(String.self, forKey: .startDate)
|
|
startChapter = try value.decodeIfPresent(Int.self, forKey: .startChapter)
|
|
dailyChapters = try value.decodeIfPresent(Int.self, forKey: .dailyChapters) ?? 3
|
|
openCredits = try value.decodeIfPresent(Int.self, forKey: .openCredits) ?? 0
|
|
closeCredits = try value.decodeIfPresent(Int.self, forKey: .closeCredits) ?? 0
|
|
playMode = try value.decodeIfPresent(Int.self, forKey: .playMode) ?? 0
|
|
playSpeed = try value.decodeIfPresent(Float.self, forKey: .playSpeed) ?? 1
|
|
}
|
|
}
|
|
|
|
public struct BookGroup: Codable, Hashable, Identifiable, Sendable {
|
|
public static let idRoot: Int64 = -100
|
|
public static let idAll: Int64 = -1
|
|
public static let idLocal: Int64 = -2
|
|
public static let idAudio: Int64 = -3
|
|
public static let idNetNone: Int64 = -4
|
|
public static let idLocalNone: Int64 = -5
|
|
public static let idVideo: Int64 = -6
|
|
public static let idError: Int64 = -11
|
|
|
|
public var groupId: Int64
|
|
public var groupName: String
|
|
public var cover: String?
|
|
public var order: Int
|
|
public var enableRefresh: Bool
|
|
public var show: Bool
|
|
public var bookSort: Int
|
|
public var onlyUpdateRead: Bool
|
|
|
|
public var id: Int64 { groupId }
|
|
|
|
public init(
|
|
groupId: Int64,
|
|
groupName: String,
|
|
cover: String? = nil,
|
|
order: Int = 0,
|
|
enableRefresh: Bool = true,
|
|
show: Bool = true,
|
|
bookSort: Int = -1,
|
|
onlyUpdateRead: Bool = false
|
|
) {
|
|
self.groupId = groupId
|
|
self.groupName = groupName
|
|
self.cover = cover
|
|
self.order = order
|
|
self.enableRefresh = enableRefresh
|
|
self.show = show
|
|
self.bookSort = bookSort
|
|
self.onlyUpdateRead = onlyUpdateRead
|
|
}
|
|
}
|
|
|
|
public struct BookChapter: Codable, Hashable, Identifiable, Sendable {
|
|
public var url: String
|
|
public var title: String
|
|
public var isVolume: Bool
|
|
public var baseUrl: String
|
|
public var bookUrl: String
|
|
public var index: Int
|
|
public var isVip: Bool
|
|
public var isPay: Bool
|
|
public var resourceUrl: String?
|
|
public var tag: String?
|
|
public var wordCount: String?
|
|
public var start: Int64?
|
|
public var end: Int64?
|
|
public var startFragmentId: String?
|
|
public var endFragmentId: String?
|
|
public var variable: String?
|
|
public var imageURL: String?
|
|
|
|
public var id: String { "\(bookUrl)\u{0}\(url)" }
|
|
|
|
public init(url: String, title: String, bookUrl: String, index: Int) {
|
|
self.url = url
|
|
self.title = title
|
|
isVolume = false
|
|
baseUrl = ""
|
|
self.bookUrl = bookUrl
|
|
self.index = index
|
|
isVip = false
|
|
isPay = false
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case url, title, isVolume, baseUrl, bookUrl, index, isVip, isPay, resourceUrl, tag, wordCount
|
|
case start, end, startFragmentId, endFragmentId, variable, imgUrl, imageURL
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let value = try decoder.container(keyedBy: CodingKeys.self)
|
|
url = try value.decodeIfPresent(String.self, forKey: .url) ?? ""
|
|
title = try value.decodeIfPresent(String.self, forKey: .title) ?? ""
|
|
isVolume = try value.decodeIfPresent(Bool.self, forKey: .isVolume) ?? false
|
|
baseUrl = try value.decodeIfPresent(String.self, forKey: .baseUrl) ?? ""
|
|
bookUrl = try value.decodeIfPresent(String.self, forKey: .bookUrl) ?? ""
|
|
index = try value.decodeIfPresent(Int.self, forKey: .index) ?? 0
|
|
isVip = try value.decodeIfPresent(Bool.self, forKey: .isVip) ?? false
|
|
isPay = try value.decodeIfPresent(Bool.self, forKey: .isPay) ?? false
|
|
resourceUrl = try value.decodeIfPresent(String.self, forKey: .resourceUrl)
|
|
tag = try value.decodeIfPresent(String.self, forKey: .tag)
|
|
wordCount = try value.decodeIfPresent(String.self, forKey: .wordCount)
|
|
start = try value.decodeIfPresent(Int64.self, forKey: .start)
|
|
end = try value.decodeIfPresent(Int64.self, forKey: .end)
|
|
startFragmentId = try value.decodeIfPresent(String.self, forKey: .startFragmentId)
|
|
endFragmentId = try value.decodeIfPresent(String.self, forKey: .endFragmentId)
|
|
variable = try value.decodeIfPresent(String.self, forKey: .variable)
|
|
imageURL = try value.decodeIfPresent(String.self, forKey: .imgUrl)
|
|
?? value.decodeIfPresent(String.self, forKey: .imageURL)
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var value = encoder.container(keyedBy: CodingKeys.self)
|
|
try value.encode(url, forKey: .url)
|
|
try value.encode(title, forKey: .title)
|
|
try value.encode(isVolume, forKey: .isVolume)
|
|
try value.encode(baseUrl, forKey: .baseUrl)
|
|
try value.encode(bookUrl, forKey: .bookUrl)
|
|
try value.encode(index, forKey: .index)
|
|
try value.encode(isVip, forKey: .isVip)
|
|
try value.encode(isPay, forKey: .isPay)
|
|
try value.encodeIfPresent(resourceUrl, forKey: .resourceUrl)
|
|
try value.encodeIfPresent(tag, forKey: .tag)
|
|
try value.encodeIfPresent(wordCount, forKey: .wordCount)
|
|
try value.encodeIfPresent(start, forKey: .start)
|
|
try value.encodeIfPresent(end, forKey: .end)
|
|
try value.encodeIfPresent(startFragmentId, forKey: .startFragmentId)
|
|
try value.encodeIfPresent(endFragmentId, forKey: .endFragmentId)
|
|
try value.encodeIfPresent(variable, forKey: .variable)
|
|
try value.encodeIfPresent(imageURL, forKey: .imgUrl)
|
|
}
|
|
}
|
|
|
|
public struct Bookmark: Codable, Hashable, Identifiable, Sendable {
|
|
public var time: Int64
|
|
public var bookName: String
|
|
public var bookAuthor: String
|
|
public var chapterIndex: Int
|
|
public var chapterPosition: Int
|
|
public var chapterName: String
|
|
public var bookText: String
|
|
public var content: String
|
|
|
|
public var id: Int64 { time }
|
|
|
|
public init(time: Int64, bookName: String, bookAuthor: String = "") {
|
|
self.time = time
|
|
self.bookName = bookName
|
|
self.bookAuthor = bookAuthor
|
|
chapterIndex = 0
|
|
chapterPosition = 0
|
|
chapterName = ""
|
|
bookText = ""
|
|
content = ""
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case time, bookName, bookAuthor, chapterIndex, chapterPos, chapterPosition
|
|
case chapterName, bookText, content
|
|
}
|
|
|
|
public func resolvedChapterIndex(in chapters: [BookChapter]) -> Int? {
|
|
if !chapterName.isEmpty,
|
|
let index = chapters.firstIndex(where: { !$0.isVolume && $0.title == chapterName }) {
|
|
return index
|
|
}
|
|
return chapters.firstIndex(where: { !$0.isVolume && $0.index == chapterIndex })
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
time = try values.decodeIfPresent(Int64.self, forKey: .time) ?? Int64(Date().timeIntervalSince1970 * 1_000)
|
|
bookName = try values.decodeIfPresent(String.self, forKey: .bookName) ?? ""
|
|
bookAuthor = try values.decodeIfPresent(String.self, forKey: .bookAuthor) ?? ""
|
|
chapterIndex = try values.decodeIfPresent(Int.self, forKey: .chapterIndex) ?? 0
|
|
chapterPosition = try values.decodeIfPresent(Int.self, forKey: .chapterPos)
|
|
?? values.decodeIfPresent(Int.self, forKey: .chapterPosition)
|
|
?? 0
|
|
chapterName = try values.decodeIfPresent(String.self, forKey: .chapterName) ?? ""
|
|
bookText = try values.decodeIfPresent(String.self, forKey: .bookText) ?? ""
|
|
content = try values.decodeIfPresent(String.self, forKey: .content) ?? ""
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var values = encoder.container(keyedBy: CodingKeys.self)
|
|
try values.encode(time, forKey: .time)
|
|
try values.encode(bookName, forKey: .bookName)
|
|
try values.encode(bookAuthor, forKey: .bookAuthor)
|
|
try values.encode(chapterIndex, forKey: .chapterIndex)
|
|
try values.encode(chapterPosition, forKey: .chapterPos)
|
|
try values.encode(chapterName, forKey: .chapterName)
|
|
try values.encode(bookText, forKey: .bookText)
|
|
try values.encode(content, forKey: .content)
|
|
}
|
|
}
|
|
|
|
public struct ReadRecord: Codable, Hashable, Identifiable, Sendable {
|
|
public var deviceId: String
|
|
public var bookName: String
|
|
public var readTime: Int64
|
|
public var lastRead: Int64
|
|
public var dailyReadTimes: [String: Int64]
|
|
|
|
public var id: String { "\(deviceId)\u{0}\(bookName)" }
|
|
|
|
public init(
|
|
deviceId: String,
|
|
bookName: String,
|
|
readTime: Int64 = 0,
|
|
lastRead: Int64? = nil,
|
|
dailyReadTimes: [String: Int64] = [:]
|
|
) {
|
|
self.deviceId = deviceId
|
|
self.bookName = bookName
|
|
self.readTime = readTime
|
|
self.lastRead = lastRead ?? Int64(Date().timeIntervalSince1970 * 1_000)
|
|
self.dailyReadTimes = dailyReadTimes
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case deviceId, bookName, readTime, lastRead, dailyReadTimes
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
deviceId = try values.decodeIfPresent(String.self, forKey: .deviceId) ?? ""
|
|
bookName = try values.decodeIfPresent(String.self, forKey: .bookName) ?? ""
|
|
readTime = try values.decodeIfPresent(Int64.self, forKey: .readTime) ?? 0
|
|
lastRead = try values.decodeIfPresent(Int64.self, forKey: .lastRead) ?? 0
|
|
dailyReadTimes = try values.decodeIfPresent([String: Int64].self, forKey: .dailyReadTimes) ?? [:]
|
|
}
|
|
}
|
|
|
|
public struct ReadRecordSummary: Hashable, Identifiable, Sendable {
|
|
public var bookName: String
|
|
public var readTime: Int64
|
|
public var lastRead: Int64
|
|
|
|
public var id: String { bookName }
|
|
|
|
public init(bookName: String, readTime: Int64, lastRead: Int64) {
|
|
self.bookName = bookName
|
|
self.readTime = readTime
|
|
self.lastRead = lastRead
|
|
}
|
|
}
|
|
|
|
public struct ReplaceRule: Codable, Hashable, Identifiable, Sendable {
|
|
public var id: Int64
|
|
public var name: String
|
|
public var group: String?
|
|
public var pattern: String
|
|
public var replacement: String
|
|
public var scope: String?
|
|
public var scopeTitle: Bool
|
|
public var scopeContent: Bool
|
|
public var excludeScope: String?
|
|
public var isEnabled: Bool
|
|
public var isRegex: Bool
|
|
public var timeoutMillisecond: Int64
|
|
public var sortOrder: Int
|
|
|
|
public init(id: Int64, name: String, pattern: String, replacement: String = "") {
|
|
self.id = id
|
|
self.name = name
|
|
self.pattern = pattern
|
|
self.replacement = replacement
|
|
scopeTitle = false
|
|
scopeContent = true
|
|
isEnabled = true
|
|
isRegex = true
|
|
timeoutMillisecond = 3_000
|
|
sortOrder = Int.min
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey {
|
|
case id, name, group, pattern, replacement, scope, scopeTitle, scopeContent
|
|
case excludeScope, isEnabled, isRegex, timeoutMillisecond, sortOrder, order
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
id = try values.decodeIfPresent(Int64.self, forKey: .id) ?? Int64(Date().timeIntervalSince1970 * 1_000)
|
|
name = try values.decodeIfPresent(String.self, forKey: .name) ?? ""
|
|
group = try values.decodeIfPresent(String.self, forKey: .group)
|
|
pattern = try values.decodeIfPresent(String.self, forKey: .pattern) ?? ""
|
|
replacement = try values.decodeIfPresent(String.self, forKey: .replacement) ?? ""
|
|
scope = try values.decodeIfPresent(String.self, forKey: .scope)
|
|
scopeTitle = try values.decodeIfPresent(Bool.self, forKey: .scopeTitle) ?? false
|
|
scopeContent = try values.decodeIfPresent(Bool.self, forKey: .scopeContent) ?? true
|
|
excludeScope = try values.decodeIfPresent(String.self, forKey: .excludeScope)
|
|
isEnabled = try values.decodeIfPresent(Bool.self, forKey: .isEnabled) ?? true
|
|
isRegex = try values.decodeIfPresent(Bool.self, forKey: .isRegex) ?? true
|
|
timeoutMillisecond = try values.decodeIfPresent(Int64.self, forKey: .timeoutMillisecond) ?? 3_000
|
|
sortOrder = try values.decodeIfPresent(Int.self, forKey: .sortOrder)
|
|
?? values.decodeIfPresent(Int.self, forKey: .order)
|
|
?? Int.min
|
|
}
|
|
|
|
public func encode(to encoder: Encoder) throws {
|
|
var values = encoder.container(keyedBy: CodingKeys.self)
|
|
try values.encode(id, forKey: .id)
|
|
try values.encode(name, forKey: .name)
|
|
try values.encodeIfPresent(group, forKey: .group)
|
|
try values.encode(pattern, forKey: .pattern)
|
|
try values.encode(replacement, forKey: .replacement)
|
|
try values.encodeIfPresent(scope, forKey: .scope)
|
|
try values.encode(scopeTitle, forKey: .scopeTitle)
|
|
try values.encode(scopeContent, forKey: .scopeContent)
|
|
try values.encodeIfPresent(excludeScope, forKey: .excludeScope)
|
|
try values.encode(isEnabled, forKey: .isEnabled)
|
|
try values.encode(isRegex, forKey: .isRegex)
|
|
try values.encode(timeoutMillisecond, forKey: .timeoutMillisecond)
|
|
try values.encode(sortOrder, forKey: .order)
|
|
}
|
|
}
|