Merge branch 'resolve-conflicts' into main

This commit is contained in:
neoarz 2025-03-27 18:15:43 -04:00 • committed by GitHub
commit ae4afaff80
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 341 additions and 31 deletions

67
.github/workflows/build_ipa.yml vendored Normal file
View file

@ -0,0 +1,67 @@
name: Build Unsigned IPA
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
workflow_dispatch:
jobs:
build:
name: Build Unsigned IPA
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Setup Xcode
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: 'latest'
- name: Fix Project Format
run: |
sed -i '' 's/objectVersion = 77/objectVersion = 56/' StikJIT.xcodeproj/project.pbxproj
sed -i '' 's/preferredProjectObjectVersion = 77/preferredProjectObjectVersion = 56/' StikJIT.xcodeproj/project.pbxproj
- name: List Available SDKs
run: xcodebuild -showsdks
- name: List Available Schemes
run: xcodebuild -list -project StikJit.xcodeproj
- name: Archive Build (Unsigned)
run: |
xcodebuild clean archive \
-project StikJIT.xcodeproj \
-scheme "StikJIT" \
-configuration Release \
-archivePath build/StikJit.xcarchive \
-sdk iphoneos \
-destination 'generic/platform=iOS' \
ONLY_ACTIVE_ARCH=NO \
CODE_SIGN_IDENTITY="" \
CODE_SIGNING_REQUIRED=NO \
CODE_SIGNING_ALLOWED=NO \
SWIFT_OPTIMIZATION_LEVEL="-Onone" \
IPHONEOS_DEPLOYMENT_TARGET=17.4
- name: Create IPA from Archive
run: |
# Copy the .app from the archive
cp -R build/StikJit.xcarchive/Products/Applications/StikJit.app .
# Package the app into an IPA
mkdir -p Payload
cp -R StikJit.app Payload/
zip -r StikJit.ipa Payload
rm -rf Payload StikJit.app
- name: Upload IPA
uses: actions/upload-artifact@v4
with:
name: StikJit.ipa
path: StikJit.ipa
retention-days: 90

View file

@ -7,24 +7,77 @@
import SwiftUI
import em_proxy
import UniformTypeIdentifiers
@main
struct HeartbeatApp: App {
@State private var isLoading = true
@State private var isPairing = false
@State private var heartBeat = false
@State private var error: Int32? = nil
init() {
let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))!
let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))!
method_exchangeImplementations(origMethod, fixMethod)
}
var body: some Scene {
WindowGroup {
if isLoading {
LoadingView()
.onAppear {
DispatchQueue.main.asyncAfter(deadline: .now() + 5) {
withAnimation(.easeInOut(duration: 1.0)) {
isLoading = false
}
}
startProxy()
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
if heartBeat {
isLoading = false
timer.invalidate()
} else {
if let error {
if error == InvalidHostID.rawValue {
isPairing = true
} else {
startHeartbeatInBackground()
}
self.error = nil
}
}
}
startHeartbeatInBackground()
} else {
isLoading = false
}
}
.fileImporter(isPresented: $isPairing, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
switch result {
case .success(let url):
let fileManager = FileManager.default
let accessing = url.startAccessingSecurityScopedResource()
if fileManager.fileExists(atPath: url.path) {
do {
if fileManager.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
try fileManager.removeItem(at: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
}
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!")
startHeartbeatInBackground()
} catch {
print("Error copying file: \(error)")
}
} else {
print("Source file does not exist.")
}
if accessing {
url.stopAccessingSecurityScopedResource()
}
case .failure(_):
print("Failed")
}
}
} else {
@ -50,16 +103,57 @@ struct HeartbeatApp: App {
}
}
}
func startHeartbeatInBackground() {
let heartBeat = Thread {
let cCompletionHandler: @convention(block) (Int32, UnsafePointer<CChar>?) -> Void = { result, messagePointer in
let message: String? = messagePointer != nil ? String(cString: messagePointer!) : nil
if result == 0 {
print("Heartbeat started successfully: \(message ?? "")")
self.heartBeat = true
} else {
print("Error: \(result == InvalidHostID.rawValue ? "Invalid host ID, Please Selecr New Pairing File" : message ?? "") (Code: \(result))")
showAlert(title: "HeartBeat Error", message: "\(message ?? "") (\(result))", showOk: true) { _ in
self.error = result
}
}
}
startHeartbeat(cCompletionHandler)
}
heartBeat.qualityOfService = .background
heartBeat.name = "HeartBeat"
heartBeat.start()
}
}
func startHeartbeatInBackground() {
let heartBeat = Thread {
startHeartbeat()
let cCompletionHandler: @convention(block) (Int32, UnsafePointer<CChar>?) -> Void = { result, messagePointer in
let message: String? = messagePointer != nil ? String(cString: messagePointer!) : nil
if result == 0 {
print("Heartbeat started successfully: \(message ?? "")")
} else {
print("Error: \(message ?? "") (Code: \(result))")
showAlert(title: "HeartBeat Error", message: "\(message ?? "") (\(result))", showOk: true) { _ in
startHeartbeatInBackground()
}
}
}
startHeartbeat(cCompletionHandler)
}
heartBeat.qualityOfService = .background
heartBeat.name = "HeartBeat"
heartBeat.name = "Heartbeat"
heartBeat.start()
}
@ -106,3 +200,23 @@ struct LoadingView: View {
}
}
}
public func showAlert(title: String, message: String, showOk: Bool, completion: @escaping (Bool) -> Void) {
DispatchQueue.main.async {
if let mainWindow = UIApplication.shared.windows.last {
let alert = UIAlertController(title: title, message: message, preferredStyle: .alert)
if showOk {
let okAction = UIAlertAction(title: "OK", style: .default) { _ in
completion(true)
}
alert.addAction(okAction)
} else {
completion(false)
}
mainWindow.rootViewController?.present(alert, animated: true, completion: nil)
}
}
}

View file

@ -8,6 +8,13 @@
import SwiftUI
import UniformTypeIdentifiers
extension UIDocumentPickerViewController {
@objc func fix_init(forOpeningContentTypes contentTypes: [UTType], asCopy: Bool) -> UIDocumentPickerViewController {
return fix_init(forOpeningContentTypes: contentTypes, asCopy: true)
}
}
struct HomeView: View {
@AppStorage("username") private var username = "User"
@AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000"
@ -107,7 +114,7 @@ struct HomeView: View {
}
.fileImporter(
isPresented: $isShowingPairingFilePicker,
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!]
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!]
) { result in
switch result {
case .success(let url):
@ -298,6 +305,24 @@ class InstalledAppsViewModel: ObservableObject {
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 {
@ -434,6 +459,7 @@ struct InstalledAppsListView: View {
// 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
@ -446,7 +472,7 @@ struct InstalledAppsListView: View {
}
}
// App icon stuff - just fetches from App Store
// Add this class before the #Preview section
class AppStoreIconFetcher {
static private var iconCache: [String: UIImage] = [:]

View file

@ -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
}
}

View file

@ -114,7 +114,7 @@ struct SettingsView: View {
}
.fileImporter(
isPresented: $isShowingPairingFilePicker,
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!]
allowedContentTypes: [UTType(filenameExtension: "plist")!, UTType(filenameExtension: "mobiledevicepairing")!, .propertyList]
) {result in
switch result {

View file

@ -66,7 +66,9 @@ char *list_installed_apps() {
plist_t *app_list = (plist_t *)apps;
char *result = malloc(16384); // Increased buffer size for additional data
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,37 +85,59 @@ char *list_installed_apps() {
if (bundle_id_node) {
char *bundle_id = NULL;
plist_get_string_val(bundle_id_node, &bundle_id);
// Get app name
// 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;
plist_t name_node = plist_dict_get_item(app, "CFBundleName");
if (name_node) {
plist_get_string_val(name_node, &app_name);
if (app_name_node) {
plist_get_string_val(app_name_node, &app_name);
} else {
name_node = plist_dict_get_item(app, "CFBundleDisplayName");
if (name_node) {
plist_get_string_val(name_node, &app_name);
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];
}
// Add bundle ID and app name to result
strcat(result, bundle_id);
strcat(result, "|"); // pipe as separator
if (app_name) {
strcat(result, app_name);
free(app_name);
} else {
strcat(result, bundle_id); // Use bundle ID as fallback name
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];
}
strcat(result, "\n");
// 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);

View file

@ -10,7 +10,9 @@
#include <CoreFoundation/CoreFoundation.h>
#include <limits.h>
void startHeartbeat() {
typedef void (^HeartbeatCompletionHandler)(int result, const char *message);
void startHeartbeat(HeartbeatCompletionHandler completion) {
printf("DEBUG: Initializing logger...\n");
idevice_init_logger(Debug, Disabled, NULL);
@ -43,6 +45,7 @@ void startHeartbeat() {
IdeviceErrorCode err = idevice_pairing_file_read(pairingFilePath, &pairing_file);
if (err != IdeviceSuccess) {
fprintf(stderr, "DEBUG: Failed to read pairing file: %d\n", err);
completion(err, "Failed to read pairing file");
return;
}
printf("DEBUG: Pairing file read successfully.\n");
@ -54,6 +57,7 @@ void startHeartbeat() {
if (err != IdeviceSuccess) {
fprintf(stderr, "DEBUG: Failed to create TCP provider: %d\n", err);
idevice_pairing_file_free(pairing_file);
completion(err, "Failed to create TCP provider");
return;
}
printf("DEBUG: TCP provider created successfully.\n");
@ -62,12 +66,15 @@ void startHeartbeat() {
HeartbeatClientHandle *client = NULL;
err = heartbeat_connect_tcp(provider, &client);
if (err != IdeviceSuccess) {
completion(err, "Failed to connect to Heartbeat");
fprintf(stderr, "DEBUG: Failed to connect to installation proxy: %d\n", err);
return;
}
tcp_provider_free(provider);
printf("DEBUG: Connected to installation proxy successfully.\n");
completion(0, "Heartbeat Completed");
u_int64_t current_interval = 15;
while (1) {
u_int64_t new_interval = 0;

View file

@ -9,6 +9,8 @@
#ifndef HEARTBEAT_H
#define HEARTBEAT_H
void startHeartbeat();
typedef void (^HeartbeatCompletionHandler)(int result, const char *message);
void startHeartbeat(HeartbeatCompletionHandler completion);
#endif /* HEARTBEAT_H */