Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 71 additions & 0 deletions StikJIT/Utilities/AppStoreIconFetcher.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
2 changes: 1 addition & 1 deletion StikJIT/Views/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
126 changes: 82 additions & 44 deletions StikJIT/Views/InstalledAppsListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
}