// // LNGiftManager.swift // Gami // // Created by OneeChan on 2026/3/24. // import Foundation private struct LNGiftCacheList: Codable { var list: [LNGiftResource] var version: String } class LNGiftManager { static let shared = LNGiftManager() private let pageSize = 100 private let cacheURL = URL.cacheDir .appendingPathComponent("Gift", isDirectory: true) .appendingPathComponent("gift_resources.json") private var resourceMap: [String: LNGiftResource] = [:] private var resourceVersion: String = "" private var isRefreshing = false private init() { DispatchQueue.global().async { [weak self] in guard let self else { return } loadCache() } LNEventDeliver.addObserver(self) } func resource(for id: String) -> LNGiftResource? { resourceMap[id] } func fetchGiftList(roomId: String, queue: DispatchQueue = .main, handler: @escaping ([LNGiftItemVO]?) -> Void) { LNHttpManager.shared.loadGiftList(roomId: roomId) { [weak self] res, err in guard let self else { return } let list = res?.list.filter { self.resourceMap[$0.resId] != nil } queue.asyncIfNotGlobal { handler(list) } if let list, list.count != res?.list.count { updateGiftResource() } if let err { showToast(err.errorDesc) } } } } extension LNGiftManager { func updateGiftResource() { guard !isRefreshing else { return } var changedGifts: [LNGiftResource] = [] var newVersion = "" isRefreshing = true func _fetchResource(next: String) { LNHttpManager.shared.loadResourceList(version: resourceVersion, size: pageSize, next: next) { [weak self] res, err in guard let self else { return } guard let res else { isRefreshing = false return } if !res.list.isEmpty { changedGifts.append(contentsOf: res.list) } if !res.version.isEmpty { newVersion = res.version } if res.list.isEmpty || res.next.isEmpty != false { mergeChangedResources(changedGifts, version: newVersion) isRefreshing = false } else { _fetchResource(next: next) } } } _fetchResource(next: "") } private func mergeChangedResources(_ changedList: [LNGiftResource], version: String) { guard !changedList.isEmpty, !version.isEmpty else { return } var newMap = resourceMap changedList.forEach { item in guard !item.id.isEmpty else { return } newMap[item.id] = item } resourceMap = newMap resourceVersion = version saveCache() } } extension LNGiftManager { private func loadCache() { guard let data = try? Data(contentsOf: cacheURL) else { return } do { let snapshot = try JSONDecoder().decode(LNGiftCacheList.self, from: data) resourceVersion = snapshot.version resourceMap = snapshot.list.reduce(into: [:]) { partialResult, item in guard !item.id.isEmpty else { return } partialResult[item.id] = item } } catch { Log.e("load gift resource cache failed: \(error.localizedDescription)") } } private func saveCache() { let snapshot = LNGiftCacheList( list: Array(resourceMap.values), version: resourceVersion ) do { let folderURL = cacheURL.deletingLastPathComponent() try FileManager.default.createDirectory(at: folderURL, withIntermediateDirectories: true, attributes: nil) let data = try JSONEncoder().encode(snapshot) try data.write(to: cacheURL, options: .atomic) } catch { Log.e("save gift resource cache failed: \(error.localizedDescription)") } } } extension LNGiftManager { func sendGift(params: LNSendGiftParams, queue: DispatchQueue = .main, handler: @escaping (Bool) -> Void) { let remain: TimeInterval = LNUserDefaults[.remainExchange, 0] if remain.isSameDay { params.seamlessRedeem = true } LNHttpManager.shared.sendGift(params: params) { res, err in queue.asyncIfNotGlobal { handler(err == nil) } if let err { showToast(err.errorDesc) if case .serverError(let code, _) = err { runOnMain { if code == LNOrderErrorCode.NotEnoughMoney.rawValue { let panel = LNMoneyNotEnoughAlertView() panel.update(.diamond) panel.popup() } else if LNOrderErrorCode.NotEnoughMoneyButCanExchange.rawValue == code { let panel = LNMoneyNotEnoughAlertView() panel.update(.diamond, exchange: .coin) panel.exchangeHandler = { params.seamlessRedeem = true self.sendGift(params: params, queue: queue, handler: handler) } panel.popup() } } } } if let res { LNPurchaseManager.shared.updateWallet(diamond: res.diamond, coin: res.goldcoin) } } } } extension LNGiftManager: LNAccountManagerNotify { func onUserLogin() { updateGiftResource() } }