mirror of
https://github.com/StikDebug/StikDebug.git
synced 2026-10-15 20:10:30 +00:00
New stuff
This commit is contained in:
parent
e59a9fe38f
commit
5fef72528d
6 changed files with 256 additions and 104 deletions
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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.")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
||||||
|
|
|
||||||
|
|
@ -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) {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue