updated downloadImage()

- to use cached local version if available
 - now writes to disk if not already saved

rewrote toEmojis() just a simple map of ApiEmoji($0).toEmoji()
use the new toEmojis in emojiHoarder

now the emojiDB gets stored locally, need to make it fetch it if it can cos cache cant be valid forever yk

using neon443/Haptics for haptics lol
added a download button
on EmojiPreview, if downloaded a lil download icon shows up, and will show that one instead of AsyncImage
This commit is contained in:
neon443
2025-10-23 09:46:53 +01:00
parent 2841bc2109
commit 4ed5695f4a
6 changed files with 93 additions and 43 deletions

View File

@@ -9,6 +9,7 @@
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
A924C3722EA9127200F20781 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; }; A924C3722EA9127200F20781 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; };
A924C3732EA9127200F20781 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; }; A924C3732EA9127200F20781 /* Emoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A924C3712EA9127200F20781 /* Emoji.swift */; };
A924C3782EA9225800F20781 /* Haptics in Frameworks */ = {isa = PBXBuildFile; productRef = A924C3772EA9225800F20781 /* Haptics */; };
A940FE3D2EA232590016870B /* ApiEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A940FE3C2EA232590016870B /* ApiEmoji.swift */; }; A940FE3D2EA232590016870B /* ApiEmoji.swift in Sources */ = {isa = PBXBuildFile; fileRef = A940FE3C2EA232590016870B /* ApiEmoji.swift */; };
A949B1F32EA04E8200215164 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A949B1EF2EA04E8200215164 /* Assets.xcassets */; }; A949B1F32EA04E8200215164 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A949B1EF2EA04E8200215164 /* Assets.xcassets */; };
A949B1F42EA04E8200215164 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949B1F02EA04E8200215164 /* ContentView.swift */; }; A949B1F42EA04E8200215164 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949B1F02EA04E8200215164 /* ContentView.swift */; };
@@ -77,6 +78,7 @@
isa = PBXFrameworksBuildPhase; isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647; buildActionMask = 2147483647;
files = ( files = (
A924C3782EA9225800F20781 /* Haptics in Frameworks */,
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
@@ -190,6 +192,7 @@
); );
name = StickerSlack; name = StickerSlack;
packageProductDependencies = ( packageProductDependencies = (
A924C3772EA9225800F20781 /* Haptics */,
); );
productName = StickerSlack; productName = StickerSlack;
productReference = A949B1DF2EA04C0B00215164 /* StickerSlack.app */; productReference = A949B1DF2EA04C0B00215164 /* StickerSlack.app */;
@@ -241,6 +244,9 @@
); );
mainGroup = A949B1D62EA04C0B00215164; mainGroup = A949B1D62EA04C0B00215164;
minimizedProjectReferenceProxies = 1; minimizedProjectReferenceProxies = 1;
packageReferences = (
A924C3762EA9225800F20781 /* XCLocalSwiftPackageReference "../Haptics" */,
);
preferredProjectObjectVersion = 77; preferredProjectObjectVersion = 77;
productRefGroup = A949B1E02EA04C0B00215164 /* Products */; productRefGroup = A949B1E02EA04C0B00215164 /* Products */;
projectDirPath = ""; projectDirPath = "";
@@ -624,6 +630,20 @@
defaultConfigurationName = Release; defaultConfigurationName = Release;
}; };
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
A924C3762EA9225800F20781 /* XCLocalSwiftPackageReference "../Haptics" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = ../Haptics;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
A924C3772EA9225800F20781 /* Haptics */ = {
isa = XCSwiftPackageProductDependency;
productName = Haptics;
};
/* End XCSwiftPackageProductDependency section */
}; };
rootObject = A949B1D72EA04C0B00215164 /* Project object */; rootObject = A949B1D72EA04C0B00215164 /* Project object */;
} }

View File

@@ -48,7 +48,6 @@ struct Emoji: Codable, Identifiable, Hashable {
self.remoteImageURL = apiEmoji.url self.remoteImageURL = apiEmoji.url
self.localImageURL = EmojiHoarder.container.appendingPathComponent(id.uuidString, conformingTo: .image) self.localImageURL = EmojiHoarder.container.appendingPathComponent(id.uuidString, conformingTo: .image)
// Task { [weak self] in // Task { [weak self] in
// let (data, response) = try await URLSession.shared.data(from: apiEmoji.url) // let (data, response) = try await URLSession.shared.data(from: apiEmoji.url)
// self.image = UIImage(data: data) // self.image = UIImage(data: data)
@@ -58,8 +57,13 @@ struct Emoji: Codable, Identifiable, Hashable {
// self.image = UIImage(data: image) ?? UIImage() // self.image = UIImage(data: image) ?? UIImage()
} }
func loadImage() async throws -> UIImage { func downloadImage() async throws -> UIImage {
if let data = try? Data(contentsOf: localImageURL),
let uiimage = UIImage(data: data) {
return uiimage
}
let (data, _) = try await URLSession.shared.data(from: remoteImageURL) let (data, _) = try await URLSession.shared.data(from: remoteImageURL)
try! data.write(to: localImageURL)
return UIImage(data: data)! return UIImage(data: data)!
} }
} }

View File

@@ -12,20 +12,8 @@ struct SlackResponse: Codable {
var imageUrl: String var imageUrl: String
var alias: String? var alias: String?
// func toEmojis() -> [Emoji] {
// let initialMap = emoji.map { static func toEmojis(from: [SlackResponse]) -> [Emoji] {
// Emoji(name: $0.key, url: $0.value) return from.map { ApiEmoji(name: $0.name, url: $0.imageUrl).toEmoji() }
// } }
// return initialMap.map {
// var ret = $0
// if ret.urlString.prefix(6) == "alias:" {
// if let orig = initialMap.first(where: {
// $0.name == "\(ret.urlString.dropFirst(6))"
// }) {
// ret.urlString = orig.urlString
// }
// }
// return ret
// }
// }
} }

View File

@@ -8,22 +8,28 @@
import Foundation import Foundation
import SwiftUI import SwiftUI
import Combine import Combine
import UniformTypeIdentifiers
class EmojiHoarder: ObservableObject { class EmojiHoarder: ObservableObject {
static let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.neon443.StickerSlack")! static let container = FileManager.default.containerURL(forSecurityApplicationGroupIdentifier: "group.com.neon443.StickerSlack")!.appendingPathComponent("Library", conformingTo: .directory)
static let localEmojiDB: URL = EmojiHoarder.container.appendingPathExtension("localEmojiDB.json")
private let endpoint: URL = URL(string: "https://cachet.dunkirk.sh/emojis")! private let endpoint: URL = URL(string: "https://cachet.dunkirk.sh/emojis")!
@Published var emojis: [Emoji] @Published var emojis: [Emoji]
init() { init() {
if let localEmojiDB = try? Data(contentsOf: EmojiHoarder.localEmojiDB) {
let decoded = try! JSONDecoder().decode([Emoji].self, from: localEmojiDB)
self.emojis = decoded
return
}
guard let data = try? Data(contentsOf: endpoint) else { fatalError("cachet unreachable") } guard let data = try? Data(contentsOf: endpoint) else { fatalError("cachet unreachable") }
let decoded: [SlackResponse] = try! JSONDecoder().decode([SlackResponse].self, from: data) let decoded: [SlackResponse] = try! JSONDecoder().decode([SlackResponse].self, from: data)
self.emojis = decoded.map { ApiEmoji(name: $0.name, url: $0.imageUrl).toEmoji() } self.emojis = SlackResponse.toEmojis(from: decoded)
// Task {
// for i in emojis.indices { try! JSONEncoder().encode(emojis).write(to: EmojiHoarder.localEmojiDB)
// emojis[i].image = try? await emojis[i].loadImage()
// }
// }
} }
// func storeStickers // func storeStickers

View File

@@ -6,6 +6,7 @@
// //
import SwiftUI import SwiftUI
import Haptics
struct ContentView: View { struct ContentView: View {
@StateObject var hoarder: EmojiHoarder = EmojiHoarder() @StateObject var hoarder: EmojiHoarder = EmojiHoarder()
@@ -15,8 +16,18 @@ struct ContentView: View {
TabView { TabView {
List { List {
ForEach(hoarder.emojis, id: \.self) { emoji in ForEach(hoarder.emojis, id: \.self) { emoji in
HStack {
EmojiPreview(emoji: emoji) EmojiPreview(emoji: emoji)
.frame(maxWidth: 100) .frame(maxWidth: 100)
Spacer()
Button("", systemImage: "arrow.down.circle") {
Task {
let _ = try? await emoji.downloadImage()
Haptic.success.trigger()
}
}
.buttonStyle(.plain)
}
} }
} }
.tabItem { .tabItem {

View File

@@ -6,20 +6,32 @@
// //
import SwiftUI import SwiftUI
import Haptics
struct EmojiPreview: View { struct EmojiPreview: View {
@State var emoji: Emoji @State var emoji: Emoji
@State private var id: UUID = UUID() @State private var id: UUID = UUID()
@State private var image: UIImage?
var body: some View { var body: some View {
VStack { VStack {
Text(emoji.name) Text(emoji.name)
if let image = emoji.image {
Group {
if let localImage = try? Data(contentsOf: emoji.localImageURL),
let image = UIImage(data: localImage) {
Image(uiImage: image) Image(uiImage: image)
} else { .resizable().scaledToFit()
ProgressView() .border(.orange)
.overlay(alignment: .bottomLeading) {
Image(systemName: "arrow.down.circle.fill")
.resizable().scaledToFit()
.frame(width: 20, height: 20)
.symbolRenderingMode(.hierarchical)
// .foregroundStyle(.gray)
} }
} else {
AsyncImage(url: emoji.remoteImageURL) { phase in AsyncImage(url: emoji.remoteImageURL) { phase in
if let image = phase.image { if let image = phase.image {
image image
@@ -27,6 +39,7 @@ struct EmojiPreview: View {
} else if phase.error != nil { } else if phase.error != nil {
Image(systemName: "xmark.app.fill") Image(systemName: "xmark.app.fill")
.resizable().scaledToFit() .resizable().scaledToFit()
.padding()
.symbolRenderingMode(.hierarchical) .symbolRenderingMode(.hierarchical)
.foregroundStyle(.red) .foregroundStyle(.red)
.onTapGesture { .onTapGesture {
@@ -36,6 +49,14 @@ struct EmojiPreview: View {
ProgressView() ProgressView()
} }
} }
}
}
.onTapGesture {
Task {
image = try? await emoji.downloadImage()
Haptic.success.trigger()
}
}
.id(id) .id(id)
} }
} }