mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-16 04:20:30 +00:00
Merge branch '0-Blu:main' into main
This commit is contained in:
commit
9fa8050d37
15 changed files with 611 additions and 88 deletions
22
README.md
22
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).
|
||||
[](https://github.com/0-Blu/StikJIT/releases)
|
||||
[](https://github.com/0-Blu/StikJIT/pulls)
|
||||
[](https://github.com/0-Blu/StikJIT/blob/main/LICENSE)
|
||||

|
||||
|
||||
## 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.
|
||||
|
|
|
|||
|
|
@ -655,4 +655,4 @@
|
|||
/* End XCConfigurationList section */
|
||||
};
|
||||
rootObject = DC6F1D2F2D94EADD0071B2B6 /* Project object */;
|
||||
}
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
BIN
StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT.png
Normal file
BIN
StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 552 KiB |
BIN
StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT_Dark.png
Normal file
BIN
StikJIT/Assets.xcassets/AppIcon.appiconset/StikJIT_Dark.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 280 KiB |
|
|
@ -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
|
||||
|
|
|
|||
91
StikJIT/Utilities/LogManager.swift
Normal file
91
StikJIT/Utilities/LogManager.swift
Normal file
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
32
StikJIT/Utilities/LogManagerBridge.swift
Normal file
32
StikJIT/Utilities/LogManagerBridge.swift
Normal file
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
303
StikJIT/Views/ConsoleLogsView.swift
Normal file
303
StikJIT/Views/ConsoleLogsView.swift
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,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",
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
|
|
|||
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue