diff --git a/StikJIT.xcodeproj/project.pbxproj b/StikJIT.xcodeproj/project.pbxproj index d4d4a9b5..c17ccb13 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/Utilities/LogManager.swift b/StikJIT/Utilities/LogManager.swift new file mode 100644 index 00000000..6b729839 --- /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 00000000..9a8528e8 --- /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 00000000..1522f8db --- /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 13ef8c4e..93b9311c 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -259,11 +259,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/SettingsView.swift b/StikJIT/Views/SettingsView.swift index e39eb528..df55ff9d 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -23,6 +23,8 @@ 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", @@ -361,8 +363,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)") @@ -376,6 +402,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, @@ -655,3 +686,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 83e985f3..c20ac94c 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 @@ - (LogFuncC)createCLogger:(LogFunc)logger { 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); }