feat: 支持 EPUB 阅读并改善规则兼容与阅读稳定性

支持 EPUB 2/3 导入、目录、封面和章节插图,保留本地书管理与阅读进度。
修复 JSON 模板、动态替换与请求参数兼容,以及 RSS 翻页和连续滚动定位。
更新编辑器工具栏和回归测试,发布版本为 0.1.12(build 18)。
This commit is contained in:
Aminark
2026-10-24 00:58:11 +08:00
parent 3322fd71db
commit 67bc212ffd
58 changed files with 2331 additions and 244 deletions
@@ -59,7 +59,12 @@ struct SyntaxCodeEditor: UIViewRepresentable {
UIBarButtonItem(title: symbol == "\n" ? "↵" : symbol == "\t" ? "⇥" : symbol,
primaryAction: UIAction(title: "", subtitle: nil) { [weak view] _ in view?.insertText(symbol) })
} + [UIBarButtonItem(systemItem: .flexibleSpace), UIBarButtonItem(title: "完成", primaryAction: UIAction(title: "", subtitle: nil) { [weak view] _ in view?.resignFirstResponder() })]
view.inputAccessoryView = bar
bar.items?.last?.accessibilityIdentifier = "syntaxCodeEditor.done"
if #available(iOS 26.0, *) {
view.inputAccessoryView = CodeInputAccessory(items: bar.items ?? [])
} else {
view.inputAccessoryView = bar
}
view.text = text
context.coordinator.highlight(view)
return view
@@ -184,6 +189,66 @@ struct SyntaxCodeEditor: UIViewRepresentable {
}
}
// Keep Done visible while allowing the editing shortcuts to scroll on narrow keyboards.
private final class CodeInputAccessory: UIInputView {
init(items: [UIBarButtonItem]) {
super.init(frame: CGRect(x: 0, y: 0, width: 0, height: 44), inputViewStyle: .keyboard)
autoresizingMask = [.flexibleWidth]
backgroundColor = .secondarySystemGroupedBackground
let scroll = UIScrollView()
scroll.showsHorizontalScrollIndicator = false
scroll.delaysContentTouches = false
scroll.translatesAutoresizingMaskIntoConstraints = false
let stack = UIStackView()
stack.axis = .horizontal
stack.spacing = 4
stack.translatesAutoresizingMaskIntoConstraints = false
let actions = items.filter { $0.primaryAction != nil }
var done: UIButton?
for (index, item) in actions.enumerated() {
let button = UIButton(type: .system)
if let action = item.primaryAction { button.addAction(action, for: .touchUpInside) }
button.setImage(item.image, for: .normal)
button.setTitle(item.title, for: .normal)
button.tintColor = UIColor(ReadingTheme.accent)
button.titleLabel?.font = .systemFont(ofSize: 16)
button.accessibilityIdentifier = item.accessibilityIdentifier
button.accessibilityLabel = item.accessibilityLabel
button.translatesAutoresizingMaskIntoConstraints = false
if index == actions.count - 1 {
button.titleLabel?.font = .systemFont(ofSize: 16, weight: .semibold)
done = button
addSubview(button)
} else {
if index < 3 { button.accessibilityLabel = ["查找与替换", "撤销", "重做"][index] }
stack.addArrangedSubview(button)
button.widthAnchor.constraint(equalToConstant: 36).isActive = true
}
}
addSubview(scroll)
scroll.addSubview(stack)
guard let done else { return }
NSLayoutConstraint.activate([
done.trailingAnchor.constraint(equalTo: trailingAnchor, constant: -8),
done.widthAnchor.constraint(equalToConstant: 60),
done.topAnchor.constraint(equalTo: topAnchor),
done.bottomAnchor.constraint(equalTo: bottomAnchor),
scroll.leadingAnchor.constraint(equalTo: leadingAnchor, constant: 4),
scroll.trailingAnchor.constraint(equalTo: done.leadingAnchor, constant: -4),
scroll.topAnchor.constraint(equalTo: topAnchor),
scroll.bottomAnchor.constraint(equalTo: bottomAnchor),
stack.leadingAnchor.constraint(equalTo: scroll.contentLayoutGuide.leadingAnchor),
stack.trailingAnchor.constraint(equalTo: scroll.contentLayoutGuide.trailingAnchor),
stack.topAnchor.constraint(equalTo: scroll.contentLayoutGuide.topAnchor),
stack.bottomAnchor.constraint(equalTo: scroll.contentLayoutGuide.bottomAnchor),
stack.heightAnchor.constraint(equalTo: scroll.frameLayoutGuide.heightAnchor)
])
}
required init?(coder: NSCoder) { fatalError("init(coder:) has not been implemented") }
override var intrinsicContentSize: CGSize { CGSize(width: UIView.noIntrinsicMetric, height: 44) }
}
struct RuleCodeFieldView: View {
let title: String
let path: String
@@ -92,9 +92,10 @@ struct ReplaceRuleEditorView: View {
RuleCodeFieldView(title: "匹配内容", path: "pattern", text: $rule.pattern, language: .regex)
} label: { fieldPreview("匹配内容", value: rule.pattern) }
NavigationLink {
RuleCodeFieldView(title: "替换为", path: "replacement", text: $rule.replacement, language: .regex)
RuleCodeFieldView(title: "替换为", path: "replacement", text: $rule.replacement,
language: rule.isRegex && rule.replacement.hasPrefix("@js:") ? .javascript : .regex)
} label: { fieldPreview("替换为(留空即删除)", value: rule.replacement) }
Text("正则替换使用 $1、$2 引用捕获组;普通文本替换按字面匹配。")
Text("正则替换使用 $1、$2 引用捕获组;以 @js: 开头可运行 JavaScript,result 为当前匹配文本,可读取 book 和 chapter。普通文本替换按字面匹配。")
.font(.footnote).foregroundStyle(.secondary)
}
Section("作用范围") {