diff --git a/StikJIT/Utilities/AppStoreIconFetcher.swift b/StikJIT/Utilities/AppStoreIconFetcher.swift new file mode 100644 index 00000000..3ca012e7 --- /dev/null +++ b/StikJIT/Utilities/AppStoreIconFetcher.swift @@ -0,0 +1,71 @@ +// +// AppStoreIconFetcher.swift +// StikJIT +// +// Created by neoarz on 3/28/25. +// + +import UIKit + +class AppStoreIconFetcher { + static private var iconCache: [String: UIImage] = [:] + + static func getIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) { + // Check cache first + if let cachedIcon = iconCache[bundleID] { + completion(cachedIcon) + return + } + + // Hit the App Store API + let baseURLString = "https://itunes.apple.com/lookup?bundleId=" + let urlString = baseURLString + bundleID + + guard let url = URL(string: urlString) else { + DispatchQueue.main.async { completion(nil) } + return + } + + URLSession.shared.dataTask(with: url) { data, response, error in + guard let data = data, error == nil else { + DispatchQueue.main.async { completion(nil) } + return + } + + do { + if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any], + let results = json["results"] as? [[String: Any]], + !results.isEmpty, + let firstApp = results.first, + let iconURLString = firstApp["artworkUrl512"] as? String ?? firstApp["artworkUrl100"] as? String, + let iconURL = URL(string: iconURLString) { + + // Download the icon image + downloadImage(from: iconURL) { image in + if let image = image { + // Cache the icon + iconCache[bundleID] = image + } + DispatchQueue.main.async { + completion(image) + } + } + } else { + DispatchQueue.main.async { completion(nil) } + } + } catch { + DispatchQueue.main.async { completion(nil) } + } + }.resume() + } + + private static func downloadImage(from url: URL, completion: @escaping (UIImage?) -> Void) { + URLSession.shared.dataTask(with: url) { data, response, error in + if let data = data, let image = UIImage(data: data) { + completion(image) + } else { + completion(nil) + } + }.resume() + } +} \ No newline at end of file diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 8939d73b..d3506628 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -38,7 +38,7 @@ struct HomeView: View { .font(.system(.largeTitle, design: .rounded)) .fontWeight(.bold) - Text("Click enable JIT to get started") + Text(pairingFileExists ? "Click enable JIT to get started" : "Pick pairing file to get started") .font(.system(.subheadline, design: .rounded)) .foregroundStyle(.secondary) .multilineTextAlignment(.center) diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index f11c2b8e..d20654eb 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -8,63 +8,101 @@ import SwiftUI struct InstalledAppsListView: View { - @AppStorage("username") private var username = "User" - @AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000" - @State private var selectedBackgroundColor: Color = Color(hex: UserDefaults.standard.string(forKey: "customBackgroundColor") ?? "#000000") ?? Color.primaryBackground - let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() - @StateObject var viewModel = InstalledAppsViewModel() - @Environment(\.dismiss) var dismiss - @State private var searchText: String = "" - - var onSelect: (String) -> Void - - var filteredApps: [String] { - if searchText.isEmpty { - return Array(viewModel.apps.keys) // Use the keys (app names) - } else { - return viewModel.apps.keys.filter { $0.localizedCaseInsensitiveContains(searchText) } - } - } + @StateObject private var viewModel = InstalledAppsViewModel() + @State private var appIcons: [String: UIImage] = [:] + @Environment(\.dismiss) private var dismiss + @Environment(\.colorScheme) private var colorScheme + var onSelectApp: (String) -> Void var body: some View { NavigationView { - ZStack { - selectedBackgroundColor.edgesIgnoringSafeArea(.all) - - List { - ForEach(filteredApps, id: \.self) { app in + ScrollView { + LazyVStack(spacing: 0) { + ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { appName, bundleID in Button(action: { - onSelect(viewModel.apps[app] ?? "") // Select using the app's bundle ID + withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { + onSelectApp(bundleID) + } }) { - Text(app) - .font(.system(.body, design: .rounded)) - .padding(.vertical, 8) + HStack(spacing: 16) { + // App Icon + if let image = appIcons[bundleID] { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + .cornerRadius(12) + .shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2), + radius: 3, x: 0, y: 1) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color(UIColor.systemGray5)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "app") + .font(.system(size: 26)) + .foregroundColor(.gray) + ) + .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) + .onAppear { + loadAppIcon(for: bundleID) + } + } + + // App Name and Bundle ID + VStack(alignment: .leading, spacing: 4) { + Text(appName) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(Color.blue) + + Text(bundleID) + .font(.system(size: 15)) + .foregroundColor(Color.gray) + .lineLimit(1) + } + + Spacer() + } + .padding(.vertical, 12) + .padding(.horizontal, 20) + .contentShape(Rectangle()) } - .listRowBackground(Color(.secondarySystemGroupedBackground)) - .cornerRadius(8) - } - } - .listStyle(InsetGroupedListStyle()) - .navigationTitle("Installed Apps") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Done") { - dismiss() + .buttonStyle(PlainButtonStyle()) + + if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key { + Divider() + .padding(.leading, 96) + .padding(.trailing, 20) + .opacity(0.4) } } } - .searchable(text: $searchText, prompt: "Search Apps") + .background(Color(UIColor.systemBackground)) } - .onReceive(timer) { _ in - refreshBackground() + .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + .navigationTitle("Installed Apps") + .navigationBarItems(leading: Button("Done") { + dismiss() } - .onAppear { - print(filteredApps) + .font(.system(size: 17, weight: .regular)) + .foregroundColor(.blue)) + } + } + + // Helper method to load app icon + private func loadAppIcon(for bundleID: String) { + AppStoreIconFetcher.getIcon(for: bundleID) { image in + if let image = image { + DispatchQueue.main.async { + withAnimation(.easeIn(duration: 0.2)) { + self.appIcons[bundleID] = image + } + } } } } +} - private func refreshBackground() { - selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground - } +#Preview { + InstalledAppsListView { _ in } }