diff --git a/StikJIT/Assets.xcassets/DarkUI.imageset/Contents.json b/StikJIT/Assets.xcassets/DarkUI.imageset/Contents.json new file mode 100644 index 0000000..c4b1af8 --- /dev/null +++ b/StikJIT/Assets.xcassets/DarkUI.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "DarkUI.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StikJIT/Assets.xcassets/DarkUI.imageset/DarkUI.png b/StikJIT/Assets.xcassets/DarkUI.imageset/DarkUI.png new file mode 100644 index 0000000..a1ce29b Binary files /dev/null and b/StikJIT/Assets.xcassets/DarkUI.imageset/DarkUI.png differ diff --git a/StikJIT/Assets.xcassets/LightUI.imageset/Contents.json b/StikJIT/Assets.xcassets/LightUI.imageset/Contents.json new file mode 100644 index 0000000..fa042e1 --- /dev/null +++ b/StikJIT/Assets.xcassets/LightUI.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "LightUI.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StikJIT/Assets.xcassets/LightUI.imageset/LightUI.png b/StikJIT/Assets.xcassets/LightUI.imageset/LightUI.png new file mode 100644 index 0000000..e98446f Binary files /dev/null and b/StikJIT/Assets.xcassets/LightUI.imageset/LightUI.png differ diff --git a/StikJIT/Assets.xcassets/System.imageset/Contents.json b/StikJIT/Assets.xcassets/System.imageset/Contents.json new file mode 100644 index 0000000..dc33a59 --- /dev/null +++ b/StikJIT/Assets.xcassets/System.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "System.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/StikJIT/Assets.xcassets/System.imageset/System.png b/StikJIT/Assets.xcassets/System.imageset/System.png new file mode 100644 index 0000000..9cc8865 Binary files /dev/null and b/StikJIT/Assets.xcassets/System.imageset/System.png differ diff --git a/StikJIT/StikJITApp.swift b/StikJIT/StikJITApp.swift index fb5c5ae..aaeab13 100644 --- a/StikJIT/StikJITApp.swift +++ b/StikJIT/StikJITApp.swift @@ -91,7 +91,9 @@ struct HeartbeatApp: App { @State private var alert_string = "" @State private var alert_title = "" @StateObject private var mount = MountingProgress.shared + @AppStorage("appTheme") private var appTheme: String = "system" @Environment(\.scenePhase) private var scenePhase + let urls: [String] = [ "https://github.com/doronz88/DeveloperDiskImage/raw/refs/heads/main/PersonalizedImages/Xcode_iOS_DDI_Personalized/BuildManifest.plist", @@ -112,6 +114,19 @@ struct HeartbeatApp: App { let fixMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.fix_init(forOpeningContentTypes:asCopy:)))! let origMethod = class_getInstanceMethod(UIDocumentPickerViewController.self, #selector(UIDocumentPickerViewController.init(forOpeningContentTypes:asCopy:)))! method_exchangeImplementations(origMethod, fixMethod) + + // Apply theme immediately when app launches + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + switch appTheme { + case "dark": + window.overrideUserInterfaceStyle = .dark + case "light": + window.overrideUserInterfaceStyle = .light + default: + window.overrideUserInterfaceStyle = .unspecified + } + } } func newVerCheck() { let currentDate = Calendar.current.startOfDay(for: Date()) @@ -138,6 +153,19 @@ struct HeartbeatApp: App { UserDefaults.standard.set(currentDate, forKey: "VersionUpdateAlert") } } + private func applyTheme() { + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + switch appTheme { + case "dark": + window.overrideUserInterfaceStyle = .dark + case "light": + window.overrideUserInterfaceStyle = .light + default: + window.overrideUserInterfaceStyle = .unspecified + } + } + } var body: some Scene { WindowGroup { if isLoading2 { @@ -214,6 +242,7 @@ struct HeartbeatApp: App { } else { MainTabView() .onAppear() { + applyTheme() let fileManager = FileManager.default for (index, urlString) in urls.enumerated() { let destinationURL = URL.documentsDirectory.appendingPathComponent(outputFiles[index]) @@ -462,6 +491,7 @@ struct LoadingView: View { @State private var animate = false @Environment(\.colorScheme) private var colorScheme @AppStorage("customAccentColor") private var customAccentColorHex: String = "" + @AppStorage("appTheme") private var appTheme: String = "system" private var accentColor: Color { if customAccentColorHex.isEmpty { @@ -471,10 +501,21 @@ struct LoadingView: View { } } + private var isDarkMode: Bool { + switch appTheme { + case "dark": + return true + case "light": + return false + default: + return colorScheme == .dark + } + } + var body: some View { ZStack { - // Use system background color instead of fixed black - Color(colorScheme == .dark ? .black : .white) + // Use theme-aware background color + Color(isDarkMode ? .black : .white) .ignoresSafeArea() VStack { @@ -482,7 +523,7 @@ struct LoadingView: View { // Background circle - slightly visible in both themes Circle() .stroke(lineWidth: 8) - .foregroundColor(colorScheme == .dark ? + .foregroundColor(isDarkMode ? Color.white.opacity(0.3) : Color.black.opacity(0.1)) .frame(width: 80, height: 80) @@ -511,7 +552,7 @@ struct LoadingView: View { // Text adapts to theme Text("Loading...") .font(.system(size: 20, weight: .medium, design: .rounded)) - .foregroundColor(colorScheme == .dark ? + .foregroundColor(isDarkMode ? .white.opacity(0.8) : .black.opacity(0.8)) .padding(.top, 20) diff --git a/StikJIT/Views/AppIconView.swift b/StikJIT/Views/AppIconView.swift new file mode 100644 index 0000000..3ae358e --- /dev/null +++ b/StikJIT/Views/AppIconView.swift @@ -0,0 +1,53 @@ +import SwiftUI + +struct AppIconView: View { + @Environment(\.dismiss) private var dismiss + @AppStorage("customAccentColor") private var customAccentColorHex: String = "" + + private var accentColor: Color { + if customAccentColorHex.isEmpty { + return .blue + } else { + return Color(hex: customAccentColorHex) ?? .blue + } + } + + var body: some View { + NavigationView { + ZStack { + Color(UIColor.systemBackground) + .ignoresSafeArea() + + VStack(spacing: 24) { + Image(systemName: "hammer.fill") + .font(.system(size: 60)) + .foregroundColor(accentColor) + .padding(.bottom, 8) + + Text("Work in Progress") + .font(.system(size: 24, weight: .bold)) + .multilineTextAlignment(.center) + + Text("App icon customization coming soon!") + .font(.system(size: 17)) + .foregroundColor(.secondary) + .multilineTextAlignment(.center) + } + .padding() + } + .navigationTitle("App Icon") + .navigationBarTitleDisplayMode(.inline) + .toolbar { + ToolbarItem(placement: .navigationBarTrailing) { + Button("Done") { + dismiss() + } + } + } + } + } +} + +#Preview { + AppIconView() +} \ No newline at end of file diff --git a/StikJIT/Views/DisplayView.swift b/StikJIT/Views/DisplayView.swift new file mode 100644 index 0000000..0bfb9e7 --- /dev/null +++ b/StikJIT/Views/DisplayView.swift @@ -0,0 +1,263 @@ +// DisplayView.swift +// StikJIT +// +// Created by neoarz on 4/9/25. + +import SwiftUI + +struct AccentColorPicker: View { + @Binding var selectedColor: Color + + let colors: [Color] = [ + .blue, // Default system blue + .init(hex: "#7FFFD4")!, // Aqua + .init(hex: "#50C878")!, // Green + .red, // Red + .init(hex: "#6A5ACD")!, // Purple + .init(hex: "#DA70D6")!, // Pink + .white, // white + .black // black + ] + + var body: some View { + VStack(alignment: .leading, spacing: 16) { + Text("Accent Color") + .font(.headline) + .foregroundColor(.primary) + + LazyVGrid(columns: Array(repeating: .init(.flexible(), spacing: 12), count: 9), spacing: 12) { + ForEach(colors, id: \.self) { color in + Circle() + .fill(color) + .frame(width: 28, height: 28) + .overlay( + Circle() + .stroke(Color.primary.opacity(0.1), lineWidth: 1) + ) + .overlay( + Circle() + .stroke(selectedColor == color ? Color.primary : Color.clear, lineWidth: 2) + ) + .onTapGesture { + selectedColor = color + } + } + + // Direct Color Picker Circle + ColorPicker("", selection: $selectedColor) + .labelsHidden() + .frame(width: 28, height: 28) + .overlay( + Circle() + .stroke(Color.primary.opacity(0.1), lineWidth: 1) + ) + .clipShape(Circle()) + } + .frame(maxWidth: .infinity) + } + .padding(.top, 12) + } +} + +struct DisplayView: View { + @AppStorage("username") private var username = "User" + @AppStorage("customAccentColor") private var customAccentColorHex: String = "" + @AppStorage("appTheme") private var appTheme: String = "system" + @AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true + @State private var selectedAccentColor: Color = .blue + @Environment(\.colorScheme) private var colorScheme + + private var accentColor: Color { + if customAccentColorHex.isEmpty { + return .blue + } else { + return Color(hex: customAccentColorHex) ?? .blue + } + } + + var body: some View { + ZStack { + Color(colorScheme == .dark ? .black : UIColor.systemBackground) + .ignoresSafeArea() + + ScrollView { + VStack(spacing: 16) { + // App Theme Section with Accent Colors + VStack(alignment: .leading, spacing: 16) { + Text("Theme") + .font(.title3) + .fontWeight(.semibold) + .foregroundColor(.primary) + .padding(.bottom, 4) + + // Theme selector with preview images + HStack(spacing: 12) { + // Automatic Theme + ThemeOptionButton( + title: "Automatic", + imageName: "System", + isSelected: appTheme == "system", + accentColor: selectedAccentColor, + action: { appTheme = "system" } + ) + + // Light Theme + ThemeOptionButton( + title: "Light", + imageName: "LightUI", + isSelected: appTheme == "light", + accentColor: selectedAccentColor, + action: { appTheme = "light" } + ) + + // Dark Theme + ThemeOptionButton( + title: "Dark", + imageName: "DarkUI", + isSelected: appTheme == "dark", + accentColor: selectedAccentColor, + action: { appTheme = "dark" } + ) + } + .onChange(of: appTheme) { newValue in + applyTheme(newValue) + } + + Divider() + .padding(.vertical, 4) + + // Accent Color Picker + AccentColorPicker(selectedColor: Binding( + get: { selectedAccentColor }, + set: { newColor in + selectedAccentColor = newColor + saveCustomAccentColor(newColor) + } + )) + } + .padding(.vertical, 16) + .padding(.horizontal, 16) + .background(Color(UIColor.systemGray6)) + .cornerRadius(16) + + // Username Section + VStack(alignment: .leading, spacing: 12) { + Text("Username") + .font(.title3) + .fontWeight(.semibold) + .foregroundColor(.primary) + + HStack { + TextField("Username", text: $username) + .font(.body) + .foregroundColor(.primary) + .padding(.vertical, 8) + + if !username.isEmpty { + Button(action: { username = "" }) { + Image(systemName: "xmark.circle.fill") + .foregroundColor(Color(UIColor.tertiaryLabel)) + .font(.system(size: 16)) + } + } + } + .padding(.horizontal, 12) + .background(Color(UIColor.tertiarySystemFill)) + .cornerRadius(8) + } + .padding(.vertical, 16) + .padding(.horizontal, 16) + .background(Color(UIColor.systemGray6)) + .cornerRadius(16) + + // JIT Options Section + VStack(alignment: .leading, spacing: 12) { + Text("App List") + .font(.title3) + .fontWeight(.semibold) + .foregroundColor(.primary) + + VStack(alignment: .leading, spacing: 6) { + Toggle("Load App Icons when Enabling JIT", isOn: $loadAppIconsOnJIT) + .foregroundColor(.primary) + .tint(.green) + + Text("Disabling this will hide app icons in the app list and may improve performance, while also giving it a more aesthetic look.") + .font(.footnote) + .foregroundColor(Color(UIColor.secondaryLabel)) + } + } + .padding(.vertical, 16) + .padding(.horizontal, 16) + .background(Color(UIColor.systemGray6)) + .cornerRadius(16) + } + .padding(.horizontal, 16) + .padding(.vertical, 20) + } + } + .navigationTitle("Display") + .onAppear { + loadCustomAccentColor() + applyTheme(appTheme) + } + } + + private func loadCustomAccentColor() { + if customAccentColorHex.isEmpty { + selectedAccentColor = .blue + } else { + selectedAccentColor = Color(hex: customAccentColorHex) ?? .blue + } + } + + private func saveCustomAccentColor(_ color: Color) { + // Always save the custom color if we got here (since auto theme mode is disabled) + customAccentColorHex = color.toHex() ?? "" + } + + private func applyTheme(_ theme: String) { + // Set the app's theme (will apply when app is restarted) + if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, + let window = windowScene.windows.first { + switch theme { + case "dark": + window.overrideUserInterfaceStyle = .dark + case "light": + window.overrideUserInterfaceStyle = .light + default: + window.overrideUserInterfaceStyle = .unspecified + } + } + } +} + +struct ThemeOptionButton: View { + let title: String + let imageName: String + let isSelected: Bool + let accentColor: Color + let action: () -> Void + + var body: some View { + Button(action: action) { + VStack(spacing: 8) { + Image(imageName) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(height: 160) + .cornerRadius(8) + .overlay( + RoundedRectangle(cornerRadius: 8) + .stroke(isSelected ? accentColor : Color.clear, lineWidth: 2) + ) + + Text(title) + .font(.caption) + .foregroundColor(isSelected ? accentColor : .primary) + } + } + .frame(maxWidth: .infinity) + } +} + diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index 204fbc4..376b4a9 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -64,6 +64,7 @@ struct AppButton: View { @State var appName: String @Binding var recentApps: [String] @Binding var appIcons: [String: UIImage] + @AppStorage("loadAppIconsOnJIT") private var loadAppIconsOnJIT = true var onSelectApp: (String) -> Void @Environment(\.colorScheme) private var colorScheme @@ -77,28 +78,31 @@ struct AppButton: View { } onSelectApp(bundleID) }) { - HStack(spacing: 16) { - if let image = appIcons[bundleID] { - Image(uiImage: image) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 60, height: 60) - .cornerRadius(12) - .shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2), radius: 3, x: 0, y: 1) - } else { - RoundedRectangle(cornerRadius: 12) - .fill(Color(UIColor.systemGray5)) - .frame(width: 60, height: 60) - .overlay( - Image(systemName: "app") - .font(.system(size: 26)) - .foregroundColor(.gray) - ) - .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) - .onAppear { - loadAppIcon(for: bundleID) - } + HStack(spacing: loadAppIconsOnJIT ? 16 : 12) { + if loadAppIconsOnJIT { + if let image = appIcons[bundleID] { + Image(uiImage: image) + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 60, height: 60) + .cornerRadius(12) + .shadow(color: colorScheme == .dark ? Color.black.opacity(0.2) : Color.gray.opacity(0.2), radius: 3, x: 0, y: 1) + } else { + RoundedRectangle(cornerRadius: 12) + .fill(Color(UIColor.systemGray5)) + .frame(width: 60, height: 60) + .overlay( + Image(systemName: "app") + .font(.system(size: 26)) + .foregroundColor(.gray) + ) + .shadow(color: Color.black.opacity(0.1), radius: 2, x: 0, y: 1) + .onAppear { + loadAppIcon(for: bundleID) + } + } } + VStack(alignment: .leading, spacing: 4) { Text(appName) .font(.system(size: 18, weight: .semibold)) @@ -110,15 +114,18 @@ struct AppButton: View { } Spacer() } + .padding(.vertical, loadAppIconsOnJIT ? 0 : 8) } } private func loadAppIcon(for bundleID: String) { - AppStoreIconFetcher.getIcon(for: bundleID) { image in - if let image = image { - DispatchQueue.main.async { - withAnimation(.easeIn(duration: 0.2)) { - self.appIcons[bundleID] = image + if loadAppIconsOnJIT { + AppStoreIconFetcher.getIcon(for: bundleID) { image in + if let image = image { + DispatchQueue.main.async { + withAnimation(.easeIn(duration: 0.2)) { + self.appIcons[bundleID] = image + } } } } diff --git a/StikJIT/Views/SettingsView.swift b/StikJIT/Views/SettingsView.swift index 37b3da7..65ebd52 100644 --- a/StikJIT/Views/SettingsView.swift +++ b/StikJIT/Views/SettingsView.swift @@ -23,12 +23,14 @@ struct SettingsView: View { @State private var importProgress: Float = 0.0 @State private var is_lc = false @State private var showColorPickerPopup = false + @State private var showingAppIconSheet = false @StateObject private var mountProg = MountingProgress.shared @State private var mounted = false @State private var showingConsoleLogsView = false + @State private var showingDisplayView = false private var appVersion: String { let marketingVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0" @@ -81,25 +83,6 @@ struct SettingsView: View { .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) - .overlay( - RoundedRectangle(cornerRadius: 12) - .stroke(Color.gray.opacity(0.2), lineWidth: 1) - ) - .shadow(color: Color.black.opacity(0.03), radius: 1, x: 0, y: 1) - } - .padding(.horizontal, 16) - .padding(.bottom, 8) } Divider() @@ -114,68 +97,62 @@ struct SettingsView: View { .foregroundColor(.primary) .padding(.bottom, 4) - // Theme selector with segmented control style - VStack(alignment: .leading, spacing: 10) { - Text("Accent Color") - .font(.subheadline) - .foregroundColor(.secondary) - - // Clean segmented style picker - Picker("Theme Mode", selection: Binding( - get: { customAccentColorHex.isEmpty }, - set: { newValue in - if newValue { - // System accent color selected - customAccentColorHex = "" - selectedAccentColor = .blue - } else if customAccentColorHex.isEmpty { - // Custom color selected - initialize with current theme color - selectedAccentColor = .blue - customAccentColorHex = selectedAccentColor.toHex() ?? "#0000FF" - } - } - )) { - Text("Default").tag(true) - Text("Custom").tag(false) - } - .pickerStyle(SegmentedPickerStyle()) - .padding(.vertical, 4) - - // Show selection button and color picker inline when in custom mode - if !customAccentColorHex.isEmpty { + // Display and App Icon buttons + VStack(spacing: 12) { + Button(action: { + showingDisplayView = true + }) { HStack { - // Left side - just text, no color preview - Text("Choose color") - .font(.subheadline) + Image(systemName: "paintbrush") + .font(.system(size: 18)) + .foregroundColor(accentColor) + Text("Display") .fontWeight(.medium) - + .foregroundColor(.primary) Spacer() - - // Right side - embedded color picker - ColorPicker("", selection: $selectedAccentColor, supportsOpacity: false) - .labelsHidden() - .scaleEffect(0.8) - .onChange(of: selectedAccentColor) { newColor in - saveCustomAccentColor(newColor) - } + Image(systemName: "chevron.right") + .font(.system(size: 14)) + .foregroundColor(.secondary) } - .padding(.vertical, 8) - .transition(.opacity) + .padding(.vertical, 12) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) + .contentShape(Rectangle()) } + .buttonStyle(PlainButtonStyle()) - // Help text - only show for system mode - if customAccentColorHex.isEmpty { - Text("Uses default blue accent color") - .font(.caption) - .foregroundColor(.secondary) - .padding(.top, 4) + Button(action: { + showingAppIconSheet = true + }) { + HStack { + Image(systemName: "app") + .font(.system(size: 18)) + .foregroundColor(accentColor) + Text("App Icon") + .fontWeight(.medium) + .foregroundColor(.primary) + Spacer() + Image(systemName: "chevron.right") + .font(.system(size: 14)) + .foregroundColor(.secondary) + } + .padding(.vertical, 12) + .padding(.horizontal, 16) + .frame(maxWidth: .infinity) + .background(Color(UIColor.tertiarySystemBackground)) + .cornerRadius(12) + .contentShape(Rectangle()) } + .buttonStyle(PlainButtonStyle()) } - .padding(.vertical, 6) } .padding(.vertical, 20) .padding(.horizontal, 16) - .animation(.easeInOut(duration: 0.2), value: customAccentColorHex.isEmpty) + } + .sheet(isPresented: $showingAppIconSheet) { + AppIconView() } SettingsCard { @@ -509,6 +486,9 @@ struct SettingsView: View { .sheet(isPresented: $showingConsoleLogsView) { ConsoleLogsView() } + .sheet(isPresented: $showingDisplayView) { + DisplayView() + } // Version info should now come after System Logs HStack {