mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
2.3.5 RC
This commit is contained in:
parent
3b844440ba
commit
bbae4ac2b0
4 changed files with 719 additions and 612 deletions
|
|
@ -723,7 +723,7 @@
|
|||
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 2.3.4;
|
||||
MARKETING_VERSION = 2.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
|
|
@ -777,7 +777,7 @@
|
|||
"$(PROJECT_DIR)/StikJIT/idevice",
|
||||
);
|
||||
MACOSX_DEPLOYMENT_TARGET = 15.1;
|
||||
MARKETING_VERSION = 2.3.4;
|
||||
MARKETING_VERSION = 2.3.5;
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SDKROOT = auto;
|
||||
|
|
|
|||
|
|
@ -490,18 +490,10 @@ struct HeartbeatApp: App {
|
|||
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
|
||||
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
|
||||
@State private var showWelcomeSheet: Bool = false
|
||||
@State private var isLoading2 = true
|
||||
@State private var isPairing = false
|
||||
@State private var heartBeat = false
|
||||
@State private var error: Int32? = nil
|
||||
@State private var show_alert = false
|
||||
@State private var alert_string = ""
|
||||
@State private var alert_title = ""
|
||||
@State private var showTimeoutError = false
|
||||
@State private var showContinueWarning = false
|
||||
@State private var timeoutTimer: Timer?
|
||||
@StateObject private var mount = MountingProgress.shared
|
||||
@StateObject private var dnsChecker = DNSChecker()
|
||||
@StateObject private var themeExpansionManager = ThemeExpansionManager()
|
||||
@Environment(\.scenePhase) private var scenePhase // Observe scene lifecycle
|
||||
|
||||
|
|
@ -566,148 +558,37 @@ struct HeartbeatApp: App {
|
|||
var body: some Scene {
|
||||
WindowGroup {
|
||||
BackgroundContainer {
|
||||
Group {
|
||||
if isLoading2 {
|
||||
LoadingView(showAlert: $show_alert, alertTitle: $alert_title, alertMessage: $alert_string)
|
||||
.onAppear {
|
||||
dnsChecker.checkDNS()
|
||||
timeoutTimer?.invalidate()
|
||||
timeoutTimer = Timer.scheduledTimer(withTimeInterval: 7, repeats: false) { _ in
|
||||
if isLoading2 {
|
||||
showTimeoutError = true
|
||||
}
|
||||
}
|
||||
checkVPNConnection() { result, vpn_error in
|
||||
if result {
|
||||
if FileManager.default.fileExists(atPath: URL.documentsDirectory.appendingPathComponent("pairingFile.plist").path) {
|
||||
Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { timer in
|
||||
if pubHeartBeat {
|
||||
isLoading2 = false
|
||||
timer.invalidate()
|
||||
} else {
|
||||
if let error {
|
||||
if error == -9 { // InvalidHostID is -9
|
||||
isPairing = true
|
||||
} else {
|
||||
startHeartbeatInBackground()
|
||||
}
|
||||
self.error = nil
|
||||
}
|
||||
}
|
||||
}
|
||||
startHeartbeatInBackground()
|
||||
} else {
|
||||
isLoading2 = false
|
||||
}
|
||||
} else if let vpn_error {
|
||||
showAlert(title: "Error", message: "EM Proxy failed to connect: \(vpn_error)", showOk: true) { _ in
|
||||
exit(0)
|
||||
}
|
||||
MainTabView()
|
||||
.onAppear {
|
||||
let fileManager = FileManager.default
|
||||
for (index, urlString) in urls.enumerated() {
|
||||
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
|
||||
if !fileManager.fileExists(atPath: destinationURL.path) {
|
||||
downloadFile(from: urlString, to: destinationURL) { result in
|
||||
if (result != "") {
|
||||
alert_title = "An Error has Occurred"
|
||||
alert_string = "[Download DDI Error]: " + result
|
||||
show_alert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
.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")
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
if showTimeoutError {
|
||||
CustomErrorView(
|
||||
title: "Connection Error",
|
||||
message: "Check your connection and ensure your pairing file is valid and try again.",
|
||||
onDismiss: {
|
||||
showTimeoutError = false
|
||||
},
|
||||
showButton: true,
|
||||
primaryButtonText: "Continue Anyway",
|
||||
onPrimaryButtonTap: {
|
||||
showContinueWarning = true
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if showContinueWarning {
|
||||
CustomErrorView(
|
||||
title: "Proceeding Without Connection",
|
||||
message: "StikDebug will not function as expected if you choose to continue.",
|
||||
onDismiss: {
|
||||
showContinueWarning = false
|
||||
},
|
||||
showButton: true,
|
||||
primaryButtonText: "I Understand",
|
||||
onPrimaryButtonTap: {
|
||||
showContinueWarning = false
|
||||
isLoading2 = false
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
} else {
|
||||
MainTabView()
|
||||
.onAppear {
|
||||
let fileManager = FileManager.default
|
||||
for (index, urlString) in urls.enumerated() {
|
||||
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index])
|
||||
if !fileManager.fileExists(atPath: destinationURL.path) {
|
||||
downloadFile(from: urlString, to: destinationURL) { result in
|
||||
if (result != "") {
|
||||
alert_title = "An Error has Occurred"
|
||||
alert_string = "[Download DDI Error]: " + result
|
||||
show_alert = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
if show_alert {
|
||||
CustomErrorView(
|
||||
title: alert_title,
|
||||
message: alert_string,
|
||||
onDismiss: {
|
||||
show_alert = false
|
||||
},
|
||||
showButton: true,
|
||||
primaryButtonText: "OK"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
.overlay(
|
||||
ZStack {
|
||||
if show_alert {
|
||||
CustomErrorView(
|
||||
title: alert_title,
|
||||
message: alert_string,
|
||||
onDismiss: {
|
||||
show_alert = false
|
||||
},
|
||||
showButton: true,
|
||||
primaryButtonText: "OK"
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
.themeExpansionManager(themeExpansionManager)
|
||||
// Apply global tint to all SwiftUI views in this window
|
||||
|
|
@ -746,54 +627,6 @@ struct HeartbeatApp: App {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) {
|
||||
let host = NWEndpoint.Host("10.7.0.1")
|
||||
let port = NWEndpoint.Port(rawValue: 62078)!
|
||||
let connection = NWConnection(host: host, port: port, using: .tcp)
|
||||
var timeoutWorkItem: DispatchWorkItem?
|
||||
|
||||
timeoutWorkItem = DispatchWorkItem { [weak connection] in
|
||||
if connection?.state != .ready {
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
if timeoutWorkItem?.isCancelled == false {
|
||||
callback(false, "[TIMEOUT] The loopback VPN is not connected. Try closing this app, turn it off and back on.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
connection.stateUpdateHandler = { [weak connection] state in
|
||||
switch state {
|
||||
case .ready:
|
||||
timeoutWorkItem?.cancel()
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
callback(true, nil)
|
||||
}
|
||||
case .failed(let error):
|
||||
timeoutWorkItem?.cancel()
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
if error == NWError.posix(.ETIMEDOUT) {
|
||||
callback(false, "The loopback VPN is not connected. Try closing the app, turn it off and back on.")
|
||||
} else if error == NWError.posix(.ECONNREFUSED) {
|
||||
callback(false, "Wifi is not connected. StikJIT won't work on cellular data.")
|
||||
} else {
|
||||
callback(false, "em proxy check error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .global())
|
||||
if let workItem = timeoutWorkItem {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 20, execute: workItem)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Additional Helpers
|
||||
|
|
@ -847,7 +680,8 @@ class MountingProgress: ObservableObject {
|
|||
self.mountingThread = nil
|
||||
}
|
||||
|
||||
mountingThread = Thread {
|
||||
mountingThread = Thread { [weak self] in
|
||||
guard let self = self else { return }
|
||||
let mountResult = mountPersonalDDI(
|
||||
imagePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg").path,
|
||||
trustcachePath: URL.documentsDirectory.appendingPathComponent("DDI/Image.dmg.trustcache").path,
|
||||
|
|
@ -855,16 +689,17 @@ class MountingProgress: ObservableObject {
|
|||
pairingFilePath: pairingpath
|
||||
)
|
||||
|
||||
if mountResult != 0 {
|
||||
showAlert(title: "Error", message: "An Error Occured when Mounting the DDI\nError Code: \(mountResult)", showOk: true, showTryAgain: true) { shouldTryAgain in
|
||||
if shouldTryAgain {
|
||||
self.mount()
|
||||
DispatchQueue.main.async {
|
||||
if mountResult != 0 {
|
||||
showAlert(title: "Error", message: "An Error Occured when Mounting the DDI\nError Code: \(mountResult)", showOk: true, showTryAgain: true) { shouldTryAgain in
|
||||
if shouldTryAgain {
|
||||
self.mount()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
} else {
|
||||
self.coolisMounted = isMounted()
|
||||
}
|
||||
self.mountingThread = nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -942,68 +777,51 @@ func startHeartbeatInBackground() {
|
|||
heartBeatThread.start()
|
||||
}
|
||||
|
||||
struct LoadingView: View {
|
||||
@Binding var showAlert: Bool
|
||||
@Binding var alertTitle: String
|
||||
@Binding var alertMessage: String
|
||||
func checkVPNConnection(callback: @escaping (Bool, String?) -> Void) {
|
||||
let host = NWEndpoint.Host("10.7.0.1")
|
||||
let port = NWEndpoint.Port(rawValue: 62078)!
|
||||
let connection = NWConnection(host: host, port: port, using: .tcp)
|
||||
var timeoutWorkItem: DispatchWorkItem?
|
||||
|
||||
@State private var animate = false
|
||||
@Environment(\.colorScheme) private var colorScheme
|
||||
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
|
||||
@Environment(\.themeExpansionManager) private var themeExpansion
|
||||
|
||||
private var accentColor: Color {
|
||||
themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
// Background now comes from global BackgroundContainer
|
||||
Color.clear.ignoresSafeArea()
|
||||
|
||||
VStack {
|
||||
ZStack {
|
||||
Circle()
|
||||
.stroke(lineWidth: 8)
|
||||
.foregroundColor(accentColor.opacity(0.18))
|
||||
.frame(width: 80, height: 80)
|
||||
|
||||
Circle()
|
||||
.trim(from: 0, to: 0.7)
|
||||
.stroke(
|
||||
AngularGradient(
|
||||
gradient: Gradient(colors: [
|
||||
accentColor.opacity(0.95),
|
||||
accentColor.opacity(0.45)
|
||||
]),
|
||||
center: .center
|
||||
),
|
||||
style: StrokeStyle(lineWidth: 6, lineCap: .round)
|
||||
)
|
||||
.rotationEffect(.degrees(animate ? 360 : 0))
|
||||
.frame(width: 80, height: 80)
|
||||
.animation(Animation.linear(duration: 1.2).repeatForever(autoreverses: false), value: animate)
|
||||
timeoutWorkItem = DispatchWorkItem { [weak connection] in
|
||||
if connection?.state != .ready {
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
if timeoutWorkItem?.isCancelled == false {
|
||||
callback(false, "[TIMEOUT] The loopback VPN is not connected. Try closing this app, turn it off and back on.")
|
||||
}
|
||||
.shadow(color: accentColor.opacity(0.25), radius: 10, x: 0, y: 0)
|
||||
.onAppear {
|
||||
animate = true
|
||||
let os = ProcessInfo.processInfo.operatingSystemVersion
|
||||
if os.majorVersion < 17 || (os.majorVersion == 17 && os.minorVersion < 4) {
|
||||
alertTitle = "Unsupported OS Version".localized
|
||||
alertMessage = String(format: "StikJIT only supports 17.4 and above. Your device is running iOS/iPadOS %@".localized, "\(os.majorVersion).\(os.minorVersion).\(os.patchVersion)")
|
||||
showAlert = true
|
||||
}
|
||||
}
|
||||
|
||||
Text("Loading...")
|
||||
.font(.system(size: 20, weight: .medium, design: .rounded))
|
||||
.foregroundColor(.primary)
|
||||
.padding(.top, 20)
|
||||
.opacity(animate ? 1.0 : 0.5)
|
||||
.animation(Animation.easeInOut(duration: 1.5).repeatForever(autoreverses: true), value: animate)
|
||||
}
|
||||
}
|
||||
// Inherit system color scheme
|
||||
}
|
||||
|
||||
connection.stateUpdateHandler = { [weak connection] state in
|
||||
switch state {
|
||||
case .ready:
|
||||
timeoutWorkItem?.cancel()
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
callback(true, nil)
|
||||
}
|
||||
case .failed(let error):
|
||||
timeoutWorkItem?.cancel()
|
||||
connection?.cancel()
|
||||
DispatchQueue.main.async {
|
||||
if error == NWError.posix(.ETIMEDOUT) {
|
||||
callback(false, "The loopback VPN is not connected. Try closing the app, turn it off and back on.")
|
||||
} else if error == NWError.posix(.ECONNREFUSED) {
|
||||
callback(false, "Wifi is not connected. StikJIT won't work on cellular data.")
|
||||
} else {
|
||||
callback(false, "em proxy check error: \(error.localizedDescription)")
|
||||
}
|
||||
}
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
connection.start(queue: .global())
|
||||
if let workItem = timeoutWorkItem {
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + 20, execute: workItem)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -112,19 +112,19 @@ struct InstalledAppsListView: View {
|
|||
return base.filter { matches(query, bundleID: $0.key, name: $0.value) }
|
||||
}
|
||||
|
||||
private enum AppListTab: String, CaseIterable, Identifiable {
|
||||
case debuggable
|
||||
case launch
|
||||
private enum AppListTab: Int, CaseIterable, Identifiable {
|
||||
case debuggable
|
||||
case launch
|
||||
|
||||
var id: String { rawValue }
|
||||
var id: Int { rawValue }
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .debuggable: return "Debuggable"
|
||||
case .launch: return "Launch"
|
||||
}
|
||||
var title: String {
|
||||
switch self {
|
||||
case .debuggable: return "Debuggable"
|
||||
case .launch: return "Launch Apps"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private struct LaunchFeedback: Identifiable {
|
||||
let id = UUID()
|
||||
|
|
@ -447,14 +447,18 @@ struct InstalledAppsListView: View {
|
|||
@ViewBuilder
|
||||
private var tabbedContent: some View {
|
||||
VStack(spacing: 14) {
|
||||
Picker("", selection: $selectedTab) {
|
||||
ForEach(AppListTab.allCases) { tab in
|
||||
Text(tab.title.localized)
|
||||
.tag(tab)
|
||||
}
|
||||
}
|
||||
.pickerStyle(.segmented)
|
||||
.padding(.horizontal, 20)
|
||||
SegmentedControl(
|
||||
titles: AppListTab.allCases.map { $0.title.localized },
|
||||
selection: Binding(
|
||||
get: { selectedTab.rawValue },
|
||||
set: { newValue in
|
||||
if let tab = AppListTab(rawValue: newValue) {
|
||||
selectedTab = tab
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
tabContent(for: selectedTab)
|
||||
}
|
||||
|
|
@ -1492,9 +1496,56 @@ extension Dictionary: @retroactive RawRepresentable where Key: Codable, Value: C
|
|||
|
||||
// MARK: - Preview
|
||||
|
||||
#Preview {
|
||||
InstalledAppsListView { _ in }
|
||||
.environment(\.colorScheme, .dark)
|
||||
struct InstalledAppsListView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
InstalledAppsListView { _ in }
|
||||
.environment(\.colorScheme, .dark)
|
||||
}
|
||||
}
|
||||
|
||||
private struct SegmentedControl: UIViewRepresentable {
|
||||
let titles: [String]
|
||||
@Binding var selection: Int
|
||||
|
||||
func makeCoordinator() -> Coordinator {
|
||||
Coordinator(self)
|
||||
}
|
||||
|
||||
func makeUIView(context: Context) -> UISegmentedControl {
|
||||
let control = UISegmentedControl(items: titles)
|
||||
control.selectedSegmentIndex = selection
|
||||
control.addTarget(context.coordinator, action: #selector(Coordinator.valueChanged(_:)), for: .valueChanged)
|
||||
return control
|
||||
}
|
||||
|
||||
func updateUIView(_ uiView: UISegmentedControl, context: Context) {
|
||||
if uiView.numberOfSegments != titles.count {
|
||||
uiView.removeAllSegments()
|
||||
for (index, title) in titles.enumerated() {
|
||||
uiView.insertSegment(withTitle: title, at: index, animated: false)
|
||||
}
|
||||
}
|
||||
|
||||
for (index, title) in titles.enumerated() {
|
||||
uiView.setTitle(title, forSegmentAt: index)
|
||||
}
|
||||
|
||||
if uiView.selectedSegmentIndex != selection {
|
||||
uiView.selectedSegmentIndex = selection
|
||||
}
|
||||
}
|
||||
|
||||
final class Coordinator: NSObject {
|
||||
var parent: SegmentedControl
|
||||
|
||||
init(_ parent: SegmentedControl) {
|
||||
self.parent = parent
|
||||
}
|
||||
|
||||
@objc func valueChanged(_ sender: UISegmentedControl) {
|
||||
parent.selection = sender.selectedSegmentIndex
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class InstalledAppsViewModel: ObservableObject {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue