// // AppScenicQueueStore.swift // suixinkan // import Foundation /// 管理当前账号的景区排队打卡点、播报和设置缓存。 final class AppScenicQueueStore { private enum Key: String, CaseIterable { case punchSpotId = "scenic_queue_punch_spot_id" case punchSpotName = "scenic_queue_punch_spot_name" case remark = "scenic_queue_remark" case settingsSnapshot = "scenic_queue_settings_snapshot" case useOfflineTTS = "scenic_queue_offline_tts_v1" case presetVoices = "scenic_queue_preset_voices_v1" } private let defaults: UserDefaults private let session: AppSessionStore /// 使用指定持久化容器和会话创建景区排队存储。 init(defaults: UserDefaults, session: AppSessionStore) { self.defaults = defaults self.session = session } /// 已保存打卡点 ID。 var punchSpotId: String { get { guard let key = scenicKey(.punchSpotId) else { return "" } return defaults.string(forKey: key) ?? "" } set { guard let key = scenicKey(.punchSpotId) else { return } defaults.set(newValue, forKey: key) } } /// 已保存打卡点名称。 var punchSpotName: String { get { spotString(for: .punchSpotName) } set { setSpotString(newValue, for: .punchSpotName) } } /// 自定义播报文案。 var remark: String { get { spotString(for: .remark) } set { setSpotString(newValue, for: .remark) } } /// 是否优先使用离线 TTS。 var useOfflineTTS: Bool { get { guard let key = scenicKey(.useOfflineTTS), defaults.object(forKey: key) != nil else { return true } return defaults.bool(forKey: key) } set { guard let key = scenicKey(.useOfflineTTS) else { return } defaults.set(newValue, forKey: key) } } /// 当前账号是否已经保存有效打卡点。 func hasPunchSpotSaved() -> Bool { let id = punchSpotId.trimmingCharacters(in: .whitespacesAndNewlines) return Int64(id).map { $0 > 0 } ?? false } /// 保存排队设置快照。 func saveSettingsSnapshot(_ snapshot: ScenicQueueSettingsSnapshot) { guard let key = spotKey(.settingsSnapshot), let data = try? JSONEncoder().encode(snapshot) else { return } defaults.set(data, forKey: key) } /// 读取排队设置快照。 func settingsSnapshot() -> ScenicQueueSettingsSnapshot? { guard let key = spotKey(.settingsSnapshot), let data = defaults.data(forKey: key) else { return nil } return try? JSONDecoder().decode(ScenicQueueSettingsSnapshot.self, from: data) } /// 保存排队预设播报文案,最多 5 条。 func savePresetVoices(_ voices: [String]) { guard let key = spotKey(.presetVoices) else { return } let normalized = voices .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } .filter { !$0.isEmpty } .prefix(5) defaults.set(Array(normalized), forKey: key) } /// 读取排队预设播报文案。 func presetVoices() -> [String] { guard let key = spotKey(.presetVoices) else { return [] } return defaults.stringArray(forKey: key) ?? [] } /// 清除当前账号的景区排队缓存。 func clear(accountScope: String) { guard !accountScope.isEmpty else { return } let scenicPrefix = "\(accountScope)_scenic_" let suffixes = Key.allCases.map { "_\($0.rawValue)" } defaults.dictionaryRepresentation().keys .filter { key in key.hasPrefix(scenicPrefix) && suffixes.contains(where: key.hasSuffix) } .forEach { defaults.removeObject(forKey: $0) } } private func scenicKey(_ key: Key) -> String? { session.scenicScopedKey(key.rawValue) } private func spotKey(_ key: Key) -> String? { session.scenicSpotScopedKey(key.rawValue, spotId: punchSpotId) } private func spotString(for key: Key) -> String { guard let scopedKey = spotKey(key) else { return "" } return defaults.string(forKey: scopedKey) ?? "" } private func setSpotString(_ value: String, for key: Key) { guard let scopedKey = spotKey(key) else { return } defaults.set(value, forKey: scopedKey) } }