Files
StickerSlack/StickerSlack/Emoji/EmojiHoarder.swift

186 lines
4.6 KiB
Swift

//
// EmojiHoarder.swift
// StickerSlack
//
// Created by neon443 on 15/10/2025.
//
import Foundation
import SwiftUI
import Combine
import UniformTypeIdentifiers
import Haptics
class EmojiHoarder: ObservableObject {
static let shared: EmojiHoarder = EmojiHoarder()
static let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.neon443.StickerSlack")!.appendingPathComponent("Library", conformingTo: .directory)
nonisolated static let localEmojiDB: URL = EmojiHoarder.container.appendingPathComponent("_localEmojiDB.json", conformingTo: .fileURL)
private let endpoint: URL = URL(string: "https://cachet.dunkirk.sh/emojis")!
private let encoder = JSONEncoder()
private let decoder = JSONDecoder()
@Published var emojis: [Emoji] = []
@Published var trie: Trie = Trie()
@Published var filteredEmojis: [String] = []
@Published var downloadedEmojis: Set<String> = []
@Published var searchTerm: String = ""
init(localOnly: Bool = false, skipIndex: Bool = false) {
let localDB = loadLocalDB()
withAnimation { self.emojis = localDB }
if !skipIndex { buildTrie() }
withAnimation { self.filteredEmojis = [] }
guard !localOnly else { return }
Task.detached {
print("start loading remote db")
await self.loadRemoteDB()
print("end")
if !skipIndex { await self.buildTrie() }
}
}
func storeStickers(_ toStore: [UUID]) {
for stickerId in toStore {
print(stickerId)
}
}
@MainActor
func deleteAllStickers() {
for i in emojis.indices {
guard downloadedEmojis.contains(emojis[i].name) else { continue }
delete(emoji: emojis[i])
}
}
func storeDB() {
try! encoder.encode(emojis).write(to: EmojiHoarder.localEmojiDB)
}
func storeDB(data: Data) {
try! data.write(to: EmojiHoarder.localEmojiDB)
}
func resetTrie() {
trie.root = TrieNode()
trie.dict = [:]
downloadedEmojis = []
filteredEmojis = []
}
func buildTrie() {
let start = Date().timeIntervalSince1970
trie.root = TrieNode()
for emoji in emojis {
trie.insert(word: emoji.name, emoji: emoji)
}
buildTrieDict()
print("done building trie in", Date().timeIntervalSince1970-start)
}
func buildTrieDict() {
var dict: [String:Emoji] = [:]
for emoji in emojis {
dict[emoji.name] = emoji
}
self.trie.dict = dict
buildDownloadedEmojis()
}
func buildDownloadedEmojis() {
downloadedEmojis = []
for emoji in emojis {
guard emoji.isLocal else { continue }
downloadedEmojis.insert(emoji.name)
}
}
nonisolated
func loadLocalDB() -> [Emoji] {
if let localEmojiDB = try? Data(contentsOf: EmojiHoarder.localEmojiDB) {
let decoded = try! decoder.decode([Emoji].self, from: localEmojiDB)
return decoded
}
return []
}
func loadRemoteDB() async {
async let fetched = self.fetchRemoteDB()
if let fetched = await fetched {
withAnimation { self.emojis = fetched }
}
}
nonisolated
func fetchRemoteDB() async -> [Emoji]? {
do {
async let (data, _) = try URLSession.shared.data(from: endpoint)
decoder.dateDecodingStrategy = .iso8601
let decoded: [SlackResponse] = try decoder.decode([SlackResponse].self, from: await data)
try await storeDB(data: await data)
return await SlackResponse.toEmojis(from: decoded)
} catch {
print(error.localizedDescription)
return nil
}
}
func refreshDB() async {
guard let fetched = await self.fetchRemoteDB() else {
let local = loadLocalDB()
await MainActor.run {
emojis = local
buildTrie()
}
return
}
await MainActor.run {
withAnimation { self.emojis = fetched }
buildTrie()
}
}
func filterEmojis(by searchTerm: String) {
withAnimation { filteredEmojis = trie.search(prefix: searchTerm) }
}
func download(emoji: Emoji) {
Task.detached {
try? await emoji.downloadImage()
await MainActor.run {
self.downloadedEmojis.insert(emoji.name)
self.trie.dict[emoji.name]?.refresh()
Haptic.success.trigger()
}
}
}
@MainActor
func delete(emoji: Emoji) {
emoji.deleteImage()
downloadedEmojis.remove(emoji.name)
self.trie.dict[emoji.name]?.refresh()
Haptic.heavy.trigger()
}
// func filterEmojis(byCategory category: FilterCategory, searchTerm: String) {
// guard category != .none else {
// filterEmojis(by: searchTerm)
// return
// }
// self.filterEmojis(by: searchTerm)
// DispatchQueue.main.async {
// switch category {
// case .none:
// fallthrough
// case .downloaded:
// withAnimation(.interactiveSpring) { self.filteredEmojis = self.filteredEmojis.filter { $0.isLocal } }
// case .notDownloaded:
// withAnimation(.interactiveSpring) { self.filteredEmojis = self.filteredEmojis.filter { !$0.isLocal } }
// }
// }
// }
}