Files
legado-ios/App/Features/Rules/ReplaceRuleManagementView.swift

131 lines
7.6 KiB
Swift

import SwiftUI
import UniformTypeIdentifiers
struct ReplaceRuleManagementView: View {
@EnvironmentObject private var library: ReplaceRuleLibraryModel
@State private var showsImporter = false
@State private var showsTextImporter = false
@State private var showsQRImporter = false
@State private var editingRule: ReplaceRule?
@State private var startsWithTest = false
@State private var query = ""
@State private var group: String?
private var filtered: [ReplaceRule] {
library.rules.filter { rule in
(group == nil || rule.group == group) && (query.isEmpty || rule.name.localizedCaseInsensitiveContains(query)
|| rule.pattern.localizedCaseInsensitiveContains(query) || (rule.group?.localizedCaseInsensitiveContains(query) ?? false))
}
}
var body: some View {
List {
if library.rules.isEmpty {
ReadingUnavailableView("还没有净化规则", systemImage: "wand.and.stars", description: Text("新建规则,或导入 Legado 净化规则。"))
Button("新建规则", systemImage: "plus") { edit(library.newRule()) }
} else {
Section {
ReadingLabeledContent("已启用", value: "\(library.rules.filter(\.isEnabled).count) / \(library.rules.count)")
Picker("分组", selection: $group) {
Text("全部").tag(String?.none)
ForEach(Array(Set(library.rules.compactMap(\.group))).sorted(), id: \.self) { Text($0).tag(String?.some($0)) }
}
}
Section {
ForEach(filtered) { rule in
HStack(spacing: 12) {
Button { edit(rule) } label: {
HStack(spacing: 12) {
Image(systemName: rule.isRegex ? "textformat.alt" : "character.cursor.ibeam")
.foregroundStyle(rule.isEnabled ? ReadingTheme.accent : .secondary)
VStack(alignment: .leading, spacing: 4) {
Text(rule.name.isEmpty ? rule.pattern : rule.name).foregroundStyle(.primary).lineLimit(1)
Text(rule.group ?? (rule.scopeTitle && rule.scopeContent ? "标题与正文" : rule.scopeTitle ? "标题" : "正文"))
.font(.caption).foregroundStyle(.secondary)
Text(rule.pattern).font(.caption.monospaced()).foregroundStyle(.secondary).lineLimit(1)
}
Spacer()
}
}.buttonStyle(.plain)
Toggle("启用", isOn: Binding(get: { rule.isEnabled }, set: { value in Task { await library.setEnabled(value, for: rule.id) } }))
.labelsHidden().accessibilityLabel("启用\(rule.name)")
}
.contextMenu {
Button("编辑", systemImage: "pencil") { edit(rule) }
Button("测试", systemImage: "play") { edit(rule, test: true) }
Button("复制", systemImage: "doc.on.doc") { edit(library.newRule(copying: rule)) }
Button("删除", systemImage: "trash", role: .destructive) { Task { await library.remove(id: rule.id) } }
}
}
.onDelete { offsets in
let ids = offsets.compactMap { filtered.indices.contains($0) ? filtered[$0].id : nil }
Task { for id in ids { await library.remove(id: id) } }
}
.onMove { offsets, destination in Task { await library.move(from: offsets, to: destination) } }
.moveDisabled(group != nil || !query.isEmpty)
} footer: { Text("点击规则编辑,长按可复制或测试。拖动排序决定执行顺序。").font(.footnote) }
}
}
.listStyle(.insetGrouped)
.navigationTitle("净化规则")
.searchable(text: $query, prompt: "名称、匹配内容或分组")
.toolbar {
ToolbarItem(placement: .topBarTrailing) { EditButton().disabled(library.rules.isEmpty || group != nil || !query.isEmpty) }
ToolbarItem(placement: .primaryAction) {
Menu("更多", systemImage: "plus") {
Button("新建规则", systemImage: "plus") { edit(library.newRule()) }
Button("从文件导入", systemImage: "doc.badge.plus") { showsImporter = true }
Button("URL 或粘贴 JSON", systemImage: "link.badge.plus") { showsTextImporter = true }
Button("二维码导入", systemImage: "qrcode.viewfinder") { showsQRImporter = true }
ReadingShareLink(item: exportJSON) { Label("导出全部规则", systemImage: "square.and.arrow.up") }
}
}
}
.sheet(item: $editingRule) { ReplaceRuleEditorView(rule: $0, startsWithTest: startsWithTest).environmentObject(library) }
.sheet(isPresented: $showsQRImporter) { QRCodeImportView { value in Task { await library.importText(value) } } }
.sheet(isPresented: $showsTextImporter) { ReplaceRuleTextImportView().environmentObject(library) }
.fileImporter(isPresented: $showsImporter, allowedContentTypes: [.item], allowsMultipleSelection: true) { result in
switch result {
case .success(let urls): Task { await library.importFiles(urls) }
case .failure(let error): library.notice = .init(kind: .error, title: "无法选择文件", detail: error.localizedDescription)
}
}
.alert(item: $library.notice) { notice in
Alert(title: Text(notice.title), message: Text(notice.detail), dismissButton: .default(Text("好")))
}
}
private func edit(_ rule: ReplaceRule, test: Bool = false) { startsWithTest = test; editingRule = rule }
private var exportJSON: String {
let encoder = JSONEncoder(); encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
return (try? encoder.encode(library.rules)).map { String(decoding: $0, as: UTF8.self) } ?? "[]"
}
}
private struct ReplaceRuleTextImportView: View {
@Environment(\.dismiss) private var dismiss
@EnvironmentObject private var library: ReplaceRuleLibraryModel
@State private var value = ""
var body: some View {
ReadingNavigationStack {
VStack(alignment: .leading, spacing: 12) {
Text("粘贴净化规则 JSON、HTTP(S) 链接或 Legado 净化规则深链。").font(.footnote).foregroundStyle(.secondary)
SyntaxCodeEditor(text: $value, language: .json)
}.padding()
.navigationTitle("导入净化规则").navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .cancellationAction) { Button("取消") { dismiss() } }
ToolbarItem(placement: .confirmationAction) {
Button("导入") {
let text = value
dismiss()
Task {
try? await Task.sleep(nanoseconds: 400_000_000)
await library.importText(text)
}
}.disabled(value.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty)
}
}
}
}
}