From afcd31fc51852f7f7fe6837cd9618bf409fe120f Mon Sep 17 00:00:00 2001 From: neoarz Date: Fri, 28 Mar 2025 17:07:41 -0400 Subject: [PATCH] Update Settings View --- StikJIT/Views/SettingsView.swift | 674 +++++++++++++++++++++---------- 1 file changed, 453 insertions(+), 221 deletions(-) diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 5c409d2..d05c47c 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -18,269 +18,337 @@ struct SettingsView: View { @State private var pairingFileIsValid = false @State private var isImportingFile = false @State private var importProgress: Float = 0.0 + + // Developer profile image URLs - updated as requested + private let developerProfiles: [String: String] = [ + "Blu": "https://github.com/0-Blu.png", + "jkcoxson": "https://github.com/jkcoxson.png", + "Stossy11": "https://github.com/Stossy11.png", + "Neo": "https://github.com/neoarz.png", + "Se2crid": "https://github.com/Se2crid.png", + "HugeBlack": "https://github.com/HugeBlack.png" + ] var body: some View { ZStack { - selectedBackgroundColor + Color(UIColor.systemBackground) .ignoresSafeArea() - Form { - Section(header: Text("General") - .font(.headline) - .foregroundColor(.primaryText)) { - HStack { - Label("", systemImage: "person.fill") - .foregroundColor(.primaryText) - Spacer() - TextField("Username", text: $username) - .foregroundColor(.primaryText) - .padding(10) - .background(Color.cardBackground) - .cornerRadius(10) - .overlay( - RoundedRectangle(cornerRadius: 10) - .stroke(selectedBackgroundColor, lineWidth: 1) - ) - } - .listRowBackground(Color.cardBackground) - } - - Section(header: Text("Appearance") - .font(.headline) - .foregroundColor(.primaryText)) { - ColorPicker("Background Color", selection: $selectedBackgroundColor) - .onChange(of: selectedBackgroundColor) { newColor in - saveCustomBackgroundColor(newColor) + ScrollView { + VStack(spacing: 12) { + // App Logo and Username Section - No box, directly on background + VStack(spacing: 16) { + // App Logo + Image(uiImage: UIImage(named: selectedAppIcon) ?? UIImage(named: "AppIcon") ?? UIImage()) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 80, height: 80) + .clipShape(RoundedRectangle(cornerRadius: 16)) + .padding(.top, 16) + + Text("StikJIT") + .font(.title2) + .fontWeight(.semibold) + .foregroundColor(.primary) + + // Username Field + VStack(alignment: .leading, spacing: 8) { + Text("Username") + .font(.subheadline) + .foregroundColor(.secondary) + + TextField("Username", text: $username) + .padding(14) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) } - .listRowBackground(Color.cardBackground) - .foregroundColor(.primaryText) - } - - Section(header: Text("Pairing File") - .font(.headline) - .foregroundColor(.primaryText)) { - HStack { - Button { - isShowingPairingFilePicker = true - } label: { - Text("Import New Pairing File") - } - Spacer() + .padding(.horizontal, 16) + .padding(.bottom, 8) } - .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) + Divider() + .padding(.horizontal, 16) + .opacity(0.6) + + // Appearance section + SettingsCard { + VStack(alignment: .leading, spacing: 20) { + Text("Appearance") + .font(.headline) + .foregroundColor(.primary) + .padding(.bottom, 4) + + ColorPicker("Background Color", selection: $selectedBackgroundColor) + .onChange(of: selectedBackgroundColor) { newColor in + saveCustomBackgroundColor(newColor) + } + .foregroundColor(.primary) + .padding(.vertical, 6) + } + .padding(.vertical, 20) + .padding(.horizontal, 16) + } + + // Pairing File section + SettingsCard { + VStack(alignment: .leading, spacing: 20) { + Text("Pairing File") + .font(.headline) + .foregroundColor(.primary) + .padding(.bottom, 4) + + Button { + isShowingPairingFilePicker = true + } label: { + HStack { + Image(systemName: "doc.badge.plus") + .font(.system(size: 18)) + Text("Import New Pairing File") + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 14) + .foregroundColor(.white) + .background(Color.blue) + .cornerRadius(12) } - GeometryReader { geometry in - ZStack(alignment: .leading) { - RoundedRectangle(cornerRadius: 4) - .fill(Color.cardBackground) - .frame(height: 8) + if isImportingFile { + VStack(spacing: 10) { + HStack { + Text("Processing pairing file...") + .font(.system(.caption, design: .rounded)) + .foregroundColor(.secondary) + Spacer() + Text("\(Int(importProgress * 100))%") + .font(.system(.caption, design: .rounded)) + .foregroundColor(.secondary) + } - RoundedRectangle(cornerRadius: 4) - .fill(Color.green) - .frame(width: geometry.size.width * CGFloat(importProgress), height: 8) - .animation(.linear(duration: 0.3), value: importProgress) + GeometryReader { geometry in + ZStack(alignment: .leading) { + RoundedRectangle(cornerRadius: 6) + .fill(Color(UIColor.tertiarySystemFill)) + .frame(height: 10) + + RoundedRectangle(cornerRadius: 6) + .fill(Color.green) + .frame(width: geometry.size.width * CGFloat(importProgress), height: 10) + .animation(.linear(duration: 0.3), value: importProgress) + } + } + .frame(height: 10) } + .padding(.top, 6) + } + + if showPairingFileMessage && pairingFileIsValid { + HStack { + Spacer() + Text("✓ Pairing file successfully imported") + .font(.system(.callout, design: .rounded)) + .foregroundColor(.green) + .padding(.vertical, 10) + .padding(.horizontal, 18) + .background(Color.green.opacity(0.1)) + .cornerRadius(10) + Spacer() + } + .padding(.top, 6) + .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)) + ) + ) } - .frame(height: 8) } - .padding(.vertical, 8) - .listRowBackground(Color.cardBackground) + .padding(.vertical, 20) + .padding(.horizontal, 16) } - 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() + // About section + SettingsCard { + VStack(alignment: .leading, spacing: 20) { + Text("About") + .font(.headline) + .foregroundColor(.primary) + .padding(.bottom, 4) + + // Main Developers (side by side) + VStack(alignment: .leading, spacing: 12) { + Text("Developers") + .font(.subheadline) + .foregroundColor(.secondary) + + // Main developers in a horizontal layout + HStack(spacing: 16) { + // App Creator + VStack(spacing: 8) { + ProfileImage(url: developerProfiles["Blu"] ?? "") + .frame(width: 60, height: 60) + + Text("Blu") + .fontWeight(.semibold) + + Text("App Creator") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(minWidth: 0, maxWidth: .infinity) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) + .onTapGesture { + if let url = URL(string: "https://github.com/0-Blu") { + UIApplication.shared.open(url) + } + } + + // Library Developer + VStack(spacing: 8) { + ProfileImage(url: developerProfiles["jkcoxson"] ?? "") + .frame(width: 60, height: 60) + + Text("jkcoxson") + .fontWeight(.semibold) + + Text("idevice & em_proxy") + .font(.caption) + .foregroundColor(.secondary) + } + .frame(minWidth: 0, maxWidth: .infinity) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) + .onTapGesture { + if let url = URL(string: "https://jkcoxson.com/") { + UIApplication.shared.open(url) + } + } + } + } + + Divider() + .padding(.vertical, 8) + + // Collaborators in vertical stack + VStack(alignment: .leading, spacing: 12) { + Text("Collaborators") + .font(.subheadline) + .foregroundColor(.secondary) + + // Vertical stack of collaborators + VStack(spacing: 12) { + CollaboratorRow(name: "Stossy11", url: "https://github.com/Stossy11", imageUrl: developerProfiles["Stossy11"] ?? "") + + CollaboratorRow(name: "Neo", url: "https://neoarz.xyz/", imageUrl: developerProfiles["Neo"] ?? "") + + CollaboratorRow(name: "Se2crid", url: "https://github.com/Se2crid", imageUrl: developerProfiles["Se2crid"] ?? "") + + CollaboratorRow(name: "HugeBlack", url: "https://github.com/HugeBlack", imageUrl: developerProfiles["HugeBlack"] ?? "") + } + } + + Divider() + .padding(.vertical, 8) + + VStack(alignment: .leading, spacing: 12) { + Text("Links") + .font(.subheadline) + .foregroundColor(.secondary) + + VStack(spacing: 6) { + LinkRow(icon: "link", title: "Source Code", url: "https://github.com/0-Blu/StikJIT") + LinkRow(icon: "xmark.shield", title: "Report an Issue", url: "https://github.com/0-Blu/StikJIT/issues") + + // StikNES promotion - moved here as requested + 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(.secondary) + Spacer() + Image(systemName: "gamecontroller.fill") + .font(.system(size: 18)) + .foregroundColor(.blue) + } + } + .padding(.vertical, 8) + } + } } - .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)) - ) - ) + .padding(.vertical, 20) + .padding(.horizontal, 16) } + + // Version text - now outside of any card, as standalone text at the bottom + HStack { + Spacer() + Text("Version 1.0 • iOS \(UIDevice.current.systemVersion)") + .font(.footnote) + .foregroundColor(.secondary.opacity(0.8)) + Spacer() + } + .padding(.top, 8) + .padding(.bottom, 16) } - - Section(header: Text("About") - .font(.headline) - .foregroundColor(.primaryText)) { - HStack { - Text("Version:") - .foregroundColor(.secondaryText) - Spacer() - Text("1.0") - .foregroundColor(.primaryText) - } - .listRowBackground(Color.cardBackground) - - HStack { - Text("App Creator:") - .foregroundColor(.secondaryText) - Spacer() - Link("Stephen", destination: URL(string: "https://github.com/0-Blu")!) - .foregroundColor(.primaryText) - } - .listRowBackground(Color.cardBackground) - - HStack { - Text("idevice & em_proxy Creator:") - .foregroundColor(.secondaryText) - Spacer() - Link("jkcoxson", destination: URL(string: "https://jkcoxson.com")!) - .foregroundColor(.primaryText) - } - .listRowBackground(Color.cardBackground) - - VStack(alignment: .leading, spacing: 4) { - Text("Collaborators:") - .foregroundColor(.secondaryText) - HStack(spacing: 8) { - Button(action: { - if let url = URL(string: "https://github.com/Stossy11") { - UIApplication.shared.open(url) - } - }) { - Text("Stossy11") - .padding(.vertical, 4) - .contentShape(Rectangle()) - .foregroundColor(.primaryText) - } - .buttonStyle(PlainButtonStyle()) - - Button(action: { - if let url = URL(string: "https://neoarz.xyz/") { - UIApplication.shared.open(url) - } - }) { - Text("Neo") - .padding(.vertical, 4) - .contentShape(Rectangle()) - .foregroundColor(.primaryText) - } - .buttonStyle(PlainButtonStyle()) - - Button(action: { - if let url = URL(string: "https://github.com/Se2crid") { - UIApplication.shared.open(url) - } - }) { - Text("Se2crid") - .padding(.vertical, 4) - .contentShape(Rectangle()) - .foregroundColor(.primaryText) - } - .buttonStyle(PlainButtonStyle()) - - Button(action: { - if let url = URL(string: "https://github.com/HugeBlack") { - UIApplication.shared.open(url) - } - }) { - Text("HugeBlack") - .padding(.vertical, 4) - .contentShape(Rectangle()) - .foregroundColor(.primaryText) - } - .buttonStyle(PlainButtonStyle()) - } - } - .listRowBackground(Color.cardBackground) - - Button(action: { - if let url = URL(string: "https://github.com/0-Blu/StikJIT") { - UIApplication.shared.open(url) - } - }) { - HStack { - Text("View Source Code") - .foregroundColor(.secondaryText) - Spacer() - Image(systemName: "arrow.up.right.square") - .foregroundColor(.primaryText) - } - } - .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.upright.square") - .foregroundColor(.primaryText) - } - } - .listRowBackground(Color.cardBackground) - } + .padding(.horizontal, 16) + .padding(.vertical, 20) } - .background(selectedBackgroundColor) - .scrollContentBackground(.hidden) - .navigationBarTitle("Settings") - .font(.bodyFont) - .accentColor(.accentColor) } - .fileImporter(isPresented: $isShowingPairingFilePicker, - allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList]) { result in + .fileImporter( + isPresented: $isShowingPairingFilePicker, + allowedContentTypes: [UTType(filenameExtension: "mobiledevicepairing", conformingTo: .data)!, .propertyList], + allowsMultipleSelection: false + ) { result in switch result { - case .success(let url): + case .success(let urls): + // Get the first URL from the array + guard let url = urls.first else { return } + let fileManager = FileManager.default let accessing = url.startAccessingSecurityScopedResource() if fileManager.fileExists(atPath: url.path) { do { - let destination = URL.documentsDirectory.appendingPathComponent("pairingFile.plist") - if fileManager.fileExists(atPath: destination.path) { - try fileManager.removeItem(at: destination) + 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: destination) + try fileManager.copyItem(at: url, to: URL.documentsDirectory.appendingPathComponent("pairingFile.plist")) print("File copied successfully!") + // Show progress bar and initialize progress DispatchQueue.main.async { isImportingFile = true importProgress = 0.0 + pairingFileIsValid = false } + // Create timer to update progress let progressTimer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { timer in DispatchQueue.main.async { if importProgress < 1.0 { - importProgress += 0.25 + importProgress += 0.05 } 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 @@ -290,8 +358,12 @@ struct SettingsView: View { } } - startHeartbeatInBackground() + // Ensure timer keeps running RunLoop.current.add(progressTimer, forMode: .common) + + // Start heartbeat in background + startHeartbeatInBackground() + } catch { print("Error copying file: \(error)") } @@ -302,8 +374,8 @@ struct SettingsView: View { if accessing { url.stopAccessingSecurityScopedResource() } - case .failure(_): - print("Failed") + case .failure(let error): + print("Failed to import file: \(error)") } } .onAppear { @@ -311,7 +383,6 @@ struct SettingsView: View { } } - private func loadCustomBackgroundColor() { selectedBackgroundColor = Color(hex: customBackgroundColorHex) ?? Color.primaryBackground } @@ -340,13 +411,174 @@ struct SettingsView: View { .frame(width: 24, height: 24) .clipShape(RoundedRectangle(cornerRadius: 5)) Text(label) - .foregroundColor(.primaryText) + .foregroundColor(.primary) Spacer() } .padding() - .background(Color.white.opacity(0.2)) + .background(Color(UIColor.secondarySystemBackground)) .cornerRadius(10) } .padding(.horizontal) } } + +// Helper components +struct SettingsCard: View { + let content: Content + + init(@ViewBuilder content: () -> Content) { + self.content = content() + } + + var body: some View { + content + .background(Color(UIColor.secondarySystemBackground)) + .cornerRadius(16) + .shadow(color: Color.black.opacity(0.08), radius: 3, x: 0, y: 2) + } +} + +struct InfoRow: View { + var title: String + var value: String + + var body: some View { + HStack { + Text(title) + .foregroundColor(.secondary) + Spacer() + Text(value) + .foregroundColor(.primary) + .fontWeight(.medium) + } + .padding(.vertical, 4) + } +} + +struct LinkRow: View { + var icon: String + var title: String + var url: String + + var body: some View { + Button(action: { + if let url = URL(string: url) { + UIApplication.shared.open(url) + } + }) { + HStack { + Text(title) + .foregroundColor(.secondary) + Spacer() + Image(systemName: icon) + .font(.system(size: 18)) + .foregroundColor(.blue) + } + } + .padding(.vertical, 8) + } +} + +// Component for 2x2 grid layout of collaborators +struct CollaboratorGridItem: View { + var name: String + var url: String + var imageUrl: String + + var body: some View { + Button(action: { + if let url = URL(string: url) { + UIApplication.shared.open(url) + } + }) { + VStack(spacing: 8) { + ProfileImage(url: imageUrl) + .frame(width: 50, height: 50) + + Text(name) + .foregroundColor(.primary) + .fontWeight(.medium) + .font(.subheadline) + } + .frame(minWidth: 80) + .padding(.vertical, 8) + .padding(.horizontal, 12) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) + } + } +} + +struct ProfileImage: View { + var url: String + @State private var image: UIImage? + + var body: some View { + Group { + if let image = image { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fill) + .clipShape(Circle()) + .overlay( + Circle() + .stroke(Color.gray.opacity(0.2), lineWidth: 1) + ) + } else { + Circle() + .fill(Color(UIColor.systemGray4)) + .overlay( + Image(systemName: "person.fill") + .font(.system(size: 16)) + .foregroundColor(.white) + ) + .onAppear { + loadImage() + } + } + } + } + + private func loadImage() { + guard let imageUrl = URL(string: url) else { return } + + URLSession.shared.dataTask(with: imageUrl) { data, response, error in + if let data = data, let downloadedImage = UIImage(data: data) { + DispatchQueue.main.async { + self.image = downloadedImage + } + } + }.resume() + } +} + +// Component for vertical collaborator list - Removed background for cleaner look +struct CollaboratorRow: View { + var name: String + var url: String + var imageUrl: String + + var body: some View { + Button(action: { + if let url = URL(string: url) { + UIApplication.shared.open(url) + } + }) { + HStack(spacing: 12) { + ProfileImage(url: imageUrl) + .frame(width: 40, height: 40) + + Text(name) + .foregroundColor(.primary) + .fontWeight(.medium) + + Spacer() + + Image(systemName: "link") + .font(.system(size: 16)) + .foregroundColor(.blue) + } + .padding(.vertical, 8) + } + } +} \ No newline at end of file