Merge remote-tracking branch 'upstream/main'

This commit is contained in:
hugeBlack 2025-03-28 21:08:14 +08:00
commit 99fbf4670d
6 changed files with 381 additions and 64 deletions

View file

@ -8,6 +8,9 @@ Powered by [idevice](https://github.com/jkcoxson/idevice) and [Emotional Manglin
All dependencies remain under their original licenses. All dependencies remain under their original licenses.
## Privacy Policy
No data is collected
## TODO List ## TODO List
- [X] Integrate `em_proxy` - [X] Integrate `em_proxy`

View file

@ -454,7 +454,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2; IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (
@ -502,7 +502,7 @@
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
INFOPLIST_KEY_UISupportsDocumentBrowser = YES; INFOPLIST_KEY_UISupportsDocumentBrowser = YES;
IPHONEOS_DEPLOYMENT_TARGET = 18.2; IPHONEOS_DEPLOYMENT_TARGET = 17.4;
LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks"; LD_RUNPATH_SEARCH_PATHS = "@executable_path/Frameworks";
"LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks"; "LD_RUNPATH_SEARCH_PATHS[sdk=macosx*]" = "@executable_path/../Frameworks";
LIBRARY_SEARCH_PATHS = ( LIBRARY_SEARCH_PATHS = (

View file

@ -0,0 +1,71 @@
//
// AppStoreIconFetcher.swift
// StikJIT
//
// Created by neoarz on 3/28/25.
//
import UIKit
class AppStoreIconFetcher {
static private var iconCache: [String: UIImage] = [:]
static func getIcon(for bundleID: String, completion: @escaping (UIImage?) -> Void) {
// Check cache first
if let cachedIcon = iconCache[bundleID] {
completion(cachedIcon)
return
}
// Hit the App Store API
let baseURLString = "https://itunes.apple.com/lookup?bundleId="
let urlString = baseURLString + bundleID
guard let url = URL(string: urlString) else {
DispatchQueue.main.async { completion(nil) }
return
}
URLSession.shared.dataTask(with: url) { data, response, error in
guard let data = data, error == nil else {
DispatchQueue.main.async { completion(nil) }
return
}
do {
if let json = try JSONSerialization.jsonObject(with: data) as? [String: Any],
let results = json["results"] as? [[String: Any]],
!results.isEmpty,
let firstApp = results.first,
let iconURLString = firstApp["artworkUrl512"] as? String ?? firstApp["artworkUrl100"] as? String,
let iconURL = URL(string: iconURLString) {
// Download the icon image
downloadImage(from: iconURL) { image in
if let image = image {
// Cache the icon
iconCache[bundleID] = image
}
DispatchQueue.main.async {
completion(image)
}
}
} else {
DispatchQueue.main.async { completion(nil) }
}
} catch {
DispatchQueue.main.async { completion(nil) }
}
}.resume()
}
private static func downloadImage(from url: URL, completion: @escaping (UIImage?) -> Void) {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data, let image = UIImage(data: data) {
completion(image)
} else {
completion(nil)
}
}.resume()
}
}

View file

@ -23,6 +23,11 @@ struct HomeView: View {
@State private var isProcessing = false @State private var isProcessing = false
@State private var isShowingInstalledApps = false @State private var isShowingInstalledApps = false
@State private var isShowingPairingFilePicker = false @State private var isShowingPairingFilePicker = false
@State private var pairingFileExists: Bool = false
@State private var showPairingFileMessage = false
@State private var pairingFileIsValid = false
@State private var isImportingFile = false
@State private var importProgress: Float = 0.0
@State private var viewDidAppeared = false @State private var viewDidAppeared = false
@State private var pendingBundleIdToEnableJIT : String? = nil @State private var pendingBundleIdToEnableJIT : String? = nil
@ -38,38 +43,98 @@ struct HomeView: View {
.font(.system(.largeTitle, design: .rounded)) .font(.system(.largeTitle, design: .rounded))
.fontWeight(.bold) .fontWeight(.bold)
Text("Click enable jit to get started") Text(pairingFileExists ? "Click enable JIT to get started" : "Pick pairing file to get started")
.font(.system(.subheadline, design: .rounded)) .font(.system(.subheadline, design: .rounded))
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.multilineTextAlignment(.center) .multilineTextAlignment(.center)
} }
.padding(.top, 40) .padding(.top, 40)
// Main action button - changes based on whether we have a pairing file
Button(action: { Button(action: {
if !FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) { if pairingFileExists {
isShowingPairingFilePicker = true // Got a pairing file, show apps
} else {
isShowingInstalledApps = true isShowingInstalledApps = true
} else {
// No pairing file yet, let's get one
isShowingPairingFilePicker = true
} }
}) { }) {
Label("Enable JIT", systemImage: "list.bullet") HStack {
.font(.system(.title3, design: .rounded)) Image(systemName: pairingFileExists ? "bolt.fill" : "doc.badge.plus")
.fontWeight(.semibold) .font(.system(size: 20))
.frame(maxWidth: .infinity) Text(pairingFileExists ? "Enable JIT" : "Select Pairing File")
.padding() .font(.system(.title3, design: .rounded))
.background(Color.blue) .fontWeight(.semibold)
.foregroundColor(.white) }
.cornerRadius(16) .frame(maxWidth: .infinity)
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4) .padding()
.background(Color.blue)
.foregroundColor(.white)
.cornerRadius(16)
.shadow(color: Color.blue.opacity(0.3), radius: 8, x: 0, y: 4)
} }
.padding(.horizontal, 20) .padding(.horizontal, 20)
// Status message area - keeps layout consistent
ZStack {
// Progress bar for importing file
if isImportingFile {
VStack(spacing: 8) {
HStack {
Text("Processing pairing file...")
.font(.system(.caption, design: .rounded))
.foregroundColor(.secondaryText)
Spacer()
Text("\(Int(importProgress * 100))%")
.font(.system(.caption, design: .rounded))
.foregroundColor(.secondaryText)
}
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.black.opacity(0.2))
.frame(height: 8)
RoundedRectangle(cornerRadius: 4)
.fill(Color.green)
.frame(width: geometry.size.width * CGFloat(importProgress), height: 8)
.animation(.linear(duration: 0.3), value: importProgress)
}
}
.frame(height: 8)
}
.padding(.horizontal, 40)
}
// Success message
if showPairingFileMessage && pairingFileIsValid {
Text("✓ Pairing file successfully imported")
.font(.system(.callout, design: .rounded))
.foregroundColor(.green)
.padding(.vertical, 4)
.padding(.horizontal, 12)
.background(Color.green.opacity(0.1))
.cornerRadius(8)
.transition(.opacity)
}
// Invisible text to reserve space - no layout jumps
Text(" ").opacity(0)
}
.frame(height: isImportingFile ? 60 : 30) // Adjust height based on what's showing
Spacer() Spacer()
} }
.padding() .padding()
} }
.onAppear {
checkPairingFileExists()
}
.onReceive(timer) { _ in .onReceive(timer) { _ in
refreshBackground() refreshBackground()
checkPairingFileExists()
} }
.fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in .fileImporter(isPresented: $isShowingPairingFilePicker, allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) {result in
switch result { switch result {
@ -86,9 +151,45 @@ struct HomeView: View {
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!") print("File copied successfully!")
// Show progress bar and initialize progress
DispatchQueue.main.async {
isImportingFile = true
importProgress = 0.0
pairingFileExists = true
}
// Start heartbeat in background
startHeartbeatInBackground() startHeartbeatInBackground()
Thread.sleep(forTimeInterval: 5) // Create timer to update progress instead of sleeping
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
DispatchQueue.main.async {
if importProgress < 1.0 {
importProgress += 0.25
} else {
timer.invalidate()
isImportingFile = false
pairingFileIsValid = true
// Show success message
withAnimation {
showPairingFileMessage = true
}
// Hide message after delay
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
showPairingFileMessage = false
}
}
}
}
}
// Ensure timer keeps running
RunLoop.current.add(progressTimer, forMode: .common)
} catch { } catch {
print("Error copying file: \(error)") print("Error copying file: \(error)")
} }
@ -99,8 +200,8 @@ struct HomeView: View {
if accessing { if accessing {
url.stopAccessingSecurityScopedResource() url.stopAccessingSecurityScopedResource()
} }
case .failure(_): case .failure(let error):
print("Failed") print("Failed to import file: \(error)")
} }
} }
.sheet(isPresented: $isShowingInstalledApps) { .sheet(isPresented: $isShowingInstalledApps) {
@ -136,6 +237,12 @@ struct HomeView: View {
} }
} }
private func checkPairingFileExists() {
pairingFileExists = FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path)
}
private func refreshBackground() { private func refreshBackground() {
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground
} }

View file

@ -8,63 +8,101 @@
import SwiftUI import SwiftUI
struct InstalledAppsListView: View { struct InstalledAppsListView: View {
@AppStorage("username") private var username = "User" @StateObject private var viewModel = InstalledAppsViewModel()
@AppStorage("customBackgroundColor") private var customBackgroundColorHex: String = Color.primaryBackground.toHex() ?? "#000000" @State private var appIcons: [String: UIImage] = [:]
@State private var selectedBackgroundColor: Color = Color(hex: UserDefaults.standard.string(forKey: "customBackgroundColor") ?? "#000000") ?? Color.primaryBackground @Environment(\.dismiss) private var dismiss
let timer = Timer.publish(every: 1, on: .main, in: .common).autoconnect() @Environment(\.colorScheme) private var colorScheme
@StateObject var viewModel = InstalledAppsViewModel() var onSelectApp: (String) -> Void
@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.filter { $0.value.localizedCaseInsensitiveContains(searchText) }.map { $0.key }
}
}
var body: some View { var body: some View {
NavigationView { NavigationView {
ZStack { ScrollView {
selectedBackgroundColor.edgesIgnoringSafeArea(.all) LazyVStack(spacing: 0) {
ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in
List {
ForEach(filteredApps, id: \.self) { bundleId in
Button(action: { Button(action: {
onSelect(bundleId) // Select using the app's bundle ID withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) {
onSelectApp(bundleID)
}
}) { }) {
Text(viewModel.apps[bundleId] ?? "Unknown App") HStack(spacing: 16) {
.font(.system(.body, design: .rounded)) // App Icon
.padding(.vertical, 8) 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)
}
}
// 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())
} }
.listRowBackground(Color(.secondarySystemGroupedBackground)) .buttonStyle(PlainButtonStyle())
.cornerRadius(8)
} if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key {
} Divider()
.listStyle(InsetGroupedListStyle()) .padding(.leading, 96)
.navigationTitle("Installed Apps") .padding(.trailing, 20)
.toolbar { .opacity(0.4)
ToolbarItem(placement: .cancellationAction) {
Button("Done") {
dismiss()
} }
} }
} }
.searchable(text: $searchText, prompt: "Search Apps") .background(Color(UIColor.systemBackground))
} }
.onReceive(timer) { _ in .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all))
refreshBackground() .navigationTitle("Installed Apps")
.navigationBarItems(leading: Button("Done") {
dismiss()
} }
.onAppear { .font(.system(size: 17, weight: .regular))
print(filteredApps) .foregroundColor(.blue))
}
}
// Helper method to load app icon
private func loadAppIcon(for bundleID: String) {
AppStoreIconFetcher.getIcon(for: bundleID) { image in
if let image = image {
DispatchQueue.main.async {
withAnimation(.easeIn(duration: 0.2)) {
self.appIcons[bundleID] = image
}
}
} }
} }
} }
}
private func refreshBackground() {
selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground #Preview {
} InstalledAppsListView { _ in }
} }

View file

@ -16,6 +16,10 @@ struct SettingsView: View {
@State private var selectedBackgroundColor: Color = Color.primaryBackground @State private var selectedBackgroundColor: Color = Color.primaryBackground
@State private var showIconPopover = false @State private var showIconPopover = false
@State private var showPairingFileMessage = false
@State private var pairingFileIsValid = false
@State private var isImportingFile = false
@State private var importProgress: Float = 0.0
var body: some View { var body: some View {
ZStack { ZStack {
@ -60,6 +64,58 @@ struct SettingsView: View {
Spacer() Spacer()
} }
.listRowBackground(Color.cardBackground) .listRowBackground(Color.cardBackground)
if isImportingFile {
VStack(spacing: 8) {
HStack {
Text("Processing pairing file...")
.font(.system(.caption, design: .rounded))
.foregroundColor(.secondaryText)
Spacer()
Text("\(Int(importProgress * 100))%")
.font(.system(.caption, design: .rounded))
.foregroundColor(.secondaryText)
}
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: 4)
.fill(Color.cardBackground)
.frame(height: 8)
RoundedRectangle(cornerRadius: 4)
.fill(Color.green)
.frame(width: geometry.size.width * CGFloat(importProgress), height: 8)
.animation(.linear(duration: 0.3), value: importProgress)
}
}
.frame(height: 8)
}
.padding(.vertical, 8)
.listRowBackground(Color.cardBackground)
}
if showPairingFileMessage && pairingFileIsValid {
HStack {
Spacer()
Text("✓ Pairing file successfully imported")
.font(.system(.callout, design: .rounded))
.foregroundColor(.green)
.padding(.vertical, 4)
.padding(.horizontal, 12)
.background(Color.green.opacity(0.1))
.cornerRadius(8)
Spacer()
}
.padding(.vertical, 4)
.listRowBackground(Color.cardBackground)
.transition(
.asymmetric(
insertion: .scale(scale: 0.9).combined(with: .opacity).animation(.spring(response: 0.4, dampingFraction: 0.7)),
removal: .opacity.animation(.easeOut(duration: 0.25))
)
)
}
} }
Section(header: Text("About").font(.headline).foregroundColor(.primaryText)) { Section(header: Text("About").font(.headline).foregroundColor(.primaryText)) {
@ -116,6 +172,20 @@ struct SettingsView: View {
} }
} }
.listRowBackground(Color.cardBackground) .listRowBackground(Color.cardBackground)
Button(action: {
if let url = URL(string: "https://apps.apple.com/us/app/stiknes/id6737158545") {
UIApplication.shared.open(url)
}
}) {
HStack {
Text("Like this app? Check out StikNES!")
.foregroundColor(.secondaryText)
Spacer()
Image(systemName: "arrow.up.right.square")
.foregroundColor(.primaryText)
}
}
.listRowBackground(Color.cardBackground)
} }
} }
.background(selectedBackgroundColor) .background(selectedBackgroundColor)
@ -139,9 +209,37 @@ struct SettingsView: View {
try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist"))
print("File copied successfully!") print("File copied successfully!")
DispatchQueue.main.async {
isImportingFile = true
importProgress = 0.0
}
let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in
DispatchQueue.main.async {
if importProgress < 1.0 {
importProgress += 0.25
} else {
timer.invalidate()
isImportingFile = false
pairingFileIsValid = true
withAnimation {
showPairingFileMessage = true
}
DispatchQueue.main.asyncAfter(deadline: .now() + 3) {
withAnimation {
showPairingFileMessage = false
}
}
}
}
}
startHeartbeatInBackground() startHeartbeatInBackground()
Thread.sleep(forTimeInterval: 5) RunLoop.current.add(progressTimer, forMode: .common)
} catch { } catch {
print("Error copying file: \(error)") print("Error copying file: \(error)")
} }