- 侧边栏 4 → 6 项:首页 / 历史记录 / 词汇表 / 风格 / 帮助中心 / 设置 - 词典 → 词汇表,整页改 chip 流式布局:phrase + hitCount,hover ✕ 删除, 批量输入(每行一个),重置统计 / 清除全部 - DictionaryEntry 加 hitCount + 兼容旧 dictionary.json 解码 - DictationCoordinator 在润色后扫描最终文本递增命中数; 删除 learnTerms 自动学词路径 - 润色模式 → 风格 (StyleTab):4 张模式卡 + 启用开关; 关闭时跳过 Ark 直接插入原文 (UserPreferences.polishEnabled) - 帮助中心:快速上手 4 步、快捷键速查、FAQ、GitHub 外链 - 设置 Tab 合并 Volc ASR + Ark 凭据、录音、授权、隐私、关于 + 检查更新 - 胶囊:NSPanel 220×96, hasShadow=false;macOS 26 分支去掉 SwiftUI 阴影 → 解决"红绿灯外圈黑边" - 侧边栏视觉:strokeBorder 0.08 → 0.04、阴影 radius 16 → 6 → 解决"灰色边栏 + 黑色卡顿" - 首页底部新增今日概览 + 风格预览
114 lines
3.6 KiB
Swift
114 lines
3.6 KiB
Swift
import Foundation
|
|
import OpenLessCore
|
|
|
|
public final class DictionaryStore: @unchecked Sendable {
|
|
private let fileURL: URL
|
|
private let lock = NSLock()
|
|
private let encoder = JSONEncoder()
|
|
private let decoder = JSONDecoder()
|
|
|
|
public init() {
|
|
encoder.dateEncodingStrategy = .iso8601
|
|
decoder.dateDecodingStrategy = .iso8601
|
|
let dir = FileManager.default
|
|
.urls(for: .applicationSupportDirectory, in: .userDomainMask)[0]
|
|
.appendingPathComponent("OpenLess", isDirectory: true)
|
|
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
self.fileURL = dir.appendingPathComponent("dictionary.json")
|
|
if !FileManager.default.fileExists(atPath: fileURL.path) {
|
|
FileManager.default.createFile(atPath: fileURL.path, contents: "[]".data(using: .utf8))
|
|
}
|
|
}
|
|
|
|
public func all() -> [DictionaryEntry] {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
return loadAll()
|
|
}
|
|
|
|
public func enabledEntries() -> [DictionaryEntry] {
|
|
all().filter { $0.enabled && !$0.trimmedPhrase.isEmpty }
|
|
}
|
|
|
|
public func upsert(_ entry: DictionaryEntry) {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
var entries = loadAll()
|
|
var updated = entry
|
|
updated.phrase = updated.trimmedPhrase
|
|
updated.updatedAt = Date()
|
|
if let index = entries.firstIndex(where: { $0.id == updated.id }) {
|
|
entries[index] = updated
|
|
} else {
|
|
entries.insert(updated, at: 0)
|
|
}
|
|
saveLocked(entries)
|
|
}
|
|
|
|
public func delete(id: UUID) {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
let entries = loadAll().filter { $0.id != id }
|
|
saveLocked(entries)
|
|
}
|
|
|
|
/// 扫描润色后文本:每个启用词条只要在文本里出现就 +1(同一次输出最多 +1)。
|
|
/// 返回这次新增命中的 phrase,方便日志。
|
|
@discardableResult
|
|
public func incrementHits(matching text: String) -> [String] {
|
|
let trimmed = text.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else { return [] }
|
|
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
|
|
var entries = loadAll()
|
|
let lowerText = trimmed.lowercased()
|
|
var hit: [String] = []
|
|
for index in entries.indices {
|
|
guard entries[index].enabled else { continue }
|
|
let phrase = entries[index].trimmedPhrase
|
|
guard !phrase.isEmpty else { continue }
|
|
if lowerText.contains(phrase.lowercased()) {
|
|
entries[index].hitCount += 1
|
|
hit.append(phrase)
|
|
}
|
|
}
|
|
if !hit.isEmpty {
|
|
saveLocked(entries)
|
|
}
|
|
return hit
|
|
}
|
|
|
|
public func resetHits() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
var entries = loadAll()
|
|
for index in entries.indices {
|
|
entries[index].hitCount = 0
|
|
}
|
|
saveLocked(entries)
|
|
}
|
|
|
|
public func clearAll() {
|
|
lock.lock()
|
|
defer { lock.unlock() }
|
|
saveLocked([])
|
|
}
|
|
|
|
private func loadAll() -> [DictionaryEntry] {
|
|
guard let data = try? Data(contentsOf: fileURL) else { return [] }
|
|
return (try? decoder.decode([DictionaryEntry].self, from: data)) ?? []
|
|
}
|
|
|
|
private func saveLocked(_ entries: [DictionaryEntry]) {
|
|
let sorted = entries.sorted { lhs, rhs in
|
|
if lhs.enabled != rhs.enabled { return lhs.enabled && !rhs.enabled }
|
|
return lhs.updatedAt > rhs.updatedAt
|
|
}
|
|
if let data = try? encoder.encode(sorted) {
|
|
try? data.write(to: fileURL, options: .atomic)
|
|
}
|
|
}
|
|
}
|