mirror of
https://github.com/Aminark/legado-ios.git
synced 2026-10-20 19:21:22 +08:00
200 lines
11 KiB
Swift
200 lines
11 KiB
Swift
import Foundation
|
||
|
||
public struct BookSourceEditorField: Identifiable, Sendable {
|
||
public let path: String
|
||
public let title: String
|
||
public var id: String { path }
|
||
public var help: String {
|
||
switch path {
|
||
case "bookSourceUrl": return "填写书源站点地址,例如 https://example.com;它也是书源的唯一标识。"
|
||
case "bookSourceName": return "显示在书源列表中的名称,不能为空。"
|
||
case "bookSourceGroup": return "多个分组可用逗号或分号分隔。"
|
||
case "bookUrlPattern": return "匹配该书源的书籍详情 URL 的正则表达式,用于从网址添加书籍。"
|
||
case "concurrentRate": return "1000 表示请求间隔 1000 ms;1/1000 表示每 1000 ms 最多 1 次请求。"
|
||
case "header": return "填写请求头 JSON,或用 @js: / <js> 动态生成。User-Agent 为 null 可禁用默认 UA。"
|
||
case "jsLib": return "填写共享 JavaScript,或以 JSON 对象配置在线脚本库。"
|
||
case "searchUrl": return "用 {{key}} 表示关键词、{{page}} 表示页码;URL 后可用逗号附加请求选项 JSON。"
|
||
case "exploreUrl": return "填写 名称::地址,多项用换行或 && 分隔;也支持 JSON 分类数组和 JavaScript。"
|
||
case "loginUi": return "填写登录界面 JSON。"
|
||
case "ruleToc.formatJs": return "目录去重后处理标题,可使用 title、index(从 1 开始)和 gInt。"
|
||
case "ruleContent.nextContentUrl": return "提取当前章的下一分页地址,而非下一章。"
|
||
case "ruleContent.replaceRegex": return "在多页正文合并后执行的替换规则,使用 ## 分隔正则和替换内容。"
|
||
case "ruleContent.imageStyle": return "FULL 表示图片铺满;留空使用默认样式。"
|
||
default:
|
||
if isJavaScript { return "使用 JavaScript 编写,可通过更多菜单调试当前书源草稿。" }
|
||
if path.contains(".") { return "支持 CSS / JSoup、XPath、JSONPath、正则和 JavaScript;留空表示不配置此项。" }
|
||
return "留空表示不配置此项。"
|
||
}
|
||
}
|
||
public var isJavaScript: Bool {
|
||
path.lowercased().contains("js") || path == "jsLib" || path.hasSuffix("imageDecode") || path.hasSuffix("payAction")
|
||
}
|
||
}
|
||
|
||
public struct BookSourceEditorSection: Identifiable, Sendable {
|
||
public let id: String
|
||
public let title: String
|
||
public let fields: [BookSourceEditorField]
|
||
}
|
||
|
||
public enum BookSourceEditorSchema {
|
||
public static let sections: [BookSourceEditorSection] = [
|
||
.init(id: "basic", title: "基础", fields: [
|
||
.init(path: "bookSourceName", title: "书源名称"),
|
||
.init(path: "bookSourceUrl", title: "书源地址"),
|
||
.init(path: "bookSourceGroup", title: "分组"),
|
||
.init(path: "bookSourceComment", title: "备注"),
|
||
.init(path: "bookUrlPattern", title: "书籍 URL 正则"),
|
||
.init(path: "concurrentRate", title: "并发率"),
|
||
.init(path: "header", title: "请求头"),
|
||
.init(path: "jsLib", title: "JavaScript 库"),
|
||
.init(path: "variableComment", title: "变量说明"),
|
||
]),
|
||
.init(id: "login", title: "登录", fields: [
|
||
.init(path: "loginUrl", title: "登录地址 / 脚本"),
|
||
.init(path: "loginUi", title: "自定义登录界面"),
|
||
.init(path: "loginCheckJs", title: "登录检测脚本"),
|
||
.init(path: "coverDecodeJs", title: "封面解密脚本"),
|
||
]),
|
||
.init(id: "search", title: "搜索", fields: [
|
||
.init(path: "searchUrl", title: "搜索地址"),
|
||
.init(path: "ruleSearch.checkKeyWord", title: "测试关键词"),
|
||
.init(path: "ruleSearch.bookList", title: "书籍列表"),
|
||
.init(path: "ruleSearch.name", title: "书名"),
|
||
.init(path: "ruleSearch.author", title: "作者"),
|
||
.init(path: "ruleSearch.intro", title: "简介"),
|
||
.init(path: "ruleSearch.kind", title: "分类"),
|
||
.init(path: "ruleSearch.lastChapter", title: "最新章节"),
|
||
.init(path: "ruleSearch.updateTime", title: "更新时间"),
|
||
.init(path: "ruleSearch.bookUrl", title: "详情地址"),
|
||
.init(path: "ruleSearch.coverUrl", title: "封面地址"),
|
||
.init(path: "ruleSearch.wordCount", title: "字数"),
|
||
]),
|
||
.init(id: "explore", title: "发现", fields: [
|
||
.init(path: "exploreUrl", title: "发现分类地址"),
|
||
.init(path: "exploreScreen", title: "发现筛选配置"),
|
||
.init(path: "ruleExplore.bookList", title: "书籍列表"),
|
||
.init(path: "ruleExplore.name", title: "书名"),
|
||
.init(path: "ruleExplore.author", title: "作者"),
|
||
.init(path: "ruleExplore.intro", title: "简介"),
|
||
.init(path: "ruleExplore.kind", title: "分类"),
|
||
.init(path: "ruleExplore.lastChapter", title: "最新章节"),
|
||
.init(path: "ruleExplore.updateTime", title: "更新时间"),
|
||
.init(path: "ruleExplore.bookUrl", title: "详情地址"),
|
||
.init(path: "ruleExplore.coverUrl", title: "封面地址"),
|
||
.init(path: "ruleExplore.wordCount", title: "字数"),
|
||
]),
|
||
.init(id: "info", title: "详情", fields: [
|
||
.init(path: "ruleBookInfo.init", title: "预处理"),
|
||
.init(path: "ruleBookInfo.name", title: "书名"),
|
||
.init(path: "ruleBookInfo.author", title: "作者"),
|
||
.init(path: "ruleBookInfo.intro", title: "简介"),
|
||
.init(path: "ruleBookInfo.kind", title: "分类"),
|
||
.init(path: "ruleBookInfo.lastChapter", title: "最新章节"),
|
||
.init(path: "ruleBookInfo.updateTime", title: "更新时间"),
|
||
.init(path: "ruleBookInfo.coverUrl", title: "封面地址"),
|
||
.init(path: "ruleBookInfo.tocUrl", title: "目录地址"),
|
||
.init(path: "ruleBookInfo.wordCount", title: "字数"),
|
||
.init(path: "ruleBookInfo.canReName", title: "允许修改书名作者"),
|
||
.init(path: "ruleBookInfo.downloadUrls", title: "文件下载地址"),
|
||
]),
|
||
.init(id: "toc", title: "目录", fields: [
|
||
.init(path: "ruleToc.preUpdateJs", title: "更新前脚本"),
|
||
.init(path: "ruleToc.chapterList", title: "章节列表"),
|
||
.init(path: "ruleToc.chapterName", title: "章节名称"),
|
||
.init(path: "ruleToc.chapterUrl", title: "章节地址"),
|
||
.init(path: "ruleToc.formatJs", title: "标题处理脚本"),
|
||
.init(path: "ruleToc.isVolume", title: "卷名标识"),
|
||
.init(path: "ruleToc.isVip", title: "VIP 标识"),
|
||
.init(path: "ruleToc.isPay", title: "已购买标识"),
|
||
.init(path: "ruleToc.updateTime", title: "章节信息"),
|
||
.init(path: "ruleToc.nextTocUrl", title: "目录下一页"),
|
||
]),
|
||
.init(id: "content", title: "正文", fields: [
|
||
.init(path: "ruleContent.content", title: "正文"),
|
||
.init(path: "ruleContent.subContent", title: "副正文"),
|
||
.init(path: "ruleContent.title", title: "正文标题"),
|
||
.init(path: "ruleContent.nextContentUrl", title: "正文下一页"),
|
||
.init(path: "ruleContent.webJs", title: "WebView 注入脚本"),
|
||
.init(path: "ruleContent.sourceRegex", title: "资源正则"),
|
||
.init(path: "ruleContent.replaceRegex", title: "合并后替换"),
|
||
.init(path: "ruleContent.imageStyle", title: "图片样式"),
|
||
.init(path: "ruleContent.imageDecode", title: "图片解密脚本"),
|
||
.init(path: "ruleContent.payAction", title: "购买脚本"),
|
||
.init(path: "ruleContent.callBackJs", title: "事件回调脚本"),
|
||
]),
|
||
.init(id: "review", title: "段评", fields: [
|
||
.init(path: "ruleReview.reviewUrl", title: "段评地址"),
|
||
.init(path: "ruleReview.avatarRule", title: "头像"),
|
||
.init(path: "ruleReview.contentRule", title: "内容"),
|
||
.init(path: "ruleReview.postTimeRule", title: "发布时间"),
|
||
.init(path: "ruleReview.reviewQuoteUrl", title: "回复地址"),
|
||
.init(path: "ruleReview.voteUpUrl", title: "点赞地址"),
|
||
.init(path: "ruleReview.voteDownUrl", title: "点踩地址"),
|
||
.init(path: "ruleReview.postReviewUrl", title: "发布段评"),
|
||
.init(path: "ruleReview.postQuoteUrl", title: "发布回复"),
|
||
.init(path: "ruleReview.deleteUrl", title: "删除地址"),
|
||
]),
|
||
]
|
||
}
|
||
|
||
public struct BookSourceEditorDocument: Sendable, Equatable {
|
||
public private(set) var fields: [String: JSONValue]
|
||
|
||
public init(source: BookSource) throws {
|
||
self = try Self(json: String(decoding: JSONEncoder().encode(source), as: UTF8.self))
|
||
}
|
||
|
||
public init(json: String) throws {
|
||
guard case .object(let object) = try JSONValue.decode(data: Data(json.utf8)) else {
|
||
throw BookSourceImportError.invalidDocument("编辑器需要单个书源 JSON 对象")
|
||
}
|
||
fields = object
|
||
for key in Self.ruleKeys {
|
||
if case .string(let text) = fields[key], !text.isEmpty,
|
||
case .object(let object) = try JSONValue.decode(data: Data(text.utf8)) {
|
||
fields[key] = .object(object)
|
||
}
|
||
}
|
||
}
|
||
|
||
public func value(at path: String) -> JSONValue? {
|
||
let keys = path.split(separator: ".").map(String.init)
|
||
guard let key = keys.first else { return nil }
|
||
return keys.count == 1 ? fields[key] : fields[key]?[keys[1]]
|
||
}
|
||
|
||
public mutating func set(_ value: JSONValue?, at path: String) {
|
||
let keys = path.split(separator: ".").map(String.init)
|
||
guard let key = keys.first else { return }
|
||
if keys.count == 1 { fields[key] = value; return }
|
||
var object: [String: JSONValue] = [:]
|
||
if case .object(let existing) = fields[key] { object = existing }
|
||
object[keys[1]] = value
|
||
fields[key] = object.isEmpty ? nil : .object(object)
|
||
}
|
||
|
||
public func json() throws -> String {
|
||
let encoder = JSONEncoder()
|
||
encoder.outputFormatting = [.prettyPrinted, .sortedKeys, .withoutEscapingSlashes]
|
||
return String(decoding: try encoder.encode(fields), as: UTF8.self)
|
||
}
|
||
|
||
public func source() throws -> BookSource {
|
||
let source = try JSONDecoder().decode(BookSource.self, from: JSONEncoder().encode(fields))
|
||
let errors = BookSourceValidator.validate(source).filter { $0.severity == .error }
|
||
guard errors.isEmpty else { throw BookSourceImportError.invalidSources(errors) }
|
||
return source
|
||
}
|
||
|
||
static let ruleKeys = ["ruleSearch", "ruleExplore", "ruleBookInfo", "ruleToc", "ruleContent", "ruleReview"]
|
||
}
|
||
|
||
struct SourceCodingKey: CodingKey {
|
||
let stringValue: String
|
||
let intValue: Int? = nil
|
||
init(_ value: String) { stringValue = value }
|
||
init?(stringValue: String) { self.init(stringValue) }
|
||
init?(intValue: Int) { return nil }
|
||
}
|