import Foundation import UIKit final class StickerStore { static let shared = StickerStore() private let packsKey = "savedPacks" private let suiteName = "group.de.felixfoertsch.StickerCloner" private let fileManager = FileManager.default private let thumbnailCache = NSCache() private lazy var defaults: UserDefaults = { guard let defaults = UserDefaults(suiteName: suiteName) else { fatalError("App Group '\(suiteName)' is not configured") } return defaults }() private init() { migrateIfNeeded() } // MARK: - Pack persistence var savedPacks: [SavedPack] { get { guard let data = defaults.data(forKey: packsKey) else { return [] } return (try? JSONDecoder().decode([SavedPack].self, from: data)) ?? [] } set { let data = try? JSONEncoder().encode(newValue) defaults.set(data, forKey: packsKey) } } // MARK: - File management private var stickersDirectory: URL { let container = fileManager.containerURL(forSecurityApplicationGroupIdentifier: suiteName)! return container.appendingPathComponent("stickers", isDirectory: true) } private func packDirectory(name: String) -> URL { stickersDirectory.appendingPathComponent(name, isDirectory: true) } // MARK: - Add / remove packs func addPack(response: StickerSetResponse) async throws { let pack = SavedPack(name: response.name, title: response.title, stickerCount: response.stickerCount) let packDir = packDirectory(name: response.name) try fileManager.createDirectory(at: packDir, withIntermediateDirectories: true) for sticker in response.stickers { let localURL = packDir.appendingPathComponent("\(sticker.id).png") if fileManager.fileExists(atPath: localURL.path) { continue } try await StickerClonerAPI.downloadSticker(from: sticker.pngUrl, to: localURL) } var packs = savedPacks packs.removeAll { $0.name == pack.name } packs.append(pack) savedPacks = packs } func removePack(name: String) { var packs = savedPacks packs.removeAll { $0.name == name } savedPacks = packs thumbnailCache.removeObject(forKey: name as NSString) let packDir = packDirectory(name: name) try? fileManager.removeItem(at: packDir) } // MARK: - Load sticker file URLs func stickerFileURLs() -> [URL] { var urls: [URL] = [] for pack in savedPacks { let packDir = packDirectory(name: pack.name) guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { continue } for file in files.sorted(by: { $0.lastPathComponent < $1.lastPathComponent }) { guard file.pathExtension == "png" else { continue } urls.append(file) } } return urls } func stickerFileURLs(forPack name: String) -> [URL] { let packDir = packDirectory(name: name) guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return [] } return files .filter { $0.pathExtension == "png" } .sorted { $0.lastPathComponent < $1.lastPathComponent } } func firstStickerURL(forPack name: String) -> URL? { let packDir = packDirectory(name: name) guard let files = try? fileManager.contentsOfDirectory(at: packDir, includingPropertiesForKeys: nil) else { return nil } return files .filter { $0.pathExtension == "png" } .sorted { $0.lastPathComponent < $1.lastPathComponent } .first } func thumbnail(forPack name: String) -> UIImage? { let key = name as NSString if let cached = thumbnailCache.object(forKey: key) { return cached } guard let url = firstStickerURL(forPack: name), let data = try? Data(contentsOf: url), let image = UIImage(data: data) else { return nil } thumbnailCache.setObject(image, forKey: key) return image } // MARK: - Migration from old sandbox private func migrateIfNeeded() { let migrated = defaults.bool(forKey: "didMigrateToAppGroup") guard !migrated else { return } let oldDefaults = UserDefaults.standard if let data = oldDefaults.data(forKey: packsKey) { defaults.set(data, forKey: packsKey) oldDefaults.removeObject(forKey: packsKey) } let oldCaches = fileManager.urls(for: .cachesDirectory, in: .userDomainMask).first! let oldStickersDir = oldCaches.appendingPathComponent("stickers", isDirectory: true) if fileManager.fileExists(atPath: oldStickersDir.path) { let newStickersDir = stickersDirectory try? fileManager.createDirectory(at: newStickersDir.deletingLastPathComponent(), withIntermediateDirectories: true) if !fileManager.fileExists(atPath: newStickersDir.path) { try? fileManager.moveItem(at: oldStickersDir, to: newStickersDir) } } defaults.set(true, forKey: "didMigrateToAppGroup") } }