diff --git a/StickerSlack.xcodeproj/project.pbxproj b/StickerSlack.xcodeproj/project.pbxproj index ab1de96..9dcd089 100644 --- a/StickerSlack.xcodeproj/project.pbxproj +++ b/StickerSlack.xcodeproj/project.pbxproj @@ -9,6 +9,7 @@ /* Begin PBXBuildFile section */ A924C3722EA9127200F20781 /* 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 */; }; A949B1F32EA04E8200215164 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = A949B1EF2EA04E8200215164 /* Assets.xcassets */; }; A949B1F42EA04E8200215164 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A949B1F02EA04E8200215164 /* ContentView.swift */; }; @@ -77,6 +78,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + A924C3782EA9225800F20781 /* Haptics in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -190,6 +192,7 @@ ); name = StickerSlack; packageProductDependencies = ( + A924C3772EA9225800F20781 /* Haptics */, ); productName = StickerSlack; productReference = A949B1DF2EA04C0B00215164 /* StickerSlack.app */; @@ -241,6 +244,9 @@ ); mainGroup = A949B1D62EA04C0B00215164; minimizedProjectReferenceProxies = 1; + packageReferences = ( + A924C3762EA9225800F20781 /* XCLocalSwiftPackageReference "../Haptics" */, + ); preferredProjectObjectVersion = 77; productRefGroup = A949B1E02EA04C0B00215164 /* Products */; projectDirPath = ""; @@ -624,6 +630,20 @@ defaultConfigurationName = Release; }; /* 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 */; } diff --git a/StickerSlack/Emoji/Emoji.swift b/StickerSlack/Emoji/Emoji.swift index 81ce506..80feae5 100644 --- a/StickerSlack/Emoji/Emoji.swift +++ b/StickerSlack/Emoji/Emoji.swift @@ -48,7 +48,6 @@ struct Emoji: Codable, Identifiable, Hashable { self.remoteImageURL = apiEmoji.url self.localImageURL = EmojiHoarder.container.appendingPathComponent(id.uuidString, conformingTo: .image) - // Task { [weak self] in // let (data, response) = try await URLSession.shared.data(from: apiEmoji.url) // self.image = UIImage(data: data) @@ -58,8 +57,13 @@ struct Emoji: Codable, Identifiable, Hashable { // 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) + try! data.write(to: localImageURL) return UIImage(data: data)! } } diff --git a/StickerSlack/Emoji/SlackResponse.swift b/StickerSlack/Emoji/SlackResponse.swift index 51d32fe..1f6f991 100644 --- a/StickerSlack/Emoji/SlackResponse.swift +++ b/StickerSlack/Emoji/SlackResponse.swift @@ -12,20 +12,8 @@ struct SlackResponse: Codable { var imageUrl: String var alias: String? -// func toEmojis() -> [Emoji] { -// let initialMap = emoji.map { -// Emoji(name: $0.key, url: $0.value) -// } -// 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 -// } -// } + + static func toEmojis(from: [SlackResponse]) -> [Emoji] { + return from.map { ApiEmoji(name: $0.name, url: $0.imageUrl).toEmoji() } + } } diff --git a/StickerSlack/EmojiHoarder.swift b/StickerSlack/EmojiHoarder.swift index a998677..a3628f8 100644 --- a/StickerSlack/EmojiHoarder.swift +++ b/StickerSlack/EmojiHoarder.swift @@ -8,22 +8,28 @@ import Foundation import SwiftUI import Combine +import UniformTypeIdentifiers 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")! @Published var emojis: [Emoji] 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") } + let decoded: [SlackResponse] = try! JSONDecoder().decode([SlackResponse].self, from: data) - self.emojis = decoded.map { ApiEmoji(name: $0.name, url: $0.imageUrl).toEmoji() } -// Task { -// for i in emojis.indices { -// emojis[i].image = try? await emojis[i].loadImage() -// } -// } + self.emojis = SlackResponse.toEmojis(from: decoded) + + try! JSONEncoder().encode(emojis).write(to: EmojiHoarder.localEmojiDB) } // func storeStickers diff --git a/StickerSlack/Views/ContentView.swift b/StickerSlack/Views/ContentView.swift index d25c1f6..db16902 100644 --- a/StickerSlack/Views/ContentView.swift +++ b/StickerSlack/Views/ContentView.swift @@ -6,6 +6,7 @@ // import SwiftUI +import Haptics struct ContentView: View { @StateObject var hoarder: EmojiHoarder = EmojiHoarder() @@ -15,8 +16,18 @@ struct ContentView: View { TabView { List { ForEach(hoarder.emojis, id: \.self) { emoji in - EmojiPreview(emoji: emoji) - .frame(maxWidth: 100) + HStack { + EmojiPreview(emoji: emoji) + .frame(maxWidth: 100) + Spacer() + Button("", systemImage: "arrow.down.circle") { + Task { + let _ = try? await emoji.downloadImage() + Haptic.success.trigger() + } + } + .buttonStyle(.plain) + } } } .tabItem { diff --git a/StickerSlack/Views/EmojiPreview.swift b/StickerSlack/Views/EmojiPreview.swift index 8dd8f3f..e9885f3 100644 --- a/StickerSlack/Views/EmojiPreview.swift +++ b/StickerSlack/Views/EmojiPreview.swift @@ -6,34 +6,55 @@ // import SwiftUI +import Haptics struct EmojiPreview: View { @State var emoji: Emoji @State private var id: UUID = UUID() + @State private var image: UIImage? var body: some View { VStack { Text(emoji.name) - if let image = emoji.image { - Image(uiImage: image) - } else { - ProgressView() - } - AsyncImage(url: emoji.remoteImageURL) { phase in - if let image = phase.image { - image + + Group { + if let localImage = try? Data(contentsOf: emoji.localImageURL), + let image = UIImage(data: localImage) { + Image(uiImage: image) .resizable().scaledToFit() - } else if phase.error != nil { - Image(systemName: "xmark.app.fill") - .resizable().scaledToFit() - .symbolRenderingMode(.hierarchical) - .foregroundStyle(.red) - .onTapGesture { - id = UUID() + .border(.orange) + .overlay(alignment: .bottomLeading) { + Image(systemName: "arrow.down.circle.fill") + .resizable().scaledToFit() + .frame(width: 20, height: 20) + .symbolRenderingMode(.hierarchical) +// .foregroundStyle(.gray) } } else { - ProgressView() + AsyncImage(url: emoji.remoteImageURL) { phase in + if let image = phase.image { + image + .resizable().scaledToFit() + } else if phase.error != nil { + Image(systemName: "xmark.app.fill") + .resizable().scaledToFit() + .padding() + .symbolRenderingMode(.hierarchical) + .foregroundStyle(.red) + .onTapGesture { + id = UUID() + } + } else { + ProgressView() + } + } + } + } + .onTapGesture { + Task { + image = try? await emoji.downloadImage() + Haptic.success.trigger() } } .id(id)