Files
legado-ios/App/Features/Rules/ReplaceRuleLibraryModel.swift
T

142 lines
5.7 KiB
Swift

#if SWIFT_PACKAGE
import LegadoCore
#endif
import Foundation
@MainActor
final class ReplaceRuleLibraryModel: ObservableObject {
@Published private(set) var rules: [ReplaceRule] = []
@Published var notice: Notice?
struct Notice: Identifiable {
enum Kind { case success, error }
let id = UUID()
let kind: Kind
let title: String
let detail: String
}
private let repository: any ReplaceRuleRepository
private let importer = ReplaceRuleImporter()
init(repository: any ReplaceRuleRepository) { self.repository = repository }
static func live() -> ReplaceRuleLibraryModel {
let base = FileManager.default.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
.appendingPathComponent("Legado", isDirectory: true)
return ReplaceRuleLibraryModel(repository: FileReplaceRuleRepository(fileURL: base.appendingPathComponent("replace-rules.json")))
}
static func demo() -> ReplaceRuleLibraryModel {
ReplaceRuleLibraryModel(repository: InMemoryReplaceRuleRepository())
}
func load() async {
do { rules = try await repository.load() }
catch { notice = .init(kind: .error, title: "净化规则读取失败", detail: error.localizedDescription) }
}
func importFiles(_ urls: [URL]) async {
do {
var updated = rules
var count = 0
for url in urls {
let granted = url.startAccessingSecurityScopedResource()
defer { if granted { url.stopAccessingSecurityScopedResource() } }
let incoming = try importer.decode(Data(contentsOf: url, options: .mappedIfSafe))
updated = importer.merge(incoming, into: updated)
count += incoming.count
}
try await repository.save(updated)
rules = updated
notice = .init(kind: .success, title: "净化规则已导入", detail: "本次读取 \(count) 条,当前共 \(rules.count) 条。")
} catch {
notice = .init(kind: .error, title: "净化规则导入失败", detail: error.localizedDescription)
}
}
func importRemoteURL(_ value: String) async { await importText(value) }
func importText(_ value: String) async {
do {
let input = try RuleImportInput.parse(value, kind: "replaceRule")
let data: Data
switch input {
case .json(let value): data = Data(value.utf8)
case .remote:
let (body, response) = try await URLSessionHTTPTransport.shared.download(for: input.request()!)
if let response = response as? HTTPURLResponse, !(200...299).contains(response.statusCode) { throw URLError(.badServerResponse) }
data = body
}
let incoming = try importer.decode(data)
let updated = importer.merge(incoming, into: rules)
try await repository.save(updated)
rules = updated
notice = .init(kind: .success, title: "净化规则已导入", detail: "本次读取 \(incoming.count) 条,当前共 \(rules.count) 条。")
} catch { notice = .init(kind: .error, title: "净化规则导入失败", detail: error.localizedDescription) }
}
func newRule(copying original: ReplaceRule? = nil) -> ReplaceRule {
let now = Int64(Date().timeIntervalSince1970 * 1_000)
let maximumID = rules.map(\.id).max() ?? 0
let id = max(now, maximumID < Int64.max ? maximumID + 1 : now)
var rule = original ?? ReplaceRule(id: id, name: "", pattern: "")
rule.id = id
if original != nil { rule.name += " 副本" }
rule.sortOrder = (rules.map(\.sortOrder).filter { $0 != Int.min && $0 < Int.max }.max() ?? -1) + 1
return rule
}
func save(_ rule: ReplaceRule) async -> Bool {
do { try ReplaceRuleValidator.validate(rule) }
catch { notice = .init(kind: .error, title: "规则无效", detail: error.localizedDescription); return false }
return await persist(importer.merge([rule], into: rules))
}
func move(from offsets: IndexSet, to destination: Int) async {
var updated = rules
updated.move(fromOffsets: offsets, toOffset: destination)
for index in updated.indices { updated[index].sortOrder = index }
await persist(updated)
}
func remove(id: Int64) async { await persist(rules.filter { $0.id != id }) }
func applySilentSubscription(_ data: Data) async throws {
let incoming = try importer.decode(data)
let changed = incoming.filter { value in
guard let old = rules.first(where: { $0.id == value.id }) else { return true }
return old.pattern != value.pattern || old.replacement != value.replacement
}
guard !changed.isEmpty else { return }
let updated = importer.merge(changed, into: rules)
try await repository.save(updated)
rules = updated
}
func setEnabled(_ enabled: Bool, for id: Int64) async {
guard let index = rules.firstIndex(where: { $0.id == id }) else { return }
var updated = rules
updated[index].isEnabled = enabled
await persist(updated)
}
func remove(at offsets: IndexSet) async {
var updated = rules
updated.remove(atOffsets: offsets)
await persist(updated)
}
@discardableResult
private func persist(_ updated: [ReplaceRule]) async -> Bool {
do {
try await repository.save(updated)
rules = updated
return true
} catch {
notice = .init(kind: .error, title: "净化规则保存失败", detail: error.localizedDescription)
return false
}
}
}