organised the project for the first time

This commit is contained in:
neon443
2025-11-05 19:25:55 +00:00
parent 578f5e0149
commit c3cfecb87e
12 changed files with 59 additions and 19 deletions

View File

@@ -1,83 +0,0 @@
//
// EmojiCollectionView.swift
// StickerSlack
//
// Created by neon443 on 03/11/2025.
//
import Foundation
import UIKit
import SwiftUI
import Haptics
struct EmojiCollectionView: UIViewRepresentable {
let hoarder: EmojiHoarder
let items: [String]
func makeUIView(context: Context) -> UITableView {
let tableView = UITableView()
tableView.register(UITableViewCell.self, forCellReuseIdentifier: "cell")
tableView.dataSource = context.coordinator
return tableView
}
func updateUIView(_ uiView: UITableView, context: Context) {
context.coordinator.hoarder = hoarder
context.coordinator.items = items
uiView.reloadData()
}
func makeCoordinator() -> Coordinator {
Coordinator(hoarder: hoarder, items: items)
}
final class Coordinator: NSObject, UITableViewDataSource {
var hoarder: EmojiHoarder
var items: [String]
init(hoarder: EmojiHoarder, items: [String]) {
self.hoarder = hoarder
self.items = items
}
func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return items.count
}
func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
let emojiName = items[indexPath.row]
let cell = tableView.dequeueReusableCell(withIdentifier: "cell", for: indexPath)
cell.contentConfiguration = UIHostingConfiguration {
HStack {
EmojiPreview(
hoarder: hoarder,
emoji: hoarder.trie.dict[emojiName]!
)
.frame(maxWidth: 100, maxHeight: 100)
Spacer()
if hoarder.trie.dict[emojiName]!.isLocal {
Button("", systemImage: "trash") {
self.hoarder.trie.dict[emojiName]!.deleteImage()
self.hoarder.trie.dict[emojiName]!.refresh()
Haptic.heavy.trigger()
}
.buttonStyle(.plain)
} else {
Button("", systemImage: "arrow.down.circle") {
Task.detached {
try? await self.hoarder.trie.dict[emojiName]!.downloadImage()
await MainActor.run {
self.hoarder.trie.dict[emojiName]!.refresh()
Haptic.success.trigger()
}
}
}
.buttonStyle(.plain)
}
}
}
return cell
}
}
}

View File

@@ -0,0 +1,170 @@
//
// EmojiHoarder.swift
// StickerSlack
//
// Created by neon443 on 15/10/2025.
//
import Foundation
import SwiftUI
import Combine
import UniformTypeIdentifiers
class EmojiHoarder: ObservableObject {
static let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.neon443.StickerSlack")!.appendingPathComponent("Library", conformingTo: .directory)
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: [String] = []
@Published var searchTerm: String = ""
init(localOnly: Bool = false) {
let localDB = loadLocalDB()
withAnimation { self.emojis = localDB }
buildTrie()
withAnimation { self.filteredEmojis = [] }
guard !localOnly else { return }
Task.detached {
print("start loading remote db")
await self.loadRemoteDB()
print("end")
await self.buildTrie()
}
}
func storeStickers(_ toStore: [UUID]) {
for stickerId in toStore {
print(stickerId)
}
}
func deleteAllStickers() async {
await withTaskGroup { group in
for i in emojis.indices {
group.addTask {
guard await self.emojis[i].isLocal else { return }
await self.emojis[i].deleteImage()
DispatchQueue.main.sync {
self.emojis[i].refresh()
}
}
}
}
}
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.append(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 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 } }
// }
// }
// }
}

View File

@@ -1,51 +0,0 @@
//
// EmojiRow.swift
// StickerSlack
//
// Created by neon443 on 03/11/2025.
//
import SwiftUI
import Haptics
struct EmojiRow: View {
@ObservedObject var hoarder: EmojiHoarder
@State var emoji: Emoji
var body: some View {
HStack {
EmojiPreview(
hoarder: hoarder,
emoji: emoji
)
.frame(maxWidth: 100, maxHeight: 100)
Spacer()
if emoji.isLocal {
Button("", systemImage: "trash") {
emoji.deleteImage()
emoji.refresh()
Haptic.heavy.trigger()
}
.buttonStyle(.plain)
} else {
Button("", systemImage: "arrow.down.circle") {
Task.detached {
try? await emoji.downloadImage()
await MainActor.run {
emoji.refresh()
Haptic.success.trigger()
}
}
}
.buttonStyle(.plain)
}
}
}
}
#Preview {
EmojiRow(
hoarder: EmojiHoarder(localOnly: true),
emoji: Emoji.test
)
}

View File

@@ -1,80 +0,0 @@
//
// Trie.swift
// StickerSlack
//
// Created by neon443 on 03/11/2025.
//
import Foundation
import SwiftUI
import Combine
class TrieNode: ObservableObject {
@Published var children: [Character: TrieNode] = [:]
@Published var isEndOfWord: Bool = false
}
class Trie: ObservableObject {
@Published var root: TrieNode = TrieNode()
@Published var dict: [String:Emoji] = [:]
func insert(word: String, emoji: Emoji) {
let word = word.lowercased()
var currentNode = root
let indices = word.indices
let last = indices.last
for i in indices {
let char = word[i]
if let node = currentNode.children[char] {
currentNode = node
} else {
currentNode.children[char] = TrieNode()
currentNode = currentNode.children[char]!
}
if i == last {
currentNode.isEndOfWord = true
}
}
}
func search(for query: String) -> Bool {
var currentNode = root
for char in query.lowercased() {
if let node = currentNode.children[char] {
currentNode = node
} else {
return false
}
}
return currentNode.isEndOfWord
}
func search(prefix prefixQuery: String) -> [String] {
guard !prefixQuery.isEmpty else { return [] }
let prefixQuery = prefixQuery.lowercased()
var currentNode = root
for char in prefixQuery {
guard let child = currentNode.children[char] else {
return []
}
currentNode = child
}
return collectWords(startingWith: prefixQuery, from: currentNode)
}
func collectWords(startingWith: String, from node: TrieNode) -> [String] {
var results: [String] = []
if node.isEndOfWord {
results.append(startingWith)
}
for child in node.children {
results += collectWords(startingWith: startingWith+String(child.key), from: child.value)
}
return results
}
}

View File

@@ -1,98 +0,0 @@
//
// TrieTestingView.swift
// StickerSlack
//
// Created by neon443 on 03/11/2025.
//
import SwiftUI
struct TrieTestingView: View {
@ObservedObject var hoarder: EmojiHoarder = EmojiHoarder(localOnly: true)
@State var id: UUID = UUID()
@State var newWord: String = "hello"
@State var searchTerm: String = ""
@State var searchStatus: Bool? = nil
@State var filterTerm: String = ""
@State var filterResult: [String] = []
@State var uikit: Bool = false
var body: some View {
VStack {
Toggle("uikit!!", isOn: $uikit)
.foregroundStyle(.blue)
Button("reset", role: .destructive) {
hoarder.resetTrie()
}
Button("add emojis!") {
hoarder.buildTrie()
}
.buttonStyle(.borderedProminent)
HStack {
TextField("", text: $searchTerm)
.textFieldStyle(.roundedBorder)
.border(.orange)
.onChange(of: searchTerm) { _ in
searchStatus = hoarder.trie.search(for: searchTerm)
}
if let searchStatus {
Circle()
.frame(width: 20, height: 20)
.foregroundStyle(searchStatus ? .green : .red)
} else {
Text("?")
.frame(width: 20, height: 20)
}
}
HStack {
TextField("", text: $filterTerm)
.textFieldStyle(.roundedBorder)
.border(.orange)
.onChange(of: filterTerm) { _ in
withAnimation { filterResult = hoarder.trie.search(prefix: filterTerm) }
}
Text("\(filterResult.count)")
.modifier(numericTextCompat())
}
if uikit {
EmojiCollectionView(hoarder: hoarder, items: filterResult)
.id(filterResult)
} else {
List(filterResult, id: \.self) { item in
EmojiRow(hoarder: hoarder, emoji: hoarder.trie.dict[item]!)
}
}
Text("\(hoarder.trie.root.children.count)")
}
}
}
struct TrieNodeView: View {
@ObservedObject var trie: Trie
@State var trieNode: TrieNode
var body: some View {
ForEach(trieNode.children.map { $0.key }, id: \.self) { key in
let node = trieNode.children[key]!
Text(String(key))
.foregroundStyle(node.isEndOfWord ? .red : .primary)
.frame(width: 20, height: 20)
TrieNodeView(trie: trie, trieNode: node)
.padding(.leading, 20)
}
}
}
#Preview {
TrieTestingView()
}