mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
259 lines
13 KiB
Swift
259 lines
13 KiB
Swift
import Foundation
|
|
|
|
public struct BookSource: Codable, Hashable, Identifiable, Sendable {
|
|
public var bookSourceUrl: String
|
|
public var bookSourceName: String
|
|
public var bookSourceGroup: String?
|
|
public var bookSourceType: Int
|
|
public var bookUrlPattern: String?
|
|
public var customOrder: Int
|
|
public var enabled: Bool
|
|
public var enabledExplore: Bool
|
|
public var jsLib: String?
|
|
public var enabledCookieJar: Bool?
|
|
public var concurrentRate: String?
|
|
public var header: String?
|
|
public var loginUrl: String?
|
|
public var loginUi: String?
|
|
public var loginCheckJs: String?
|
|
public var coverDecodeJs: String?
|
|
public var bookSourceComment: String?
|
|
public var variableComment: String?
|
|
public var lastUpdateTime: Int64
|
|
public var respondTime: Int64
|
|
public var weight: Int
|
|
public var exploreUrl: String?
|
|
public var exploreScreen: String?
|
|
public var ruleExplore: ExploreRule?
|
|
public var searchUrl: String?
|
|
public var ruleSearch: SearchRule?
|
|
public var ruleBookInfo: BookInfoRule?
|
|
public var ruleToc: TocRule?
|
|
public var ruleContent: ContentRule?
|
|
public var ruleReview: ReviewRule?
|
|
public var eventListener: Bool
|
|
public var customButton: Bool
|
|
|
|
public var additionalFields: [String: JSONValue] = [:]
|
|
|
|
public var id: String { bookSourceUrl }
|
|
|
|
public init(bookSourceUrl: String, bookSourceName: String) {
|
|
self.bookSourceUrl = bookSourceUrl
|
|
self.bookSourceName = bookSourceName
|
|
bookSourceGroup = nil
|
|
bookSourceType = 0
|
|
bookUrlPattern = nil
|
|
customOrder = 0
|
|
enabled = true
|
|
enabledExplore = true
|
|
jsLib = nil
|
|
enabledCookieJar = true
|
|
concurrentRate = nil
|
|
header = nil
|
|
loginUrl = nil
|
|
loginUi = nil
|
|
loginCheckJs = nil
|
|
coverDecodeJs = nil
|
|
bookSourceComment = nil
|
|
variableComment = nil
|
|
lastUpdateTime = 0
|
|
respondTime = 180_000
|
|
weight = 0
|
|
exploreUrl = nil
|
|
exploreScreen = nil
|
|
ruleExplore = nil
|
|
searchUrl = nil
|
|
ruleSearch = nil
|
|
ruleBookInfo = nil
|
|
ruleToc = nil
|
|
ruleContent = nil
|
|
ruleReview = nil
|
|
eventListener = false
|
|
customButton = false
|
|
}
|
|
|
|
private enum CodingKeys: String, CodingKey, CaseIterable {
|
|
case bookSourceUrl, bookSourceName, bookSourceGroup, bookSourceType, bookUrlPattern, customOrder, enabled, enabledExplore, jsLib, enabledCookieJar, concurrentRate, header, loginUrl, loginUi, loginCheckJs, coverDecodeJs, bookSourceComment, variableComment, lastUpdateTime, respondTime, weight, exploreUrl, exploreScreen, ruleExplore, searchUrl, ruleSearch, ruleBookInfo, ruleToc, ruleContent, ruleReview, eventListener, customButton
|
|
}
|
|
|
|
public init(from decoder: Decoder) throws {
|
|
let values = try decoder.container(keyedBy: CodingKeys.self)
|
|
bookSourceUrl = try values.decodeIfPresent(String.self, forKey: .bookSourceUrl) ?? ""
|
|
bookSourceName = try values.decodeIfPresent(String.self, forKey: .bookSourceName) ?? ""
|
|
bookSourceGroup = try values.decodeIfPresent(String.self, forKey: .bookSourceGroup)
|
|
bookSourceType = try values.decodeLossyInt(forKey: .bookSourceType) ?? 0
|
|
bookUrlPattern = try values.decodeIfPresent(String.self, forKey: .bookUrlPattern)
|
|
customOrder = try values.decodeLossyInt(forKey: .customOrder) ?? 0
|
|
enabled = try values.decodeLossyBool(forKey: .enabled) ?? true
|
|
enabledExplore = try values.decodeLossyBool(forKey: .enabledExplore) ?? true
|
|
jsLib = try values.decodeIfPresent(String.self, forKey: .jsLib)
|
|
enabledCookieJar = values.contains(.enabledCookieJar)
|
|
? try values.decodeLossyBool(forKey: .enabledCookieJar)
|
|
: true
|
|
concurrentRate = try values.decodeIfPresent(String.self, forKey: .concurrentRate)
|
|
header = try values.decodeIfPresent(String.self, forKey: .header)
|
|
loginUrl = try values.decodeIfPresent(String.self, forKey: .loginUrl)
|
|
loginUi = try values.decodeIfPresent(String.self, forKey: .loginUi)
|
|
loginCheckJs = try values.decodeIfPresent(String.self, forKey: .loginCheckJs)
|
|
coverDecodeJs = try values.decodeIfPresent(String.self, forKey: .coverDecodeJs)
|
|
bookSourceComment = try values.decodeIfPresent(String.self, forKey: .bookSourceComment)
|
|
variableComment = try values.decodeIfPresent(String.self, forKey: .variableComment)
|
|
lastUpdateTime = try values.decodeLossyInt64(forKey: .lastUpdateTime) ?? 0
|
|
respondTime = try values.decodeLossyInt64(forKey: .respondTime) ?? 180_000
|
|
weight = try values.decodeLossyInt(forKey: .weight) ?? 0
|
|
exploreUrl = try values.decodeIfPresent(String.self, forKey: .exploreUrl)
|
|
exploreScreen = try values.decodeIfPresent(String.self, forKey: .exploreScreen)
|
|
ruleExplore = try values.decodeFlexible(ExploreRule.self, forKey: .ruleExplore)
|
|
searchUrl = try values.decodeIfPresent(String.self, forKey: .searchUrl)
|
|
ruleSearch = try values.decodeFlexible(SearchRule.self, forKey: .ruleSearch)
|
|
ruleBookInfo = try values.decodeFlexible(BookInfoRule.self, forKey: .ruleBookInfo)
|
|
ruleToc = try values.decodeFlexible(TocRule.self, forKey: .ruleToc)
|
|
ruleContent = try values.decodeFlexible(ContentRule.self, forKey: .ruleContent)
|
|
ruleReview = try values.decodeFlexible(ReviewRule.self, forKey: .ruleReview)
|
|
eventListener = try values.decodeLossyBool(forKey: .eventListener) ?? false
|
|
customButton = try values.decodeLossyBool(forKey: .customButton) ?? false
|
|
if case .object(var raw) = try JSONValue(from: decoder) {
|
|
for key in CodingKeys.allCases {
|
|
if BookSourceEditorDocument.ruleKeys.contains(key.rawValue) {
|
|
var nested: [String: JSONValue] = [:]
|
|
var value = raw[key.rawValue]
|
|
if case .string(let text) = value { value = try? JSONValue.decode(data: Data(text.utf8)) }
|
|
if case .object(let object) = value { nested = object }
|
|
for field in BookSourceEditorSchema.sections.flatMap(\.fields) where field.path.hasPrefix(key.rawValue + ".") {
|
|
nested.removeValue(forKey: String(field.path.split(separator: ".")[1]))
|
|
}
|
|
raw[key.rawValue] = nested.isEmpty ? nil : .object(nested)
|
|
} else { raw.removeValue(forKey: key.rawValue) }
|
|
}
|
|
additionalFields = raw
|
|
}
|
|
}
|
|
public func encode(to encoder: Encoder) throws {
|
|
var values = encoder.container(keyedBy: SourceCodingKey.self)
|
|
for (key, value) in additionalFields { try values.encode(value, forKey: SourceCodingKey(key)) }
|
|
try values.encode(bookSourceUrl, forKey: SourceCodingKey("bookSourceUrl"))
|
|
try values.encode(bookSourceName, forKey: SourceCodingKey("bookSourceName"))
|
|
try values.encodeIfPresent(bookSourceGroup, forKey: SourceCodingKey("bookSourceGroup"))
|
|
try values.encode(bookSourceType, forKey: SourceCodingKey("bookSourceType"))
|
|
try values.encodeIfPresent(bookUrlPattern, forKey: SourceCodingKey("bookUrlPattern"))
|
|
try values.encode(customOrder, forKey: SourceCodingKey("customOrder"))
|
|
try values.encode(enabled, forKey: SourceCodingKey("enabled"))
|
|
try values.encode(enabledExplore, forKey: SourceCodingKey("enabledExplore"))
|
|
try values.encodeIfPresent(jsLib, forKey: SourceCodingKey("jsLib"))
|
|
try values.encode(enabledCookieJar, forKey: SourceCodingKey("enabledCookieJar"))
|
|
try values.encodeIfPresent(concurrentRate, forKey: SourceCodingKey("concurrentRate"))
|
|
try values.encodeIfPresent(header, forKey: SourceCodingKey("header"))
|
|
try values.encodeIfPresent(loginUrl, forKey: SourceCodingKey("loginUrl"))
|
|
try values.encodeIfPresent(loginUi, forKey: SourceCodingKey("loginUi"))
|
|
try values.encodeIfPresent(loginCheckJs, forKey: SourceCodingKey("loginCheckJs"))
|
|
try values.encodeIfPresent(coverDecodeJs, forKey: SourceCodingKey("coverDecodeJs"))
|
|
try values.encodeIfPresent(bookSourceComment, forKey: SourceCodingKey("bookSourceComment"))
|
|
try values.encodeIfPresent(variableComment, forKey: SourceCodingKey("variableComment"))
|
|
try values.encode(lastUpdateTime, forKey: SourceCodingKey("lastUpdateTime"))
|
|
try values.encode(respondTime, forKey: SourceCodingKey("respondTime"))
|
|
try values.encode(weight, forKey: SourceCodingKey("weight"))
|
|
try values.encodeIfPresent(exploreUrl, forKey: SourceCodingKey("exploreUrl"))
|
|
try values.encodeIfPresent(exploreScreen, forKey: SourceCodingKey("exploreScreen"))
|
|
try encodeRule(ruleExplore, key: "ruleExplore", into: &values)
|
|
try values.encodeIfPresent(searchUrl, forKey: SourceCodingKey("searchUrl"))
|
|
try encodeRule(ruleSearch, key: "ruleSearch", into: &values)
|
|
try encodeRule(ruleBookInfo, key: "ruleBookInfo", into: &values)
|
|
try encodeRule(ruleToc, key: "ruleToc", into: &values)
|
|
try encodeRule(ruleContent, key: "ruleContent", into: &values)
|
|
try encodeRule(ruleReview, key: "ruleReview", into: &values)
|
|
try values.encode(eventListener, forKey: SourceCodingKey("eventListener"))
|
|
try values.encode(customButton, forKey: SourceCodingKey("customButton"))
|
|
}
|
|
|
|
private func encodeRule<T: Encodable>(_ rule: T?, key: String, into values: inout KeyedEncodingContainer<SourceCodingKey>) throws {
|
|
var object: [String: JSONValue] = [:]
|
|
if case .object(let extra) = additionalFields[key] { object = extra }
|
|
if let rule, case .object(let known) = try JSONValue.decode(data: JSONEncoder().encode(rule)) {
|
|
object.merge(known) { _, new in new }
|
|
}
|
|
if rule != nil || !object.isEmpty { try values.encode(object, forKey: SourceCodingKey(key)) }
|
|
}
|
|
|
|
}
|
|
|
|
public extension BookSource {
|
|
var groups: [String] {
|
|
Self.parseGroups(bookSourceGroup)
|
|
}
|
|
|
|
func hasGroup(_ group: String) -> Bool {
|
|
groups.contains(group.trimmingCharacters(in: .whitespacesAndNewlines))
|
|
}
|
|
|
|
mutating func addGroups(_ value: String) {
|
|
bookSourceGroup = Self.joinGroups(groups + Self.parseGroups(value))
|
|
}
|
|
|
|
mutating func removeGroups(_ value: String) {
|
|
let removed = Set(Self.parseGroups(value))
|
|
bookSourceGroup = Self.joinGroups(groups.filter { !removed.contains($0) })
|
|
}
|
|
|
|
mutating func replaceGroup(_ oldValue: String, with newValue: String?) {
|
|
let oldValue = oldValue.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
var values = groups.filter { $0 != oldValue }
|
|
if let newValue { values += Self.parseGroups(newValue) }
|
|
bookSourceGroup = Self.joinGroups(values)
|
|
}
|
|
|
|
static func parseGroups(_ value: String?) -> [String] {
|
|
guard let value else { return [] }
|
|
var seen = Set<String>()
|
|
return value
|
|
.components(separatedBy: CharacterSet(charactersIn: ",;,;"))
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty && seen.insert($0).inserted }
|
|
}
|
|
|
|
static func joinGroups(_ values: [String]) -> String? {
|
|
var seen = Set<String>()
|
|
let normalized = values
|
|
.map { $0.trimmingCharacters(in: .whitespacesAndNewlines) }
|
|
.filter { !$0.isEmpty && seen.insert($0).inserted }
|
|
return normalized.isEmpty ? nil : normalized.joined(separator: ",")
|
|
}
|
|
}
|
|
|
|
private extension KeyedDecodingContainer {
|
|
func decodeFlexible<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? {
|
|
guard contains(key), try !decodeNil(forKey: key) else { return nil }
|
|
if let value = try? decode(T.self, forKey: key) {
|
|
return value
|
|
}
|
|
let json = try decode(String.self, forKey: key)
|
|
guard !json.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty,
|
|
let data = json.data(using: .utf8) else {
|
|
return nil
|
|
}
|
|
return try JSONDecoder().decode(T.self, from: data)
|
|
}
|
|
|
|
func decodeLossyBool(forKey key: Key) throws -> Bool? {
|
|
guard contains(key), try !decodeNil(forKey: key) else { return nil }
|
|
if let value = try? decode(Bool.self, forKey: key) { return value }
|
|
if let value = try? decode(Int.self, forKey: key) { return value != 0 }
|
|
let value = try decode(String.self, forKey: key).lowercased()
|
|
if ["true", "1", "yes"].contains(value) { return true }
|
|
if ["false", "0", "no"].contains(value) { return false }
|
|
return nil
|
|
}
|
|
|
|
func decodeLossyInt(forKey key: Key) throws -> Int? {
|
|
guard contains(key), try !decodeNil(forKey: key) else { return nil }
|
|
if let value = try? decode(Int.self, forKey: key) { return value }
|
|
return Int(try decode(String.self, forKey: key))
|
|
}
|
|
|
|
func decodeLossyInt64(forKey key: Key) throws -> Int64? {
|
|
guard contains(key), try !decodeNil(forKey: key) else { return nil }
|
|
if let value = try? decode(Int64.self, forKey: key) { return value }
|
|
return Int64(try decode(String.self, forKey: key))
|
|
}
|
|
}
|