From 5fef72528d6c1f94ceb9cb70f52daa02100813cc Mon Sep 17 00:00:00 2001 From: Stephen B <158498287+StephenDev0@users.noreply.github.com> Date: Tue, 2 Dec 2025 22:40:25 -0500 Subject: [PATCH] New stuff --- StikDebug.xcodeproj/project.pbxproj | 16 +- StikJIT/StikJITApp.swift | 165 +++++++++--------- .../ContinuedProcessingManager.swift | 20 +++ StikJIT/Views/HomeView.swift | 41 +++++ StikJIT/Views/ProfileView.swift | 41 ++++- StikJIT/Views/SettingsView.swift | 77 +++++++- 6 files changed, 256 insertions(+), 104 deletions(-) diff --git a/StikDebug.xcodeproj/project.pbxproj b/StikDebug.xcodeproj/project.pbxproj index a39e5b3..966ad40 100644 --- a/StikDebug.xcodeproj/project.pbxproj +++ b/StikDebug.xcodeproj/project.pbxproj @@ -719,18 +719,18 @@ "$(PROJECT_DIR)/StikJIT/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 2.3.5; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 2.2; }; name = Debug; @@ -775,17 +775,17 @@ "$(PROJECT_DIR)/StikJIT/idevice", ); MACOSX_DEPLOYMENT_TARGET = 15.1; - MARKETING_VERSION = 2.3.5; + MARKETING_VERSION = 26.0; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_NAME = "$(TARGET_NAME)"; SDKROOT = auto; - SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; + SUPPORTED_PLATFORMS = "iphoneos iphonesimulator"; SUPPORTS_MACCATALYST = NO; - SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; + SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h"; SWIFT_VERSION = 5.0; - TARGETED_DEVICE_FAMILY = "1,2,7"; + TARGETED_DEVICE_FAMILY = "1,2"; XROS_DEPLOYMENT_TARGET = 2.2; }; name = Release; diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index d0e62f1..27357fc 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -511,20 +511,6 @@ struct HeartbeatApp: App { @StateObject private var themeExpansionManager = ThemeExpansionManager() @Environment(\.scenePhase) private var scenePhase // Observe scene lifecycle - let urls: [String] = [ - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist", - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg", - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" - ] - - let outputDir: String = "DDI" - - let outputFiles: [String] = [ - "DDI/BuildManifest.plist", - "DDI/Image.dmg", - "DDI/Image.dmg.trustcache" - ] - init() { registerAdvancedOptionsDefault() ContinuedProcessingManager.shared.configureIfNeeded() @@ -583,16 +569,20 @@ struct HeartbeatApp: App { BackgroundContainer { 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 != "") { + Task { + let fileManager = FileManager.default + for item in ddiDownloadItems { + let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) + if fileManager.fileExists(atPath: destinationURL.path) { continue } + do { + try await downloadFile(from: item.urlString, to: destinationURL) + } catch { + await MainActor.run { alert_title = "An Error has Occurred" - alert_string = "[Download DDI Error]: " + result + alert_string = "[Download DDI Error]: \(error.localizedDescription)" show_alert = true } + break } } } @@ -961,75 +951,76 @@ public func showAlert(title: String, message: String, showOk: Bool, showTryAgain } } -func downloadFile(from urlString: String, to destinationURL: URL, completion: @escaping (String) -> Void) { - let fileManager = FileManager.default - let documentsDirectory = URL.documentsDirectory +private struct DDIDownloadItem { + let name: String + let relativePath: String + let urlString: String +} + +private let ddiDownloadItems: [DDIDownloadItem] = [ + .init( + name: "Build Manifest", + relativePath: "DDI/BuildManifest.plist", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist" + ), + .init( + name: "Image", + relativePath: "DDI/Image.dmg", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg" + ), + .init( + name: "TrustCache", + relativePath: "DDI/Image.dmg.trustcache", + urlString: "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" + ) +] + +enum DDIDownloadError: LocalizedError { + case invalidURL(String) + var errorDescription: String? { + switch self { + case .invalidURL(let string): + return "Invalid download URL: \(string)" + } + } +} + +func downloadFile(from urlString: String, to destinationURL: URL) async throws { guard let url = URL(string: urlString) else { - print("Invalid URL: \(urlString)") - completion("[Internal Invalid URL error]") - return + throw DDIDownloadError.invalidURL(urlString) } - - let task = URLSession.shared.downloadTask(with: url) { (tempLocalUrl, response, error) in - guard let tempLocalUrl = tempLocalUrl, error == nil else { - print("Error downloading file from \(urlString): \(String(describing: error))") - completion("Are you connected to the internet? [Download Failed]") - return - } - - do { - try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil) - try fileManager.moveItem(at: tempLocalUrl, to: destinationURL) - print("Downloaded \(urlString) to \(destinationURL.path)") - } catch { - print("Error saving file: \(error)") - } - } - task.resume() - completion("") -} - -func redownloadDDI() { + let (tempLocalUrl, _) = try await URLSession.shared.download(from: url) let fileManager = FileManager.default - let ddiFiles = [ - "DDI/BuildManifest.plist", - "DDI/Image.dmg", - "DDI/Image.dmg.trustcache" - ] - - for file in ddiFiles { - let fileURL = URL.documentsDirectory.appendingPathComponent(file) - if fileManager.fileExists(atPath: fileURL.path) { - do { - try fileManager.removeItem(at: fileURL) - print("Deleted \(file)") - } catch { - print("Error deleting \(file): \(error)") - } - } - } - - let urls = [ - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist", - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg", - "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/Image.dmg.trustcache" - ] - - let group = DispatchGroup() - - for (index, urlString) in urls.enumerated() { - let destinationURL = URL.documentsDirectory.appendingPathComponent(ddiFiles[index]) - group.enter() - downloadFile(from: urlString, to: destinationURL) { result in - if (result != "") { - showAlert(title: "An Error has Occurred", message: "[Download DDI Error]: " + result, showOk: true) - } - group.leave() - } - } - - group.notify(queue: .main) { - showAlert(title: "Success", message: "DDI files have been redownloaded.", showOk: true) + try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(), + withIntermediateDirectories: true) + if fileManager.fileExists(atPath: destinationURL.path) { + try fileManager.removeItem(at: destinationURL) } + try fileManager.moveItem(at: tempLocalUrl, to: destinationURL) +} + +func redownloadDDI(progressHandler: ((Double, String) -> Void)? = nil) async throws { + let fileManager = FileManager.default + let totalStages = Double(ddiDownloadItems.count + 1) + var completedStages = 0.0 + + progressHandler?(0.0, "Removing existing DDI files…") + for item in ddiDownloadItems { + let fileURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) + if fileManager.fileExists(atPath: fileURL.path) { + try fileManager.removeItem(at: fileURL) + } + } + completedStages += 1.0 + progressHandler?(completedStages / totalStages, "Starting downloads…") + + for item in ddiDownloadItems { + progressHandler?(completedStages / totalStages, "Downloading \(item.name)…") + let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath) + try await downloadFile(from: item.urlString, to: destinationURL) + completedStages += 1.0 + progressHandler?(completedStages / totalStages, "\(item.name) ready") + } + progressHandler?(1.0, "DDI download complete.") } diff --git a/StikJIT/Utilities/ContinuedProcessingManager.swift b/StikJIT/Utilities/ContinuedProcessingManager.swift index b358020..ac86cac 100644 --- a/StikJIT/Utilities/ContinuedProcessingManager.swift +++ b/StikJIT/Utilities/ContinuedProcessingManager.swift @@ -25,6 +25,10 @@ final class ContinuedProcessingManager { func configureIfNeeded() { handler.configureIfNeeded() } + + func cancelPendingTasks() { + handler.cancelPendingTasks() + } func begin(title: String, subtitle: String) { handler.begin(title: title, subtitle: subtitle) @@ -42,6 +46,7 @@ final class ContinuedProcessingManager { private protocol ContinuedProcessingHandling: AnyObject { var isSupported: Bool { get } func configureIfNeeded() + func cancelPendingTasks() func begin(title: String, subtitle: String) func updateProgress(_ fraction: Double) func finish(success: Bool) @@ -50,6 +55,7 @@ private protocol ContinuedProcessingHandling: AnyObject { private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling { var isSupported: Bool { false } func configureIfNeeded() {} + func cancelPendingTasks() {} func begin(title: String, subtitle: String) {} func updateProgress(_ fraction: Double) {} func finish(success: Bool) {} @@ -95,6 +101,8 @@ private final class ModernContinuedProcessingHandler: ContinuedProcessingHandlin } } guard reserved else { return } + // Clear any stale request that might block new submissions. + scheduler.cancel(taskRequestWithIdentifier: taskIdentifier) let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, title: title, subtitle: subtitle) @@ -110,6 +118,18 @@ private final class ModernContinuedProcessingHandler: ContinuedProcessingHandlin } } + func cancelPendingTasks() { + queue.async { [weak self] in + guard let self else { return } + if let task = activeTask { + task.setTaskCompleted(success: false) + activeTask = nil + } + pendingMetadata = nil + scheduler.cancel(taskRequestWithIdentifier: taskIdentifier) + } + } + func updateProgress(_ fraction: Double) { queue.async { [weak self] in guard let task = self?.activeTask else { return } diff --git a/StikJIT/Views/HomeView.swift b/StikJIT/Views/HomeView.swift index 9496a25..d7d7adc 100644 --- a/StikJIT/Views/HomeView.swift +++ b/StikJIT/Views/HomeView.swift @@ -91,6 +91,32 @@ struct HomeView: View { !ddiMounted && !heartbeatOK } + private var sanitizedUsername: String { + let trimmed = username.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmed.isEmpty ? "there" : trimmed + } + private var greetingTitle: String { + "\(timeOfDayGreeting), \(sanitizedUsername)!" + } + private var greetingSubtitle: String { + if canConnectByApp { + return "You're all set. Connect whenever you're ready." + } else if !pairingFileExists { + return "Import your pairing file to start debugging." + } else if !ddiMounted { + return "Mount the DDI to finish preparing your device." + } + return "Complete the steps below to get ready." + } + private var timeOfDayGreeting: String { + let hour = Calendar.current.component(.hour, from: Date()) + switch hour { + case 5..<12: return "Good morning" + case 12..<17: return "Good afternoon" + case 17..<22: return "Good evening" + default: return "Hello" + } + } private var shouldPromptForWiFi: Bool { pairingFileLikelyInvalid && !wifiConnected && isCellularActive } @@ -105,6 +131,7 @@ struct HomeView: View { ScrollView { VStack(spacing: 20) { + welcomeCard setupCard connectCard // if pairingFileExists { @@ -352,6 +379,20 @@ struct HomeView: View { // MARK: - Styled Sections + private var welcomeCard: some View { + homeCard { + VStack(alignment: .leading, spacing: 6) { + Text(greetingTitle) + .font(.system(.title2, design: .rounded).weight(.semibold)) + .foregroundStyle(.primary) + Text(greetingSubtitle) + .font(.subheadline) + .foregroundStyle(.secondary) + } + .frame(maxWidth: .infinity, alignment: .leading) + } + } + private var setupCard: some View { homeCard { VStack(alignment: .leading, spacing: 16) { diff --git a/StikJIT/Views/ProfileView.swift b/StikJIT/Views/ProfileView.swift index 8896a3b..0c77cb0 100644 --- a/StikJIT/Views/ProfileView.swift +++ b/StikJIT/Views/ProfileView.swift @@ -150,10 +150,12 @@ struct ProfileView: View { } + @AppStorage("customAccentColor") private var customAccentColorHex: String = "" @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @Environment(\.themeExpansionManager) private var themeExpansion private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } + private var accentColor: Color { themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue } var body: some View { NavigationStack { @@ -290,13 +292,13 @@ struct ProfileView: View { .foregroundColor(.secondary) .textSelection(.enabled) Spacer() - HStack(spacing: 12) { - Button { saveProfile(profile: entry) } label: { Image(systemName: "square.and.arrow.down") } - Button(role: .destructive) { - removeTargetName = entry.appName - removeTargetUUID = entry.uuid - confirmRemove = true - } label: { Image(systemName: "trash") } + HStack(spacing: 10) { + profileActionButton(icon: "square.and.arrow.down", color: accentColor) { + saveProfile(profile: entry) + } + profileActionButton(icon: "trash", color: .refreshRed) { + removeProfilePrompt(entry: entry) + } } } .padding(.vertical, 4) @@ -331,6 +333,31 @@ struct ProfileView: View { .shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) } + private func profileActionButton(icon: String, color: Color, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(.system(size: 13, weight: .bold)) + .foregroundStyle(color.contrastText()) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .fill(color.opacity(0.9)) + ) + .overlay( + RoundedRectangle(cornerRadius: 12, style: .continuous) + .stroke(Color.white.opacity(0.2), lineWidth: 1) + ) + } + .buttonStyle(.plain) + } + + private func removeProfilePrompt(entry: Profile) { + removeTargetName = entry.appName + removeTargetUUID = entry.uuid + confirmRemove = true + } + func loadProfiles() async { working = true do { diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 5a5cf26..1550f9a 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -33,6 +33,11 @@ struct SettingsView: View { @State private var showRemovePairingFileDialog = false @State private var is_lc = false @State private var showColorPickerPopup = false + @State private var showDDIConfirmation = false + @State private var isRedownloadingDDI = false + @State private var ddiDownloadProgress: Double = 0.0 + @State private var ddiStatusMessage: String = "" + @State private var ddiResultMessage: (text: String, isError: Bool)? @State private var showingDisplayView = false @@ -201,6 +206,14 @@ struct SettingsView: View { print("Failed to import file: \(error)") } } + .confirmationDialog("Redownload DDI Files?", isPresented: $showDDIConfirmation, titleVisibility: .visible) { + Button("Redownload", role: .destructive) { + redownloadDDIPressed() + } + Button("Cancel", role: .cancel) { } + } message: { + Text("Existing DDI files will be removed before downloading fresh copies.") + } } // MARK: - Cards @@ -358,6 +371,13 @@ struct SettingsView: View { enableTesting = false } } + .onChange(of: enableContinuedProcessing) { _, newValue in + if newValue { + ContinuedProcessingManager.shared.configureIfNeeded() + } else { + ContinuedProcessingManager.shared.cancelPendingTasks() + } + } } } @@ -399,7 +419,7 @@ struct SettingsView: View { } .padding(.vertical, 8) } - Button(action: { redownloadDDIPressed() }) { + Button(action: { showDDIConfirmation = true }) { HStack { Image(systemName: "arrow.down.circle") .font(.system(size: 18)) @@ -410,6 +430,22 @@ struct SettingsView: View { } .padding(.vertical, 8) } + .disabled(isRedownloadingDDI) + + if isRedownloadingDDI { + VStack(alignment: .leading, spacing: 6) { + ProgressView(value: ddiDownloadProgress, total: 1.0) + .progressViewStyle(.linear) + .tint(accentColor) + Text(ddiStatusMessage) + .font(.caption) + .foregroundColor(.secondary) + } + } else if let result = ddiResultMessage { + Text(result.text) + .font(.caption) + .foregroundColor(result.isError ? .red : .green) + } } } } @@ -562,7 +598,44 @@ struct SettingsView: View { } private func redownloadDDIPressed() { - redownloadDDI() + guard !isRedownloadingDDI else { return } + Task { + await MainActor.run { + isRedownloadingDDI = true + ddiDownloadProgress = 0 + ddiStatusMessage = "Preparing download…" + ddiResultMessage = nil + } + do { + try await redownloadDDI { progress, status in + Task { @MainActor in + self.ddiDownloadProgress = progress + self.ddiStatusMessage = status + } + } + await MainActor.run { + isRedownloadingDDI = false + ddiResultMessage = ("DDI files refreshed successfully.", false) + } + } catch { + await MainActor.run { + isRedownloadingDDI = false + ddiResultMessage = ("Failed to redownload DDI files: \(error.localizedDescription)", true) + } + } + } + scheduleDDIStatusDismiss() + } + + private func scheduleDDIStatusDismiss() { + Task { + try? await Task.sleep(nanoseconds: 4_000_000_000) + await MainActor.run { + if !isRedownloadingDDI { + ddiResultMessage = nil + } + } + } } }