mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 12:30:29 +00:00
Merge pull request #3 from neoarz/list-apps-view
part #1: give the apps list view a refresh
This commit is contained in:
commit
ac1857118b
3 changed files with 155 additions and 46 deletions
71
StikJIT/Utilities/AppStoreIconFetcher.swift
Normal file
71
StikJIT/Utilities/AppStoreIconFetcher.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue