diff --git a/StikJIT/Views/InstalledAppsListView.swift b/StikJIT/Views/InstalledAppsListView.swift index b4b399b..07296d8 100644 --- a/StikJIT/Views/InstalledAppsListView.swift +++ b/StikJIT/Views/InstalledAppsListView.swift @@ -11,84 +11,113 @@ struct InstalledAppsListView: View { @StateObject private var viewModel = InstalledAppsViewModel() @State private var appIcons: [String: UIImage] = [:] @Environment(\.dismiss) private var dismiss - @Environment(\.colorScheme) private var colorScheme + @AppStorage("recentApps") var recentApps: [String] = [] var onSelectApp: (String) -> Void - + var body: some View { NavigationView { - ScrollView { - LazyVStack(spacing: 0) { - ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in - Button(action: { - withAnimation(.spring(response: 0.3, dampingFraction: 0.7)) { - onSelectApp(bundleID) - } - }) { - HStack(spacing: 16) { - // App Icon - 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) + List { + if !recentApps.isEmpty { + Section { + ForEach(recentApps, id: \.self) { bundleID in + AppButton(bundleID: bundleID, appName: viewModel.apps[bundleID] ?? "", recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp) + .swipeActions(edge: .trailing) { + Button(role: .destructive) { + withAnimation { + recentApps.removeAll(where: { $0 == bundleID }) } + } label: { + Label("Delete", systemImage: "trash") + } } - - // App Name and Bundle ID - VStack(alignment: .leading, spacing: 4) { - Text(appName) - .font(.system(size: 18, weight: .semibold)) - .foregroundColor(Color.blue) - - Text(bundleID) - .font(.system(size: 15)) - .foregroundColor(Color.gray) - .lineLimit(1) - } - - Spacer() - } - .padding(.vertical, 12) - .padding(.horizontal, 20) - .contentShape(Rectangle()) - } - .buttonStyle(PlainButtonStyle()) - - if appName != viewModel.apps.sorted(by: { $0.key < $1.key }).last?.key { - Divider() - .padding(.leading, 96) - .padding(.trailing, 20) - .opacity(0.4) } + } header: { + Text("Recents") + } + } + + Section { + ForEach(viewModel.apps.sorted(by: { $0.key < $1.key }), id: \.key) { bundleID, appName in + AppButton(bundleID: bundleID, appName: appName, recentApps: $recentApps, appIcons: $appIcons, onSelectApp: onSelectApp) + } + } header: { + if !recentApps.isEmpty { + Text("All applications") + } else { + EmptyView() } } - .background(Color(UIColor.systemBackground)) } - .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + .listStyle(.plain) .navigationTitle("Installed Apps") - .navigationBarItems(leading: Button("Done") { - dismiss() + .toolbar(content: { + Button("Done") { + dismiss() + } + }) + } + .background(Color(UIColor.systemGroupedBackground).edgesIgnoringSafeArea(.all)) + } +} + +struct AppButton: View { + @State var bundleID: String + @State var appName: String + @Binding var recentApps: [String] + @Binding var appIcons: [String: UIImage] + var onSelectApp: (String) -> Void + + @Environment(\.colorScheme) private var colorScheme + + var body: some View { + Button(action: { + recentApps.removeAll(where: { $0 == bundleID }) + recentApps = [bundleID] + recentApps + + onSelectApp(bundleID) + }) { + HStack(spacing: 16) { + // App Icon + 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) + } + } + + // App Name and Bundle ID + VStack(alignment: .leading, spacing: 4) { + Text(appName) + .font(.system(size: 18, weight: .semibold)) + .foregroundColor(Color(.label)) + + Text(bundleID) + .font(.system(size: 15)) + .foregroundColor(Color.gray) + .lineLimit(1) + } + + Spacer() } - .font(.system(size: 17, weight: .regular)) - .foregroundColor(.blue)) } } - + // Helper method to load app icon private func loadAppIcon(for bundleID: String) { AppStoreIconFetcher.getIcon(for: bundleID) { image in @@ -103,6 +132,26 @@ struct InstalledAppsListView: View { } } +extension Array: @retroactive RawRepresentable where Element: Codable { + public init?(rawValue: String) { + guard let data = rawValue.data(using: .utf8), + let result = try? JSONDecoder().decode([Element].self, from: data) + else { + return nil + } + self = result + } + + public var rawValue: String { + guard let data = try? JSONEncoder().encode(self), + let result = String(data: data, encoding: .utf8) + else { + return "[]" + } + return result + } +} + #Preview { InstalledAppsListView { _ in } }