mirror of
https://github.com/neon443/StickerSlack.git
synced 2026-03-11 13:26:17 +00:00
organised the project for the first time
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
170
StickerSlack/Emoji/EmojiHoarder.swift
Normal file
170
StickerSlack/Emoji/EmojiHoarder.swift
Normal 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 } }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
}
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user