mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Merge remote-tracking branch 'upstream/main'
This commit is contained in:
commit
99fbf4670d
6 changed files with 381 additions and 64 deletions
|
|
@ -8,6 +8,9 @@ Powered by [idevice](https://github.com/jkcoxson/idevice) and [Emotional Manglin
|
||||||
|
|
||||||
All dependencies remain under their original licenses.
|
All dependencies remain under their original licenses.
|
||||||
|
|
||||||
|
## Privacy Policy
|
||||||
|
No data is collected
|
||||||
|
|
||||||
## TODO List
|
## TODO List
|
||||||
|
|
||||||
- [X] Integrate `em_proxy`
|
- [X] Integrate `em_proxy`
|
||||||
|
|
|
||||||
|
|
@ -454,7 +454,7 @@
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
|
|
@ -502,7 +502,7 @@
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||||
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
|
||||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||||
LIBRARY_SEARCH_PATHS = (
|
LIBRARY_SEARCH_PATHS = (
|
||||||
|
|
|
||||||
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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -23,6 +23,11 @@ struct HomeView: View {
|
||||||
@State private var isProcessing = false
|
@State private var isProcessing = false
|
||||||
@State private var isShowingInstalledApps = false
|
@State private var isShowingInstalledApps = false
|
||||||
@State private var isShowingPairingFilePicker = false
|
@State private var isShowingPairingFilePicker = false
|
||||||
|
@State private var pairingFileExists: Bool = false
|
||||||
|
@State private var showPairingFileMessage = false
|
||||||
|
@State private var pairingFileIsValid = false
|
||||||
|
@State private var isImportingFile = false
|
||||||
|
@State private var importProgress: Float = 0.0
|
||||||
|
|
||||||
@State private var viewDidAppeared = false
|
@State private var viewDidAppeared = false
|
||||||
@State private var pendingBundleIdToEnableJIT : String? = nil
|
@State private var pendingBundleIdToEnableJIT : String? = nil
|
||||||
|
|
@ -38,38 +43,98 @@ struct HomeView: View {
|
||||||
.font(.system(.largeTitle, design: .rounded))
|
.font(.system(.largeTitle, design: .rounded))
|
||||||
.fontWeight(.bold)
|
.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))
|
.font(.system(.subheadline, design: .rounded))
|
||||||
.foregroundStyle(.secondary)
|
.foregroundStyle(.secondary)
|
||||||
.multilineTextAlignment(.center)
|
.multilineTextAlignment(.center)
|
||||||
}
|
}
|
||||||
.padding(.top, 40)
|
.padding(.top, 40)
|
||||||
|
|
||||||
|
// Main action button - changes based on whether we have a pairing file
|
||||||
Button(action: {
|
Button(action: {
|
||||||
if !FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
if pairingFileExists {
|
||||||
isShowingPairingFilePicker = true
|
// Got a pairing file, show apps
|
||||||
} else {
|
|
||||||
isShowingInstalledApps = true
|
isShowingInstalledApps = true
|
||||||
|
} else {
|
||||||
|
// No pairing file yet, let's get one
|
||||||
|
isShowingPairingFilePicker = true
|
||||||
}
|
}
|
||||||
}) {
|
}) {
|
||||||
Label("Enable JIT", systemImage: "list.bullet")
|
HStack {
|
||||||
.font(.system(.title3, design: .rounded))
|
Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus")
|
||||||
.fontWeight(.semibold)
|
.font(.system(size: 20))
|
||||||
.frame(maxWidth: .infinity)
|
Text(pairingFileExists ? "Enable JIT" : "Select Pairing File")
|
||||||
.padding()
|
.font(.system(.title3, design: .rounded))
|
||||||
.background(Color.blue)
|
.fontWeight(.semibold)
|
||||||
.foregroundColor(.white)
|
}
|
||||||
.cornerRadius(16)
|
.frame(maxWidth: .infinity)
|
||||||
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4)
|
.padding()
|
||||||
|
.background(Color.blue)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.cornerRadius(16)
|
||||||
|
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 20)
|
.padding(.horizontal, 20)
|
||||||
|
|
||||||
|
// Status message area - keeps layout consistent
|
||||||
|
ZStack {
|
||||||
|
// Progress bar for importing file
|
||||||
|
if isImportingFile {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Processing pairing file...")
|
||||||
|
.font(.system(.caption, design: .rounded))
|
||||||
|
.foregroundColor(.secondaryText)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(importProgress * 100))%")
|
||||||
|
.font(.system(.caption, design: .rounded))
|
||||||
|
.foregroundColor(.secondaryText)
|
||||||
|
}
|
||||||
|
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color.black.opacity(0.2))
|
||||||
|
.frame(height: 8)
|
||||||
|
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color.green)
|
||||||
|
.frame(width: geometry.size.width * CGFloat(importProgress), height: 8)
|
||||||
|
.animation(.linear(duration: 0.3), value: importProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 8)
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 40)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Success message
|
||||||
|
if showPairingFileMessage && pairingFileIsValid {
|
||||||
|
Text("✓ Pairing file successfully imported")
|
||||||
|
.font(.system(.callout, design: .rounded))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.background(Color.green.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
.transition(.opacity)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Invisible text to reserve space - no layout jumps
|
||||||
|
Text(" ").opacity(0)
|
||||||
|
}
|
||||||
|
.frame(height: isImportingFile ? 60 : 30) // Adjust height based on what's showing
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding()
|
.padding()
|
||||||
}
|
}
|
||||||
|
.onAppear {
|
||||||
|
checkPairingFileExists()
|
||||||
|
}
|
||||||
.onReceive(timer) { _ in
|
.onReceive(timer) { _ in
|
||||||
refreshBackground()
|
refreshBackground()
|
||||||
|
checkPairingFileExists()
|
||||||
}
|
}
|
||||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
|
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
|
||||||
switch result {
|
switch result {
|
||||||
|
|
@ -86,9 +151,45 @@ struct HomeView: View {
|
||||||
|
|
||||||
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||||
print("File copied successfully!")
|
print("File copied successfully!")
|
||||||
|
|
||||||
|
// Show progress bar and initialize progress
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
isImportingFile = true
|
||||||
|
importProgress = 0.0
|
||||||
|
pairingFileExists = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start heartbeat in background
|
||||||
startHeartbeatInBackground()
|
startHeartbeatInBackground()
|
||||||
|
|
||||||
Thread.sleep(forTimeInterval: 5)
|
// Create timer to update progress instead of sleeping
|
||||||
|
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if importProgress < 1.0 {
|
||||||
|
importProgress += 0.25
|
||||||
|
} else {
|
||||||
|
timer.invalidate()
|
||||||
|
isImportingFile = false
|
||||||
|
pairingFileIsValid = true
|
||||||
|
|
||||||
|
// Show success message
|
||||||
|
withAnimation {
|
||||||
|
showPairingFileMessage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Hide message after delay
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
withAnimation {
|
||||||
|
showPairingFileMessage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure timer keeps running
|
||||||
|
RunLoop.current.add(progressTimer, forMode: .common)
|
||||||
|
|
||||||
} catch {
|
} catch {
|
||||||
print("Error copying file: \(error)")
|
print("Error copying file: \(error)")
|
||||||
}
|
}
|
||||||
|
|
@ -99,8 +200,8 @@ struct HomeView: View {
|
||||||
if accessing {
|
if accessing {
|
||||||
url.stopAccessingSecurityScopedResource()
|
url.stopAccessingSecurityScopedResource()
|
||||||
}
|
}
|
||||||
case .failure(_):
|
case .failure(let error):
|
||||||
print("Failed")
|
print("Failed to import file: \(error)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.sheet(isPresented: $isShowingInstalledApps) {
|
.sheet(isPresented: $isShowingInstalledApps) {
|
||||||
|
|
@ -136,6 +237,12 @@ struct HomeView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private func checkPairingFileExists() {
|
||||||
|
pairingFileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
|
||||||
|
}
|
||||||
|
|
||||||
private func refreshBackground() {
|
private func refreshBackground() {
|
||||||
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
|
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,63 +8,101 @@
|
||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct InstalledAppsListView: View {
|
struct InstalledAppsListView: View {
|
||||||
@AppStorage("username") private var username = "User"
|
@StateObject private var viewModel = InstalledAppsViewModel()
|
||||||
@AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000"
|
@State private var appIcons: [String: UIImage] = [:]
|
||||||
@State private var selectedBackgroundColor: Color = Color(hex: UserDefaults.standard.string(forKey: "customBackgroundColor") ?? "#000000") ?? Color.primaryBackground
|
@Environment(\.dismiss) private var dismiss
|
||||||
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect()
|
@Environment(\.colorScheme) private var colorScheme
|
||||||
@StateObject var viewModel = InstalledAppsViewModel()
|
var onSelectApp: (String) -> Void
|
||||||
@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.filter { $0.value.localizedCaseInsensitiveContains(searchText) }.map { $0.key }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
NavigationView {
|
||||||
ZStack {
|
ScrollView {
|
||||||
selectedBackgroundColor.edgesIgnoringSafeArea(.all)
|
LazyVStack(spacing: 0) {
|
||||||
|
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
|
||||||
List {
|
|
||||||
ForEach(filteredApps, id: \.self) { bundleId in
|
|
||||||
Button(action: {
|
Button(action: {
|
||||||
onSelect(bundleId) // Select using the app's bundle ID
|
withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
|
||||||
|
onSelectApp(bundleID)
|
||||||
|
}
|
||||||
}) {
|
}) {
|
||||||
Text(viewModel.apps[bundleId] ?? "Unknown App")
|
HStack(spacing: 16) {
|
||||||
.font(.system(.body, design: .rounded))
|
// App Icon
|
||||||
.padding(.vertical, 8)
|
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))
|
.buttonStyle(PlainButtonStyle())
|
||||||
.cornerRadius(8)
|
|
||||||
}
|
if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key {
|
||||||
}
|
Divider()
|
||||||
.listStyle(InsetGroupedListStyle())
|
.padding(.leading, 96)
|
||||||
.navigationTitle("Installed Apps")
|
.padding(.trailing, 20)
|
||||||
.toolbar {
|
.opacity(0.4)
|
||||||
ToolbarItem(placement: .cancellationAction) {
|
|
||||||
Button("Done") {
|
|
||||||
dismiss()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.searchable(text: $searchText, prompt: "Search Apps")
|
.background(Color(UIColor.systemBackground))
|
||||||
}
|
}
|
||||||
.onReceive(timer) { _ in
|
.background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
|
||||||
refreshBackground()
|
.navigationTitle("Installed Apps")
|
||||||
|
.navigationBarItems(leading: Button("Done") {
|
||||||
|
dismiss()
|
||||||
}
|
}
|
||||||
.onAppear {
|
.font(.system(size: 17, weight: .regular))
|
||||||
print(filteredApps)
|
.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 }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,10 @@ struct SettingsView: View {
|
||||||
|
|
||||||
@State private var selectedBackgroundColor: Color = Color.primaryBackground
|
@State private var selectedBackgroundColor: Color = Color.primaryBackground
|
||||||
@State private var showIconPopover = false
|
@State private var showIconPopover = false
|
||||||
|
@State private var showPairingFileMessage = false
|
||||||
|
@State private var pairingFileIsValid = false
|
||||||
|
@State private var isImportingFile = false
|
||||||
|
@State private var importProgress: Float = 0.0
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
|
|
@ -60,6 +64,58 @@ struct SettingsView: View {
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.cardBackground)
|
.listRowBackground(Color.cardBackground)
|
||||||
|
|
||||||
|
if isImportingFile {
|
||||||
|
VStack(spacing: 8) {
|
||||||
|
HStack {
|
||||||
|
Text("Processing pairing file...")
|
||||||
|
.font(.system(.caption, design: .rounded))
|
||||||
|
.foregroundColor(.secondaryText)
|
||||||
|
Spacer()
|
||||||
|
Text("\(Int(importProgress * 100))%")
|
||||||
|
.font(.system(.caption, design: .rounded))
|
||||||
|
.foregroundColor(.secondaryText)
|
||||||
|
}
|
||||||
|
|
||||||
|
GeometryReader { geometry in
|
||||||
|
ZStack(alignment: .leading) {
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color.cardBackground)
|
||||||
|
.frame(height: 8)
|
||||||
|
|
||||||
|
RoundedRectangle(cornerRadius: 4)
|
||||||
|
.fill(Color.green)
|
||||||
|
.frame(width: geometry.size.width * CGFloat(importProgress), height: 8)
|
||||||
|
.animation(.linear(duration: 0.3), value: importProgress)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 8)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
.listRowBackground(Color.cardBackground)
|
||||||
|
}
|
||||||
|
|
||||||
|
if showPairingFileMessage && pairingFileIsValid {
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text("✓ Pairing file successfully imported")
|
||||||
|
.font(.system(.callout, design: .rounded))
|
||||||
|
.foregroundColor(.green)
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.padding(.horizontal, 12)
|
||||||
|
.background(Color.green.opacity(0.1))
|
||||||
|
.cornerRadius(8)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
.padding(.vertical, 4)
|
||||||
|
.listRowBackground(Color.cardBackground)
|
||||||
|
.transition(
|
||||||
|
.asymmetric(
|
||||||
|
insertion: .scale(scale: 0.9).combined(with: .opacity).animation(.spring(response: 0.4, dampingFraction: 0.7)),
|
||||||
|
removal: .opacity.animation(.easeOut(duration: 0.25))
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Section(header: Text("About").font(.headline).foregroundColor(.primaryText)) {
|
Section(header: Text("About").font(.headline).foregroundColor(.primaryText)) {
|
||||||
|
|
@ -116,6 +172,20 @@ struct SettingsView: View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.listRowBackground(Color.cardBackground)
|
.listRowBackground(Color.cardBackground)
|
||||||
|
Button(action: {
|
||||||
|
if let url = URL(string: "https://apps.apple.com/us/app/stiknes/id6737158545") {
|
||||||
|
UIApplication.shared.open(url)
|
||||||
|
}
|
||||||
|
}) {
|
||||||
|
HStack {
|
||||||
|
Text("Like this app? Check out StikNES!")
|
||||||
|
.foregroundColor(.secondaryText)
|
||||||
|
Spacer()
|
||||||
|
Image(systemName: "arrow.up.right.square")
|
||||||
|
.foregroundColor(.primaryText)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.listRowBackground(Color.cardBackground)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.background(selectedBackgroundColor)
|
.background(selectedBackgroundColor)
|
||||||
|
|
@ -139,9 +209,37 @@ struct SettingsView: View {
|
||||||
|
|
||||||
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||||
print("File copied successfully!")
|
print("File copied successfully!")
|
||||||
|
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
isImportingFile = true
|
||||||
|
importProgress = 0.0
|
||||||
|
}
|
||||||
|
|
||||||
|
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
|
||||||
|
DispatchQueue.main.async {
|
||||||
|
if importProgress < 1.0 {
|
||||||
|
importProgress += 0.25
|
||||||
|
} else {
|
||||||
|
timer.invalidate()
|
||||||
|
isImportingFile = false
|
||||||
|
pairingFileIsValid = true
|
||||||
|
|
||||||
|
withAnimation {
|
||||||
|
showPairingFileMessage = true
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||||
|
withAnimation {
|
||||||
|
showPairingFileMessage = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
startHeartbeatInBackground()
|
startHeartbeatInBackground()
|
||||||
|
|
||||||
Thread.sleep(forTimeInterval: 5)
|
RunLoop.current.add(progressTimer, forMode: .common)
|
||||||
} catch {
|
} catch {
|
||||||
print("Error copying file: \(error)")
|
print("Error copying file: \(error)")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue