diff --git a/README.md b/README.md index 5189881..b270218 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,10 @@ # StikJIT A **work-in-progress** on-device JIT enabler for iOS, powered by [`idevice`](https://github.com/jkcoxson/idevice). +[![GitHub Release](https://img.shields.io/github/v/release/0-Blu/StikJIT?include_prereleases)](https://github.com/0-Blu/StikJIT/releases) +[![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg?style=flat-square)](https://github.com/0-Blu/StikJIT/pulls) +[![GitHub License](https://img.shields.io/github/license/0-Blu/StikJIT?color=%23C96FAD&cache=none)](https://github.com/0-Blu/StikJIT/blob/main/LICENSE) +![GitHub Downloads (all assets, all releases)](https://img.shields.io/github/downloads/0-Blu/StikJIT/total) ## Requirements The [SideStore VPN](https://github.com/SideStore/SideStore/releases/download/0.1.1/SideStore.conf) is required. This allows the device to connect to itself. @@ -9,10 +13,7 @@ The [SideStore VPN](https://github.com/SideStore/SideStore/releases/download/0.1 - On-device Just-In-Time (JIT) compilation for supported apps via [`idevice`](https://github.com/jkcoxson/idevice). - Seamless integration with [`em_proxy`](https://github.com/SideStore/em_proxy). - Native UI for managing JIT-enabling. -- No data collection—ensuring full privacy. - -## Privacy Policy -StikJIT **does not collect or store any user data.** +- No data collection—ensuring full privacy. ## Compiling Instructions @@ -28,18 +29,7 @@ StikJIT **does not collect or store any user data.** 3. **Build and Run:** - Connect your iOS device. - Select your device in Xcode. - - Build and run the project. - -## TODO List -- [X] Integrate [`em_proxy`](https://github.com/SideStore/em_proxy) -- [X] Compile [`idevice`](https://github.com/jkcoxson/idevice/tree/master/ffi) -- [X] Implement heartbeat -- [X] Mount the developer image -- [X] Retrieve and filter installed apps by `get-task-allow` -- [X] Enable JIT for selected apps -- [X] Design and implement a user-friendly UI *(Done for now)* -- [ ] Write comprehensive documentation -- [ ] Prepare and release the initial version + - Build and run the project. ## License StikJIT is licensed under **AGPL-3.0**. See [`LICENSE`](LICENSE) for details. diff --git a/StikJIT.xcodeproj/project.pbxproj b/StikJIT.xcodeproj/project.pbxproj index d4d4a9b..c17ccb1 100644 --- a/StikJIT.xcodeproj/project.pbxproj +++ b/StikJIT.xcodeproj/project.pbxproj @@ -655,4 +655,4 @@ /* End XCConfigurationList section */ }; rootObject = DC6F1D2F2D94EADD0071B2B6 /* Project object */; -} +} \ No newline at end of file diff --git a/StikJIT/Assets.xcassets/AppIcon.appiconset/Contents.json b/StikJIT/Assets.xcassets/AppIcon.appiconset/Contents.json index ffdfe15..5d7d7eb 100644 --- a/StikJIT/Assets.xcassets/AppIcon.appiconset/Contents.json +++ b/StikJIT/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "StikJIT.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" @@ -12,6 +13,7 @@ "value" : "dark" } ], + "filename" : "StikJIT_Dark.png", "idiom" : "universal", "platform" : "ios", "size" : "1024x1024" diff --git a/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT.png b/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT.png new file mode 100644 index 0000000..11e4929 Binary files /dev/null and b/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT.png differ diff --git a/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT_Dark.png b/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT_Dark.png new file mode 100644 index 0000000..6250814 Binary files /dev/null and b/StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT_Dark.png differ diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index d869a4a..8edbe4e 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -53,6 +53,10 @@ struct HeartbeatApp: App { } return } + var log_path = URL.documentsDirectory.appendingPathComponent("idevice_log.txt").path.utf8CString + log_path.withUnsafeMutableBufferPointer { buffer in + idevice_init_logger(Debug, Debug, buffer.baseAddress) + } startProxy() { result, error in if result { checkVPNConnection() { result, vpn_error in diff --git a/StikJIT/Utilities/LogManager.swift b/StikJIT/Utilities/LogManager.swift new file mode 100644 index 0000000..6b72983 --- /dev/null +++ b/StikJIT/Utilities/LogManager.swift @@ -0,0 +1,91 @@ +// +// LogManager.swift +// StikJIT +// +// Created by neoarz on 3/29/25. +// + +import Foundation + +class LogManager: ObservableObject { + static let shared = LogManager() + + @Published var logs: [LogEntry] = [] + @Published var errorCount: Int = 0 + + struct LogEntry: Identifiable { + let id = UUID() + let timestamp: Date + let type: LogType + let message: String + + enum LogType: String { + case info = "INFO" + case error = "ERROR" + case debug = "DEBUG" + case warning = "WARNING" + } + } + + private init() { + // Add initial system info logs + addInfoLog("StikJIT starting up") + addInfoLog("Initializing environment") + } + + func addLog(message: String, type: LogEntry.LogType) { + //clean dumb stuff + var cleanMessage = message + + // Clean up common prefixes that match the log type + let prefixesToRemove = [ + "Info: ", "INFO: ", "Information: ", + "Error: ", "ERROR: ", "ERR: ", + "Debug: ", "DEBUG: ", "DBG: ", + "Warning: ", "WARN: ", "WARNING: " + ] + + for prefix in prefixesToRemove { + if cleanMessage.hasPrefix(prefix) { + cleanMessage = String(cleanMessage.dropFirst(prefix.count)) + break + } + } + + DispatchQueue.main.async { + self.logs.append(LogEntry(timestamp: Date(), type: type, message: cleanMessage)) + + if type == .error { + self.errorCount += 1 + } + + // Keep log size manageable + if self.logs.count > 1000 { + self.logs.removeFirst(100) + } + } + } + + func addInfoLog(_ message: String) { + addLog(message: message, type: .info) + } + + func addErrorLog(_ message: String) { + addLog(message: message, type: .error) + } + + func addDebugLog(_ message: String) { + addLog(message: message, type: .debug) + } + + func addWarningLog(_ message: String) { + addLog(message: message, type: .warning) + } + + func clearLogs() { + DispatchQueue.main.async { + self.logs.removeAll() + self.errorCount = 0 + } + } +} diff --git a/StikJIT/Utilities/LogManagerBridge.swift b/StikJIT/Utilities/LogManagerBridge.swift new file mode 100644 index 0000000..9a8528e --- /dev/null +++ b/StikJIT/Utilities/LogManagerBridge.swift @@ -0,0 +1,32 @@ +// +// LogManagerBridge.swift +// StikJIT +// +// Created by neoarz on 3/29/25. +// + +import Foundation +//objc bridge for logs +@objc class LogManagerBridge: NSObject { + @objc static let shared = LogManagerBridge() + + private override init() { + super.init() + } + + @objc func addInfoLog(_ message: String) { + LogManager.shared.addInfoLog(message) + } + + @objc func addErrorLog(_ message: String) { + LogManager.shared.addErrorLog(message) + } + + @objc func addDebugLog(_ message: String) { + LogManager.shared.addDebugLog(message) + } + + @objc func addWarningLog(_ message: String) { + LogManager.shared.addWarningLog(message) + } +} diff --git a/StikJIT/Views/ConsoleLogsView.swift b/StikJIT/Views/ConsoleLogsView.swift new file mode 100644 index 0000000..1522f8d --- /dev/null +++ b/StikJIT/Views/ConsoleLogsView.swift @@ -0,0 +1,303 @@ +// +// ConsoleLogsView.swift +// StikJIT +// +// Created by neoarz on 3/29/25. +// + +import SwiftUI +import UIKit + +struct ConsoleLogsView: View { + @Environment(\.dismiss) private var dismiss + @StateObject private var logManager = LogManager.shared + @State private var autoScroll = true + @State private var scrollView: ScrollViewProxy? = nil + + // Alert handlin + @State private var showingExportAlert = false + @State private var showingCopyAlert = false + @State private var alertMessage = "" + @State private var alertTitle = "" + @State private var isError = false + + var body: some View { + NavigationView { + ZStack { + Color.black + .edgesIgnoringSafeArea(.all) + + VStack(spacing: 0) { + // Terminal logs area (made it look somewhat like feathers implementation) + ScrollViewReader { proxy in + ScrollView { + VStack(spacing: 0) { + // Device Information (all thw way on top) + ForEach(["Version: \(UIDevice.current.systemVersion)", + "Name: \(UIDevice.current.name)", + "Model: \(UIDevice.current.model)", + "StikJIT Version: App Version: 1.0"], id: \.self) { info in + Text("[\(timeString())] ℹ️ \(info)") + .font(.system(size: 11, design: .monospaced)) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 2) + .padding(.horizontal, 4) + } + + Spacer() + + // Log entries + ForEach(logManager.logs) { logEntry in + Text(AttributedString(createLogAttributedString(logEntry))) + .font(.system(size: 11, design: .monospaced)) + .textSelection(.enabled) + .lineLimit(nil) + .fixedSize(horizontal: false, vertical: true) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.vertical, 1) + .padding(.horizontal, 4) + .id(logEntry.id) + } + } + } + .onAppear { + scrollView = proxy + } + .onChange(of: logManager.logs.count) { + if autoScroll, let lastLog = logManager.logs.last { + proxy.scrollTo(lastLog.id, anchor: .bottom) + } + } + } + + Spacer() + + + VStack(spacing: 16) { + // Error count with red theme + HStack { + Text("\(logManager.errorCount) Critical Errors.") + .font(.headline) + .foregroundColor(.white) + .padding(.vertical, 12) + .frame(maxWidth: .infinity) + .background(Color.red) + .cornerRadius(10) + } + .padding(.horizontal) + + // Action buttons with dark background + VStack(spacing: 1) { + // Export button + Button(action: { + // Create logs content with device information + var logsContent = "=== DEVICE INFORMATION ===\n" + logsContent += "Version: \(UIDevice.current.systemVersion)\n" + logsContent += "Name: \(UIDevice.current.name)\n" + logsContent += "Model: \(UIDevice.current.model)\n" + logsContent += "StikJIT Version: App Version: 1.0\n\n" + logsContent += "=== LOG ENTRIES ===\n" + + // Add all log entries with proper formatting + logsContent += logManager.logs.map { + "[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)" + }.joined(separator: "\n") + + // Save to document directory (accessible in Files app) + let documentsDirectory = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask).first! + let dateFormatter = DateFormatter() + dateFormatter.dateFormat = "yyyy-MM-dd_HH-mm-ss" + let timestamp = dateFormatter.string(from: Date()) + let fileURL = documentsDirectory.appendingPathComponent("StikJIT_Logs_\(timestamp).txt") + + do { + // Write the logs to the file + try logsContent.write(to: fileURL, atomically: true, encoding: .utf8) + + // Set alert variables and show the alert + alertTitle = "Logs Exported" + alertMessage = "Logs have been saved to Files app in StikJIT folder." + isError = false + showingExportAlert = true + } catch { + // Set error alert variables and show the alert + alertTitle = "Export Failed" + alertMessage = "Failed to save logs: \(error.localizedDescription)" + isError = true + showingExportAlert = true + } + }) { + HStack { + Text("Export Logs") + .foregroundColor(.blue) + Spacer() + Image(systemName: "square.and.arrow.down") + .foregroundColor(.gray) + } + .padding(.vertical, 14) + .padding(.horizontal, 20) + .contentShape(Rectangle()) + } + .background(Color(red: 0.1, green: 0.1, blue: 0.1)) + + Divider() + .background(Color(red: 0.15, green: 0.15, blue: 0.15)) + + // Copy button + Button(action: { + // Create logs content with device information + var logsContent = "=== DEVICE INFORMATION ===\n" + logsContent += "Version: \(UIDevice.current.systemVersion)\n" + logsContent += "Name: \(UIDevice.current.name)\n" + logsContent += "Model: \(UIDevice.current.model)\n" + logsContent += "StikJIT Version: App Version: 1.0\n\n" + logsContent += "=== LOG ENTRIES ===\n" + + // Add all log entries with proper formatting + logsContent += logManager.logs.map { + "[\(formatTime(date: $0.timestamp))] [\($0.type.rawValue)] \($0.message)" + }.joined(separator: "\n") + + // Copy to clipboard + UIPasteboard.general.string = logsContent + + // Show success alert using SwiftUI alert + alertTitle = "Logs Copied" + alertMessage = "Logs have been copied to clipboard." + isError = false + showingCopyAlert = true + }) { + HStack { + Text("Copy Logs") + .foregroundColor(.blue) + Spacer() + Image(systemName: "doc.on.doc") + .foregroundColor(.gray) + } + .padding(.vertical, 14) + .padding(.horizontal, 20) + .contentShape(Rectangle()) + } + .background(Color(red: 0.1, green: 0.1, blue: 0.1)) + } + .cornerRadius(10) + .padding(.horizontal) + .padding(.bottom, 20) + } + } + } + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .principal) { + Text("Console Logs") + .font(.headline) + .foregroundColor(.white) + } + + ToolbarItem(placement: .navigationBarLeading) { + Button(action: { + dismiss() + }) { + HStack(spacing: 2) { + Image(systemName: "chevron.left") + .font(.system(size: 16, weight: .semibold)) + Text("Settings") + .fontWeight(.regular) + } + .foregroundColor(.blue) + } + } + + ToolbarItem(placement: .navigationBarTrailing) { + Button(action: { + logManager.clearLogs() + }) { + Text("Clear") + .foregroundColor(.blue) + } + } + } + } + .navigationViewStyle(StackNavigationViewStyle()) + .alert(alertTitle, isPresented: $showingExportAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(alertMessage) + } + .alert(alertTitle, isPresented: $showingCopyAlert) { + Button("OK", role: .cancel) { } + } message: { + Text(alertMessage) + } + } + + // Creates an NSAttributedString that combines timestamp, type, and message + // with proper styling for each component + private func createLogAttributedString(_ logEntry: LogManager.LogEntry) -> NSAttributedString { + let fullString = NSMutableAttributedString() + + // Timestamp part + let timestampString = "[\(formatTime(date: logEntry.timestamp))]" + let timestampAttr = NSAttributedString( + string: timestampString, + attributes: [.foregroundColor: UIColor.gray] + ) + fullString.append(timestampAttr) + fullString.append(NSAttributedString(string: " ")) + + // Log type part + let typeString = "[\(logEntry.type.rawValue)]" + let typeColor = UIColor(colorForLogType(logEntry.type)) + let typeAttr = NSAttributedString( + string: typeString, + attributes: [.foregroundColor: typeColor] + ) + fullString.append(typeAttr) + fullString.append(NSAttributedString(string: " ")) + + // Message part + let messageAttr = NSAttributedString( + string: logEntry.message, + attributes: [.foregroundColor: UIColor.white] + ) + fullString.append(messageAttr) + + return fullString + } + + // Helper to display current time + private func timeString() -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: Date()) + } + + // Helper to format Date objects to time strings + private func formatTime(date: Date) -> String { + let formatter = DateFormatter() + formatter.dateFormat = "HH:mm:ss" + return formatter.string(from: date) + } + + // Return color based on log type + private func colorForLogType(_ type: LogManager.LogEntry.LogType) -> Color { + switch type { + case .info: + return .green + case .error: + return .red + case .debug: + return .blue + case .warning: + return .orange + } + } +} + + +struct ConsoleLogsView_Previews: PreviewProvider { + static var previews: some View { + ConsoleLogsView() + } +} diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index fdac319..2c89125 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -260,11 +260,20 @@ struct HomeView: View { private func startJITInBackground(with bundleID: String) { isProcessing = true + + // Add log message + LogManager.shared.addInfoLog("Starting JIT for \(bundleID)") + DispatchQueue.global(qos: .background).async { - - JITEnableContext.shared().debugApp(withBundleID: bundleID, logger: nil) + JITEnableContext.shared().debugApp(withBundleID: bundleID, logger: { message in + if let message = message { + // Log messages from the JIT process + LogManager.shared.addInfoLog(message) + } + }) DispatchQueue.main.async { + LogManager.shared.addInfoLog("JIT process completed for \(bundleID)") isProcessing = false } } diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index b4b399b..204fbc4 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -11,85 +11,108 @@ struct InstalledAppsListView: View { @StateObject private var viewModel = InstalledAppsViewModel() @State private var appIcons: [String: UIImage] = [:] @Environment(\.dismiss) private var dismiss - @Environment(\.colorScheme) private var colorScheme + @AppStorage("recentApps") var recentApps: [String] = [] var onSelectApp: (String) -> Void - + var body: some View { NavigationView { - ScrollView { - LazyVStack(spacing: 0) { - ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in - Button(action: { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - onSelectApp(bundleID) - } - }) { - 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) + List { + if !recentApps.isEmpty { + Section { + ForEach(recentApps, id: \.self) { bundleID in + AppButton(bundleID: bundleID, appName: viewModel.apps[bundleID] ?? "", recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + withAnimation { + recentApps.removeAll(where: { $0 == bundleID }) } + } label: { + Label("Delete", systemImage: "trash") + } } - - // 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()) - } - .buttonStyle(PlainButtonStyle()) - - if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key { - Divider() - .padding(.leading, 96) - .padding(.trailing, 20) - .opacity(0.4) } + } header: { + Text("Recents") + } + } + Section { + ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in + AppButton(bundleID: bundleID, appName: appName, recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp) + } + } header: { + if !recentApps.isEmpty { + Text("All applications") + } else { + EmptyView() } } - .background(Color(UIColor.systemBackground)) } - .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + .listStyle(.plain) .navigationTitle("Installed Apps") - .navigationBarItems(leading: Button("Done") { - dismiss() + .toolbar(content: { + Button("Done") { + dismiss() + } + }) + } + .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + } +} + +struct AppButton: View { + @State var bundleID: String + @State var appName: String + @Binding var recentApps: [String] + @Binding var appIcons: [String: UIImage] + var onSelectApp: (String) -> Void + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Button(action: { + recentApps.removeAll(where: { $0 == bundleID }) + recentApps.insert(bundleID, at: 0) + if recentApps.count > 3 { + recentApps = Array(recentApps.prefix(3)) + } + onSelectApp(bundleID) + }) { + HStack(spacing: 16) { + 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) + } + } + VStack(alignment: .leading, spacing: 4) { + Text(appName) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(Color(.label)) + Text(bundleID) + .font(.system(size: 15)) + .foregroundColor(Color.gray) + .lineLimit(1) + } + Spacer() } - .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 { @@ -103,6 +126,26 @@ struct InstalledAppsListView: View { } } +extension Array: @retroactive RawRepresentable where Element: Codable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode([Element].self, from: data) + else { + return nil + } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} + #Preview { InstalledAppsListView { _ in } } diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index f5b87f3..035a2ea 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -24,9 +24,11 @@ struct SettingsView: View { @State private var mounted = false + @State private var showingConsoleLogsView = false + // Developer profile image URLs private let developerProfiles: [String: String] = [ - "Blu": "https://github.com/0-Blu.png", + "Stephen": "https://github.com/0-Blu.png", "jkcoxson": "https://github.com/jkcoxson.png", "Stossy11": "https://github.com/Stossy11.png", "Neo": "https://github.com/neoarz.png", @@ -380,8 +382,32 @@ struct SettingsView: View { .padding(.vertical, 20) .padding(.horizontal, 16) } + .padding(.bottom, 16) - // Version text - now outside of any card, as standalone text at the bottom + // Move System Logs section here (right after About card, before version) + SettingsCard { + Button(action: { + showingConsoleLogsView = true + }) { + HStack { + Text("System Logs") + .font(.headline) + .foregroundColor(.primary) + + Spacer() + + Image(systemName: "chevron.right") + .foregroundColor(.secondary) + } + .contentShape(Rectangle()) + .padding(.vertical, 16) + .padding(.horizontal, 16) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.bottom, 16) + + // Version info should now come after System Logs HStack { Spacer() Text("Version 1.0 • iOS \(UIDevice.current.systemVersion)") @@ -395,6 +421,11 @@ struct SettingsView: View { .padding(.horizontal, 16) .padding(.vertical, 20) } + + // Add this sheet at the end of the ZStack, before the final closing bracket + .sheet(isPresented: $showingConsoleLogsView) { + ConsoleLogsView() + } } .fileImporter( isPresented: $isShowingPairingFilePicker, @@ -674,3 +705,10 @@ struct CollaboratorRow: View { } } } + +// Define these in a separate file if they conflict +struct ConsoleLogsView_Preview: PreviewProvider { + static var previews: some View { + ConsoleLogsView() + } +} diff --git a/StikJIT/idevice/JITEnableContext.m b/StikJIT/idevice/JITEnableContext.m index 4fa6598..580b2ce 100644 --- a/StikJIT/idevice/JITEnableContext.m +++ b/StikJIT/idevice/JITEnableContext.m @@ -13,6 +13,7 @@ #include "applist.h" #include "JITEnableContext.h" +#import "StikJIT-Swift.h" // This imports the Swift files into Objective-C JITEnableContext* sharedJITContext = nil; @@ -41,6 +42,17 @@ JITEnableContext* sharedJITContext = nil; NSString *message = [[NSString alloc] initWithFormat:formatStr arguments:args]; NSLog(@"%@", message); + // Add to log manager + if ([message containsString:@"ERROR"] || [message containsString:@"Error"]) { + [[LogManagerBridge shared] addErrorLog:message]; + } else if ([message containsString:@"WARNING"] || [message containsString:@"Warning"]) { + [[LogManagerBridge shared] addWarningLog:message]; + } else if ([message containsString:@"DEBUG"]) { + [[LogManagerBridge shared] addDebugLog:message]; + } else { + [[LogManagerBridge shared] addInfoLog:message]; + } + if(logger) { logger(message); } diff --git a/StikJIT/idevice/heartbeat.c b/StikJIT/idevice/heartbeat.c index bd96c67..413c067 100644 --- a/StikJIT/idevice/heartbeat.c +++ b/StikJIT/idevice/heartbeat.c @@ -14,7 +14,6 @@ void startHeartbeat(IdevicePairingFile* pairing_file, TcpProviderHandle** provider, int* heartbeatSessionId, HeartbeatCompletionHandlerC completion, LogFuncC logger) { int currentSessionId = *heartbeatSessionId; logger("DEBUG: Initializing logger..."); - idevice_init_logger(Debug, Disabled, NULL); struct sockaddr_in addr; memset(&addr, 0, sizeof(addr)); diff --git a/StikJIT/idevice/libidevice_ffi.a b/StikJIT/idevice/libidevice_ffi.a index 731fc15..50d4755 100644 Binary files a/StikJIT/idevice/libidevice_ffi.a and b/StikJIT/idevice/libidevice_ffi.a differ