mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 12:30:29 +00:00
Revert "clean up main loading screen + fetching apps (test before merging)"
This commit is contained in:
parent
29d34e32ce
commit
48a7ba5fb0
4 changed files with 56 additions and 453 deletions
|
|
@ -439,7 +439,7 @@
|
|||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
|
|
@ -485,7 +485,7 @@
|
|||
"INFOPLIST_KEY_UIStatusBarStyle[sdk=iphonesimulator*]" = UIStatusBarStyleDefault;
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 17.4;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 18.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
|
||||
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
|
||||
LIBRARY_SEARCH_PATHS = (
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@
|
|||
import SwiftUI
|
||||
import UniformTypeIdentifiers
|
||||
|
||||
|
||||
extension UIDocumentPickerViewController {
|
||||
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
|
||||
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
|
||||
|
|
@ -24,10 +23,6 @@ struct HomeView: View {
|
|||
@State private var isProcessing = false
|
||||
@State private var isShowingInstalledApps = false
|
||||
@State private var isShowingPairingFilePicker = false
|
||||
@State private var pairingFileExists: Bool = false
|
||||
@State private var pairingFileMessage: String = ""
|
||||
@State private var showPairingFileMessage: Bool = false
|
||||
@State private var pairingFileIsValid: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
|
|
@ -40,188 +35,69 @@ struct HomeView: View {
|
|||
.font(.system(.largeTitle, design: .rounded))
|
||||
.fontWeight(.bold)
|
||||
|
||||
Text("Click enable JIT to get started")
|
||||
Text("Click enable jit to get started")
|
||||
.font(.system(.subheadline, design: .rounded))
|
||||
.foregroundStyle(.secondary)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.padding(.top, 40)
|
||||
|
||||
// Main action button - changes based on whether we have a pairing file
|
||||
Button(action: {
|
||||
if pairingFileExists {
|
||||
// Got a pairing file, show apps
|
||||
isShowingInstalledApps = true
|
||||
} else {
|
||||
// No pairing file yet, let's get one
|
||||
if !FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
||||
isShowingPairingFilePicker = true
|
||||
} else {
|
||||
isShowingInstalledApps = true
|
||||
}
|
||||
}) {
|
||||
HStack {
|
||||
Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus")
|
||||
.font(.system(size: 20))
|
||||
Text(pairingFileExists ? "Enable JIT" : "Import Pairing File")
|
||||
.font(.system(.title3, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
}
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
Label("Enable JIT", systemImage: "list.bullet")
|
||||
.font(.system(.title3, design: .rounded))
|
||||
.fontWeight(.semibold)
|
||||
.frame(maxWidth: .infinity)
|
||||
.padding()
|
||||
.background(Color.blue)
|
||||
.foregroundColor(.white)
|
||||
.cornerRadius(16)
|
||||
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4)
|
||||
}
|
||||
.padding(.horizontal, 20)
|
||||
|
||||
// Status message area - keeps layout consistent
|
||||
ZStack {
|
||||
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: 30)
|
||||
|
||||
// Only show error messages when needed
|
||||
if showPairingFileMessage && !pairingFileIsValid {
|
||||
Text(pairingFileMessage)
|
||||
.font(.system(.callout, design: .rounded))
|
||||
.foregroundColor(.red)
|
||||
.padding(.vertical, 4)
|
||||
.multilineTextAlignment(.center)
|
||||
.transition(.opacity)
|
||||
}
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding()
|
||||
}
|
||||
.onAppear {
|
||||
checkPairingFileExists()
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
refreshBackground()
|
||||
checkPairingFileExists()
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!]
|
||||
) { result in
|
||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
|
||||
switch result {
|
||||
|
||||
case .success(let url):
|
||||
guard url.startAccessingSecurityScopedResource() else {
|
||||
pairingFileMessage = "Failed to access the selected file"
|
||||
showPairingFileMessage = true
|
||||
pairingFileIsValid = false
|
||||
return
|
||||
}
|
||||
|
||||
let fileManager = FileManager.default
|
||||
|
||||
// Make sure we got the right file type
|
||||
let fileExtension = url.pathExtension.lowercased()
|
||||
guard fileExtension == "plist" || fileExtension == "mobiledevicepairing" else {
|
||||
pairingFileMessage = "Invalid file type. Please select a .plist or .mobiledevicepairing file."
|
||||
showPairingFileMessage = true
|
||||
pairingFileIsValid = false
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
return
|
||||
}
|
||||
let accessing = url.startAccessingSecurityScopedResource()
|
||||
|
||||
if fileManager.fileExists(atPath: url.path) {
|
||||
do {
|
||||
// Clear any existing pairing file first
|
||||
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
||||
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||
}
|
||||
|
||||
// Save the file to our docs directory
|
||||
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||
print("File copied successfully!")
|
||||
startHeartbeatInBackground()
|
||||
|
||||
// Check if it's actually a valid pairing file
|
||||
do {
|
||||
let data = try Data(contentsOf: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||
if let plist = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
|
||||
// Look for keys we'd expect in a device pairing file
|
||||
if plist["DeviceCertificate"] != nil || plist["HostCertificate"] != nil ||
|
||||
plist["WiFiMACAddress"] != nil || plist["DeviceID"] != nil {
|
||||
pairingFileMessage = "" // We'll use a fixed success message
|
||||
pairingFileIsValid = true
|
||||
pairingFileExists = true
|
||||
|
||||
// Start heartbeat
|
||||
startHeartbeatInBackground()
|
||||
|
||||
// Show success message briefly
|
||||
withAnimation(.easeIn(duration: 0.2)) {
|
||||
showPairingFileMessage = true
|
||||
}
|
||||
|
||||
// Clean up after 3 secs
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation(.easeOut(duration: 0.2)) {
|
||||
showPairingFileMessage = false
|
||||
}
|
||||
}
|
||||
|
||||
// Let user click the button themselves
|
||||
} else {
|
||||
pairingFileMessage = "File is a plist but doesn't appear to be a valid pairing file."
|
||||
pairingFileIsValid = false
|
||||
showPairingFileMessage = true
|
||||
}
|
||||
} else {
|
||||
pairingFileMessage = "Invalid pairing file format."
|
||||
pairingFileIsValid = false
|
||||
showPairingFileMessage = true
|
||||
}
|
||||
} catch {
|
||||
pairingFileMessage = "Could not validate pairing file: \(error.localizedDescription)"
|
||||
pairingFileIsValid = false
|
||||
showPairingFileMessage = true
|
||||
}
|
||||
Thread.sleep(forTimeInterval: 5)
|
||||
} catch {
|
||||
pairingFileMessage = "Error copying file: \(error.localizedDescription)"
|
||||
showPairingFileMessage = true
|
||||
pairingFileIsValid = false
|
||||
print("Error copying file: \(error)")
|
||||
}
|
||||
} else {
|
||||
pairingFileMessage = "Source file does not exist."
|
||||
showPairingFileMessage = true
|
||||
pairingFileIsValid = false
|
||||
print("Source file does not exist.")
|
||||
}
|
||||
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
|
||||
// Auto-hide error messages too
|
||||
if !pairingFileIsValid && showPairingFileMessage {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation {
|
||||
showPairingFileMessage = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
case .failure(let error):
|
||||
pairingFileMessage = "Failed to import file: \(error.localizedDescription)"
|
||||
showPairingFileMessage = true
|
||||
pairingFileIsValid = false
|
||||
|
||||
// Hide error after a few seconds
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
|
||||
withAnimation {
|
||||
showPairingFileMessage = false
|
||||
}
|
||||
if accessing {
|
||||
url.stopAccessingSecurityScopedResource()
|
||||
}
|
||||
case .failure(_):
|
||||
print("Failed")
|
||||
}
|
||||
}
|
||||
.sheet(isPresented: $isShowingInstalledApps) {
|
||||
|
|
@ -234,24 +110,6 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
private func checkPairingFileExists() {
|
||||
let exists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
|
||||
pairingFileExists = exists
|
||||
|
||||
// If the file exists and we haven't validated it yet, validate it
|
||||
if exists && !pairingFileIsValid {
|
||||
do {
|
||||
let data = try Data(contentsOf: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
|
||||
if let _ = try PropertyListSerialization.propertyList(from: data, options: [], format: nil) as? [String: Any] {
|
||||
pairingFileIsValid = true
|
||||
}
|
||||
} catch {
|
||||
// Silently fail - we don't want to show an error message on every timer tick
|
||||
pairingFileIsValid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshBackground() {
|
||||
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
|
||||
}
|
||||
|
|
@ -274,23 +132,8 @@ struct HomeView: View {
|
|||
}
|
||||
}
|
||||
|
||||
// Replace the AppInfo struct with a simplified version
|
||||
struct AppInfo: Identifiable, Hashable {
|
||||
let id = UUID()
|
||||
let bundleID: String
|
||||
let name: String
|
||||
|
||||
func hash(into hasher: inout Hasher) {
|
||||
hasher.combine(bundleID)
|
||||
}
|
||||
|
||||
static func == (lhs: AppInfo, rhs: AppInfo) -> Bool {
|
||||
return lhs.bundleID == rhs.bundleID
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledAppsViewModel: ObservableObject {
|
||||
@Published var apps: [AppInfo] = []
|
||||
@Published var apps: [String: String] = [:]
|
||||
|
||||
init() {
|
||||
loadApps()
|
||||
|
|
@ -298,274 +141,42 @@ class InstalledAppsViewModel: ObservableObject {
|
|||
|
||||
func loadApps() {
|
||||
guard let rawPointer = list_installed_apps() else {
|
||||
self.apps = []
|
||||
self.apps = [:]
|
||||
return
|
||||
}
|
||||
|
||||
let output = String(cString: rawPointer)
|
||||
free(rawPointer)
|
||||
|
||||
// Try to parse JSON format first
|
||||
if let jsonData = output.data(using: .utf8) {
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let appsDict = try decoder.decode([String: String].self, from: jsonData)
|
||||
if let app = appsDict.first, app.key == "error" {
|
||||
self.apps = []
|
||||
} else {
|
||||
self.apps = appsDict.map { AppInfo(bundleID: $0.key, name: $0.value) }
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
print("Error decoding JSON, falling back to text parsing: \(error.localizedDescription)")
|
||||
// Fall through to text parsing below
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback to text parsing
|
||||
if output.hasPrefix("Error:") {
|
||||
self.apps = []
|
||||
} else {
|
||||
// Parse the output into AppInfo objects
|
||||
self.apps = output.components(separatedBy: "\n")
|
||||
.filter { !$0.isEmpty }
|
||||
.compactMap { line -> AppInfo? in
|
||||
let components = line.components(separatedBy: "|")
|
||||
if components.count >= 2 {
|
||||
return AppInfo(bundleID: components[0], name: components[1])
|
||||
} else if components.count == 1 {
|
||||
return AppInfo(bundleID: components[0], name: components[0])
|
||||
}
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 = ""
|
||||
@State private var appIcons: [String: UIImage] = [:]
|
||||
|
||||
var onSelect: (String) -> Void
|
||||
|
||||
var filteredApps: [AppInfo] {
|
||||
if searchText.isEmpty {
|
||||
return viewModel.apps
|
||||
} else {
|
||||
return viewModel.apps.filter {
|
||||
$0.bundleID.localizedCaseInsensitiveContains(searchText) ||
|
||||
$0.name.localizedCaseInsensitiveContains(searchText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
NavigationView {
|
||||
ZStack {
|
||||
selectedBackgroundColor.edgesIgnoringSafeArea(.all)
|
||||
|
||||
List {
|
||||
ForEach(filteredApps) { app in
|
||||
Button(action: {
|
||||
onSelect(app.bundleID)
|
||||
}) {
|
||||
HStack {
|
||||
// Simplified app icon display
|
||||
appIconView(for: app)
|
||||
.padding(.trailing, 8)
|
||||
|
||||
// App details
|
||||
appDetailsView(for: app)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(systemName: "chevron.right")
|
||||
.font(.system(size: 14, weight: .semibold))
|
||||
.foregroundColor(.gray.opacity(0.7))
|
||||
}
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.listRowBackground(Color(.secondarySystemGroupedBackground))
|
||||
}
|
||||
}
|
||||
.listStyle(InsetGroupedListStyle())
|
||||
.navigationTitle("Installed Apps")
|
||||
.toolbar {
|
||||
ToolbarItem(placement: .cancellationAction) {
|
||||
Button("Done") {
|
||||
dismiss()
|
||||
}
|
||||
}
|
||||
}
|
||||
.searchable(text: $searchText, prompt: "Search Apps")
|
||||
}
|
||||
.onReceive(timer) { _ in
|
||||
refreshBackground()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Helper view for app icon
|
||||
private func appIconView(for app: AppInfo) -> some View {
|
||||
Group {
|
||||
if let icon = appIcons[app.bundleID] {
|
||||
// Show cached icon
|
||||
Image(uiImage: icon)
|
||||
.resizable()
|
||||
.aspectRatio(contentMode: .fit)
|
||||
.frame(width: 48, height: 48)
|
||||
.cornerRadius(12)
|
||||
} else {
|
||||
// Simple generic placeholder for apps without icons
|
||||
placeholderIconView()
|
||||
.onAppear {
|
||||
loadAppIcon(for: app)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Simplified placeholder without complex color/symbol logic
|
||||
private func placeholderIconView() -> some View {
|
||||
ZStack {
|
||||
RoundedRectangle(cornerRadius: 12)
|
||||
.fill(Color.gray.opacity(0.3))
|
||||
.frame(width: 48, height: 48)
|
||||
|
||||
Image(systemName: "app")
|
||||
.font(.system(size: 24))
|
||||
.foregroundColor(.gray)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper view for app details
|
||||
private func appDetailsView(for app: AppInfo) -> some View {
|
||||
VStack(alignment: .leading, spacing: 4) {
|
||||
Text(app.name)
|
||||
.font(.system(.body, design: .rounded))
|
||||
.fontWeight(.medium)
|
||||
|
||||
Text(app.bundleID)
|
||||
.font(.system(.caption, design: .rounded))
|
||||
.foregroundColor(.secondary)
|
||||
}
|
||||
}
|
||||
|
||||
// Helper method to load app icon
|
||||
private func loadAppIcon(for app: AppInfo) {
|
||||
// Use AppStoreIconFetcher for now
|
||||
AppStoreIconFetcher.getIcon(for: app.bundleID) { image in
|
||||
if let image = image {
|
||||
self.appIcons[app.bundleID] = image
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func refreshBackground() {
|
||||
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
|
||||
}
|
||||
}
|
||||
|
||||
// Add this class before the #Preview section
|
||||
class AppStoreIconFetcher {
|
||||
static private var iconCache: [String: UIImage] = [:]
|
||||
|
||||
static func getIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) {
|
||||
// Check our cache first - why waste bandwidth
|
||||
if let cachedIcon = iconCache[bundleID] {
|
||||
completion(cachedIcon)
|
||||
guard let jsonData = output.data(using: .utf8) else {
|
||||
print("Error: Failed to convert string to data")
|
||||
self.apps = [:]
|
||||
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) }
|
||||
print(output)
|
||||
|
||||
// Decode the JSON into a Swift dictionary
|
||||
do {
|
||||
let decoder = JSONDecoder()
|
||||
let apps = try decoder.decode([String: String].self, from: jsonData)
|
||||
if let app = apps.first, app.key == "error" {
|
||||
self.apps = [:]
|
||||
} else {
|
||||
self.apps = apps
|
||||
}
|
||||
return
|
||||
} catch {
|
||||
print("Error: Failed to decode JSON - \(error)")
|
||||
self.apps = [:]
|
||||
return
|
||||
}
|
||||
|
||||
// Three step process:
|
||||
// 1. Get app data from iTunes
|
||||
fetchAppData(from: url) { appData in
|
||||
if let appData = appData {
|
||||
// 2. Extract the icon URL
|
||||
extractIconURL(from: appData) { iconURL in
|
||||
if let iconURL = iconURL {
|
||||
// 3. Download the actual icon
|
||||
downloadIcon(from: iconURL) { image in
|
||||
if let image = image {
|
||||
// Save it for next time
|
||||
iconCache[bundleID] = image
|
||||
}
|
||||
DispatchQueue.main.async {
|
||||
completion(image)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Boring network stuff below
|
||||
private static func fetchAppData(from url: URL, completion: @escaping ([String: Any]?) -> Void) {
|
||||
URLSession.shared.dataTask(with: url) { data, response, error in
|
||||
guard let data = data, error == nil else {
|
||||
completion(nil)
|
||||
return
|
||||
}
|
||||
|
||||
do {
|
||||
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
|
||||
let results = json["results"] as? [[String: Any]],
|
||||
!results.isEmpty {
|
||||
completion(results.first)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
} catch {
|
||||
completion(nil)
|
||||
}
|
||||
}.resume()
|
||||
}
|
||||
|
||||
// Extract the high-res icon URL
|
||||
private static func extractIconURL(from appData: [String: Any], completion: @escaping (URL?) -> Void) {
|
||||
if let iconURLString = appData["artworkUrl100"] as? String,
|
||||
let iconURL = URL(string: iconURLString) {
|
||||
completion(iconURL)
|
||||
} else {
|
||||
completion(nil)
|
||||
}
|
||||
}
|
||||
|
||||
// Grab the image data and convert to UIImage
|
||||
private static func downloadIcon(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()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
#Preview {
|
||||
HomeView()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,10 +112,7 @@ struct SettingsView: View {
|
|||
.font(.bodyFont)
|
||||
.accentColor(.accentColor)
|
||||
}
|
||||
.fileImporter(
|
||||
isPresented: $isShowingPairingFilePicker,
|
||||
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!, .propertyList]
|
||||
) {result in
|
||||
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
|
||||
switch result {
|
||||
|
||||
case .success(let url):
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ char *list_installed_apps() {
|
|||
addr.sin_family = AF_INET;
|
||||
addr.sin_port = htons(LOCKDOWN_PORT);
|
||||
if (inet_pton(AF_INET, "10.7.0.1", &addr.sin_addr) <= 0) {
|
||||
return strdup("Error: Invalid IP address");
|
||||
return strdup("{\"error\": \"Invalid IP address\"}");
|
||||
}
|
||||
|
||||
char pairingFilePath[1024];
|
||||
|
|
@ -37,21 +37,21 @@ char *list_installed_apps() {
|
|||
IdevicePairingFile *pairing_file = NULL;
|
||||
IdeviceErrorCode err = idevice_pairing_file_read(pairingFilePath, &pairing_file);
|
||||
if (err != IdeviceSuccess) {
|
||||
return strdup("Error: Failed to read pairing file");
|
||||
return strdup("{\"error\": \"Failed to read pairing file\"}");
|
||||
}
|
||||
|
||||
TcpProviderHandle *provider = NULL;
|
||||
err = idevice_tcp_provider_new((struct sockaddr *)&addr, pairing_file, "ExampleProvider", &provider);
|
||||
if (err != IdeviceSuccess) {
|
||||
idevice_pairing_file_free(pairing_file);
|
||||
return strdup("Error: Failed to create TCP provider");
|
||||
return strdup("{\"error\": \"Failed to create TCP provider\"}");
|
||||
}
|
||||
|
||||
InstallationProxyClientHandle *client = NULL;
|
||||
err = installation_proxy_connect_tcp(provider, &client);
|
||||
if (err != IdeviceSuccess) {
|
||||
tcp_provider_free(provider);
|
||||
return strdup("Error: Failed to connect to installation proxy");
|
||||
return strdup("{\"error\": \"Failed to connect to installation proxy\"}");
|
||||
}
|
||||
|
||||
void *apps = NULL;
|
||||
|
|
@ -60,11 +60,11 @@ char *list_installed_apps() {
|
|||
if (err != IdeviceSuccess) {
|
||||
installation_proxy_client_free(client);
|
||||
tcp_provider_free(provider);
|
||||
return strdup("Error: Failed to get apps");
|
||||
return strdup("{\"error\": \"Failed to get apps\"}");
|
||||
}
|
||||
|
||||
plist_t *app_list = (plist_t *)apps;
|
||||
char *result = malloc(16384); // Increased buffer size for additional data
|
||||
char *result = malloc(8192); // Allocate memory for output
|
||||
result[0] = '\0';
|
||||
strcat(result, "{\n");
|
||||
|
||||
|
|
@ -143,8 +143,3 @@ char *list_installed_apps() {
|
|||
|
||||
return result;
|
||||
}
|
||||
|
||||
char *fetch_app_icon(const char *bundle_path) {
|
||||
// yall got this trust
|
||||
return NULL;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue