New stuff

This commit is contained in:
Stephen B 2025-12-02 22:40:25 -05:00
parent e59a9fe38f
commit 5fef72528d
6 changed files with 256 additions and 104 deletions

View file

@ -719,18 +719,18 @@
"$(PROJECT_DIR)/StikJIT/idevice", "$(PROJECT_DIR)/StikJIT/idevice",
); );
MACOSX_DEPLOYMENT_TARGET = 15.1; MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 2.3.5; MARKETING_VERSION = 26.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h";
SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_OPTIMIZATION_LEVEL = "-Onone";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7"; TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Debug; name = Debug;
@ -775,17 +775,17 @@
"$(PROJECT_DIR)/StikJIT/idevice", "$(PROJECT_DIR)/StikJIT/idevice",
); );
MACOSX_DEPLOYMENT_TARGET = 15.1; MACOSX_DEPLOYMENT_TARGET = 15.1;
MARKETING_VERSION = 2.3.5; MARKETING_VERSION = 26.0;
PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj; PRODUCT_BUNDLE_IDENTIFIER = com.stik.sj;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
SDKROOT = auto; SDKROOT = auto;
SUPPORTED_PLATFORMS = "iphoneos iphonesimulator xros xrsimulator"; SUPPORTED_PLATFORMS = "iphoneos iphonesimulator";
SUPPORTS_MACCATALYST = NO; SUPPORTS_MACCATALYST = NO;
SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = YES; SUPPORTS_MAC_DESIGNED_FOR_IPHONE_IPAD = NO;
SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h"; SWIFT_OBJC_BRIDGING_HEADER = "StikJIT/StikJIT-Bridging-Header.h";
SWIFT_VERSION = 5.0; SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2,7"; TARGETED_DEVICE_FAMILY = "1,2";
XROS_DEPLOYMENT_TARGET = 2.2; XROS_DEPLOYMENT_TARGET = 2.2;
}; };
name = Release; name = Release;

View file

@ -511,20 +511,6 @@ struct HeartbeatApp: App {
@StateObject private var themeExpansionManager = ThemeExpansionManager() @StateObject private var themeExpansionManager = ThemeExpansionManager()
@Environment(\.scenePhase) private var scenePhase // Observe scene lifecycle @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() { init() {
registerAdvancedOptionsDefault() registerAdvancedOptionsDefault()
ContinuedProcessingManager.shared.configureIfNeeded() ContinuedProcessingManager.shared.configureIfNeeded()
@ -583,16 +569,20 @@ struct HeartbeatApp: App {
BackgroundContainer { BackgroundContainer {
MainTabView() MainTabView()
.onAppear { .onAppear {
let fileManager = FileManager.default Task {
for (index, urlString) in urls.enumerated() { let fileManager = FileManager.default
let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index]) for item in ddiDownloadItems {
if !fileManager.fileExists(atPath: destinationURL.path) { let destinationURL = URL.documentsDirectory.appendingPathComponent(item.relativePath)
downloadFile(from: urlString, to: destinationURL) { result in if fileManager.fileExists(atPath: destinationURL.path) { continue }
if (result != "") { do {
try await downloadFile(from: item.urlString, to: destinationURL)
} catch {
await MainActor.run {
alert_title = "An Error has Occurred" alert_title = "An Error has Occurred"
alert_string = "[Download DDI Error]: " + result alert_string = "[Download DDI Error]: \(error.localizedDescription)"
show_alert = true 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) { private struct DDIDownloadItem {
let fileManager = FileManager.default let name: String
let documentsDirectory = URL.documentsDirectory 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 { guard let url = URL(string: urlString) else {
print("Invalid URL: \(urlString)") throw DDIDownloadError.invalidURL(urlString)
completion("[Internal Invalid URL error]")
return
} }
let (tempLocalUrl, _) = try await URLSession.shared.download(from: url)
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 fileManager = FileManager.default let fileManager = FileManager.default
let ddiFiles = [ try fileManager.createDirectory(at: destinationURL.deletingLastPathComponent(),
"DDI/BuildManifest.plist", withIntermediateDirectories: true)
"DDI/Image.dmg", if fileManager.fileExists(atPath: destinationURL.path) {
"DDI/Image.dmg.trustcache" try fileManager.removeItem(at: destinationURL)
]
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.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.")
} }

View file

@ -25,6 +25,10 @@ final class ContinuedProcessingManager {
func configureIfNeeded() { func configureIfNeeded() {
handler.configureIfNeeded() handler.configureIfNeeded()
} }
func cancelPendingTasks() {
handler.cancelPendingTasks()
}
func begin(title: String, subtitle: String) { func begin(title: String, subtitle: String) {
handler.begin(title: title, subtitle: subtitle) handler.begin(title: title, subtitle: subtitle)
@ -42,6 +46,7 @@ final class ContinuedProcessingManager {
private protocol ContinuedProcessingHandling: AnyObject { private protocol ContinuedProcessingHandling: AnyObject {
var isSupported: Bool { get } var isSupported: Bool { get }
func configureIfNeeded() func configureIfNeeded()
func cancelPendingTasks()
func begin(title: String, subtitle: String) func begin(title: String, subtitle: String)
func updateProgress(_ fraction: Double) func updateProgress(_ fraction: Double)
func finish(success: Bool) func finish(success: Bool)
@ -50,6 +55,7 @@ private protocol ContinuedProcessingHandling: AnyObject {
private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling { private final class NoopContinuedProcessingHandler: ContinuedProcessingHandling {
var isSupported: Bool { false } var isSupported: Bool { false }
func configureIfNeeded() {} func configureIfNeeded() {}
func cancelPendingTasks() {}
func begin(title: String, subtitle: String) {} func begin(title: String, subtitle: String) {}
func updateProgress(_ fraction: Double) {} func updateProgress(_ fraction: Double) {}
func finish(success: Bool) {} func finish(success: Bool) {}
@ -95,6 +101,8 @@ private final class ModernContinuedProcessingHandler: ContinuedProcessingHandlin
} }
} }
guard reserved else { return } guard reserved else { return }
// Clear any stale request that might block new submissions.
scheduler.cancel(taskRequestWithIdentifier: taskIdentifier)
let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier, let request = BGContinuedProcessingTaskRequest(identifier: taskIdentifier,
title: title, title: title,
subtitle: subtitle) 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) { func updateProgress(_ fraction: Double) {
queue.async { [weak self] in queue.async { [weak self] in
guard let task = self?.activeTask else { return } guard let task = self?.activeTask else { return }

View file

@ -91,6 +91,32 @@ struct HomeView: View {
!ddiMounted && !ddiMounted &&
!heartbeatOK !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 { private var shouldPromptForWiFi: Bool {
pairingFileLikelyInvalid && !wifiConnected && isCellularActive pairingFileLikelyInvalid && !wifiConnected && isCellularActive
} }
@ -105,6 +131,7 @@ struct HomeView: View {
ScrollView { ScrollView {
VStack(spacing: 20) { VStack(spacing: 20) {
welcomeCard
setupCard setupCard
connectCard connectCard
// if pairingFileExists { // if pairingFileExists {
@ -352,6 +379,20 @@ struct HomeView: View {
// MARK: - Styled Sections // 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 { private var setupCard: some View {
homeCard { homeCard {
VStack(alignment: .leading, spacing: 16) { VStack(alignment: .leading, spacing: 16) {

View file

@ -150,10 +150,12 @@ struct ProfileView: View {
} }
@AppStorage("customAccentColor") private var customAccentColorHex: String = ""
@AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue @AppStorage("appTheme") private var appThemeRaw: String = AppTheme.system.rawValue
@Environment(\.themeExpansionManager) private var themeExpansion @Environment(\.themeExpansionManager) private var themeExpansion
private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle } private var backgroundStyle: BackgroundStyle { themeExpansion?.backgroundStyle(for: appThemeRaw) ?? AppTheme.system.backgroundStyle }
private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) } private var preferredScheme: ColorScheme? { themeExpansion?.preferredColorScheme(for: appThemeRaw) }
private var accentColor: Color { themeExpansion?.resolvedAccentColor(from: customAccentColorHex) ?? .blue }
var body: some View { var body: some View {
NavigationStack { NavigationStack {
@ -290,13 +292,13 @@ struct ProfileView: View {
.foregroundColor(.secondary) .foregroundColor(.secondary)
.textSelection(.enabled) .textSelection(.enabled)
Spacer() Spacer()
HStack(spacing: 12) { HStack(spacing: 10) {
Button { saveProfile(profile: entry) } label: { Image(systemName: "square.and.arrow.down") } profileActionButton(icon: "square.and.arrow.down", color: accentColor) {
Button(role: .destructive) { saveProfile(profile: entry)
removeTargetName = entry.appName }
removeTargetUUID = entry.uuid profileActionButton(icon: "trash", color: .refreshRed) {
confirmRemove = true removeProfilePrompt(entry: entry)
} label: { Image(systemName: "trash") } }
} }
} }
.padding(.vertical, 4) .padding(.vertical, 4)
@ -331,6 +333,31 @@ struct ProfileView: View {
.shadow(color: .black.opacity(0.15), radius: 12, x: 0, y: 4) .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 { func loadProfiles() async {
working = true working = true
do { do {

View file

@ -33,6 +33,11 @@ struct SettingsView: View {
@State private var showRemovePairingFileDialog = false @State private var showRemovePairingFileDialog = false
@State private var is_lc = false @State private var is_lc = false
@State private var showColorPickerPopup = 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 @State private var showingDisplayView = false
@ -201,6 +206,14 @@ struct SettingsView: View {
print("Failed to import file: \(error)") 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 // MARK: - Cards
@ -358,6 +371,13 @@ struct SettingsView: View {
enableTesting = false 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) .padding(.vertical, 8)
} }
Button(action: { redownloadDDIPressed() }) { Button(action: { showDDIConfirmation = true }) {
HStack { HStack {
Image(systemName: "arrow.down.circle") Image(systemName: "arrow.down.circle")
.font(.system(size: 18)) .font(.system(size: 18))
@ -410,6 +430,22 @@ struct SettingsView: View {
} }
.padding(.vertical, 8) .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() { 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
}
}
}
} }
} }