diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index d6cd0c8..d7f5ded 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -126,7 +126,7 @@ struct HomeView: View { } class InstalledAppsViewModel: ObservableObject { - @Published var apps: [String] = [] + @Published var apps: [String: String] = [:] init() { loadApps() @@ -134,78 +134,40 @@ class InstalledAppsViewModel: ObservableObject { func loadApps() { guard let rawPointer = list_installed_apps() else { - self.apps = [] + self.apps = [:] return } let output = String(cString: rawPointer) free(rawPointer) - - if output.hasPrefix("Error:") { - self.apps = [] - } else { - self.apps = output.components(separatedBy: "\n").filter { !$0.isEmpty } + + guard let jsonData = output.data(using: .utf8) else { + print("Error: Failed to convert string to data") + self.apps = [:] + return } + + if output.hasPrefix("{\"error\":") { + self.apps = [:] + } + + print(output) + + // Decode the JSON into a Swift dictionary + do { + let decoder = JSONDecoder() + let apps = try decoder.decode([String: String].self, from: jsonData) + self.apps = apps + return + } catch { + print("Error: Failed to decode JSON - \(error)") + self.apps = [:] + return + } + } } -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 viewModel.apps - } else { - return viewModel.apps.filter { $0.localizedCaseInsensitiveContains(searchText) } - } - } - - var body: some View { - NavigationView { - ZStack { - selectedBackgroundColor .edgesIgnoringSafeArea(.all) - - List { - ForEach(filteredApps, id: \.self) { app in - Button(action: { - onSelect(app) - }) { - Text(app) - .font(.system(.body, design: .rounded)) - .padding(.vertical, 8) - } - .listRowBackground(Color(.secondarySystemGroupedBackground)) - .cornerRadius(8) - } - } - .listStyle(InsetGroupedListStyle()) - .navigationTitle("Installed Apps") - .toolbar { - ToolbarItem(placement: .cancellationAction) { - Button("Done") { - dismiss() - } - } - } - .searchable(text: $searchText, prompt: "Search Apps") - } - .onReceive(timer) { _ in - refreshBackground() - } - } - } - private func refreshBackground() { - selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground - } -} #Preview { diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift new file mode 100644 index 0000000..f11c2b8 --- /dev/null +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -0,0 +1,70 @@ +// +// InstalledAppsListView.swift +// StikJIT +// +// Created by Stossy11 on 28/03/2025. +// + +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) } + } + } + + var body: some View { + NavigationView { + ZStack { + selectedBackgroundColor.edgesIgnoringSafeArea(.all) + + List { + ForEach(filteredApps, id: \.self) { app in + Button(action: { + onSelect(viewModel.apps[app] ?? "") // Select using the app's bundle ID + }) { + Text(app) + .font(.system(.body, design: .rounded)) + .padding(.vertical, 8) + } + .listRowBackground(Color(.secondarySystemGroupedBackground)) + .cornerRadius(8) + } + } + .listStyle(InsetGroupedListStyle()) + .navigationTitle("Installed Apps") + .toolbar { + ToolbarItem(placement: .cancellationAction) { + Button("Done") { + dismiss() + } + } + } + .searchable(text: $searchText, prompt: "Search Apps") + } + .onReceive(timer) { _ in + refreshBackground() + } + .onAppear { + print(filteredApps) + } + } + } + + private func refreshBackground() { + selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground + } +} diff --git a/StikJIT/idevice/applist.c b/StikJIT/idevice/applist.c index 061f30e..b018c7d 100644 --- a/StikJIT/idevice/applist.c +++ b/StikJIT/idevice/applist.c @@ -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,13 +60,15 @@ 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(8192); // Allocate memory for output result[0] = '\0'; + strcat(result, "{\n"); + int first_entry = 1; for (size_t i = 0; i < apps_len; i++) { plist_t app = app_list[i]; // Check if the app has an "Entitlements" dictionary. @@ -83,15 +85,59 @@ char *list_installed_apps() { if (bundle_id_node) { char *bundle_id = NULL; plist_get_string_val(bundle_id_node, &bundle_id); - strcat(result, bundle_id); - strcat(result, "\n"); + + // Skip if bundle ID is empty + if (bundle_id == NULL || strlen(bundle_id) == 0) { + free(bundle_id); + continue; + } + + // Retrieve the app name + plist_t app_name_node = plist_dict_get_item(app, "CFBundleName"); + char *app_name = NULL; + if (app_name_node) { + plist_get_string_val(app_name_node, &app_name); + } else { + app_name = strdup("Unknown"); + } + + // Escape special characters in app name and bundle ID + char escaped_app_name[1024] = {0}; + char escaped_bundle_id[1024] = {0}; + for (int j = 0, k = 0; app_name[j] != '\0'; j++, k++) { + if (app_name[j] == '"' || app_name[j] == '\\') { + escaped_app_name[k++] = '\\'; + } + escaped_app_name[k] = app_name[j]; + } + for (int j = 0, k = 0; bundle_id[j] != '\0'; j++, k++) { + if (bundle_id[j] == '"' || bundle_id[j] == '\\') { + escaped_bundle_id[k++] = '\\'; + } + escaped_bundle_id[k] = bundle_id[j]; + } + + // Add the app name and bundle ID to the result in JSON format + if (!first_entry) { + strcat(result, ",\n"); + } + strcat(result, " \""); + strcat(result, escaped_app_name); + strcat(result, "\": \""); + strcat(result, escaped_bundle_id); + strcat(result, "\""); + + first_entry = 0; free(bundle_id); + free(app_name); } } } } } + strcat(result, "\n}\n"); + installation_proxy_client_free(client); tcp_provider_free(provider);