restructure as standalone iOS app + iMessage extension
convert container from Messages-only app to regular iOS app, move shared code (models, api client, store) to Shared/ group, add app group entitlements for cross-process data sharing, rewrite StickerStore for shared UserDefaults + container, create SwiftUI app entry point, simplify extension to read-only browser Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
116
StickerCloner/Shared/StickerStore.swift
Normal file
116
StickerCloner/Shared/StickerStore.swift
Normal file
@@ -0,0 +1,116 @@
|
||||
import Foundation
|
||||
|
||||
final class StickerStore {
|
||||
static let shared = StickerStore()
|
||||
|
||||
private let packsKey = "savedPacks"
|
||||
private let suiteName = "group.de.felixfoertsch.StickerCloner"
|
||||
private let fileManager = FileManager.default
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
// 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")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user