import SwiftUI struct GenericListView: View { // swiftlint:disable:this type_body_length @EnvironmentObject private var settings: SettingsManager @EnvironmentObject private var manager: BooruManager @Binding var selectedTab: Int @State private var searchText: String = "" @State private var isShowingRemoveAllConfirmation = false @Binding var isPresented: Bool @State private var isNewCollectionAlertPresented = false @State private var newCollectionName = "" @State private var itemPendingCollectionAssignment: UUID? @State private var isCollectionErrorAlertPresented = false @State private var selectedFolder: UUID? @State private var sort: SettingsBookmarkSort = .dateAdded @State private var isCollectionPickerPresented = false @State private var isProviderPickerPresented = false @State private var selectedProvider: BooruProvider? let allowBookmarking: Bool let title: String let emptyMessage: String let emptyIcon: String let emptyDescription: String let removeAllMessage: String let removeAllButtonText: String let items: [T] let removeAction: (IndexSet) -> Void let removeActionUUID: (UUID) -> Void let removeAllAction: () -> Void var filteredItems: [T] { items.filter { item in let matchesFolder: Bool = { if selectedFolder == nil { return true } if selectedFolder == UUID.nilUUID() { return item.folder == nil } return item.folder == selectedFolder }() let matchesSearch = searchText.isEmpty || item.tags .joined(separator: " ") .lowercased() .contains(searchText.lowercased()) let matchesProvider = selectedProvider == nil || item.provider == selectedProvider return matchesFolder && matchesSearch && matchesProvider } } var sortedFilteredItems: [T] { if allowBookmarking { return filteredItems.sorted { $0.date > $1.date } } return filteredItems.sorted { (lhs: T, rhs: T) in switch sort { case .dateAdded: return lhs.date > rhs.date case .lastVisited: return lhs.lastVisit > rhs.lastVisit case .visitCount: return lhs.visitedCount > rhs.visitedCount } } } @ViewBuilder private var listContent: some View { List { if filteredItems.isEmpty && (!searchText.isEmpty || !(selectedFolder == nil)) { Text("No matching items found") .foregroundColor(.secondary) } ForEach(sortedFilteredItems, id: \.id) { item in itemButtonContent(item: item) } .onDelete(perform: removeAction) } } private func isProviderBookmarked(_ provider: BooruProvider) -> Bool { settings.bookmarks.contains { $0.provider == provider } } private func isCollectionPopulated(_ folder: UUID?) -> Bool { settings.bookmarks.contains { $0.folder == folder } } var body: some View { NavigationStack { VStack(spacing: 0) { if items.isEmpty { ContentUnavailableView( emptyMessage, systemImage: emptyIcon, description: Text(emptyDescription) ) } else { #if os(macOS) if !settings.folders.isEmpty && !allowBookmarking { Picker("Sort", selection: $sort) { ForEach(SettingsBookmarkSort.allCases, id: \.rawValue) { sortMode in Text(sortMode.rawValue).tag(sortMode) } } .padding() .pickerStyle(.menu) Picker("Collection", selection: $selectedFolder) { Text("All").tag(nil as UUID?) if items.contains(where: { $0.folder == nil }) { Text("Uncategorised").tag(UUID.nilUUID() as UUID?) } ForEach(settings.folders, id: \.id) { folder in if isCollectionPopulated(folder.id) { Text(folder.name).tag(folder.id) } } } .padding(.bottom) .padding(.horizontal) .pickerStyle(.menu) Picker("Provider", selection: $selectedProvider) { Text("All").tag(nil as BooruProvider?) ForEach(BooruProvider.allCases, id: \.rawValue) { provider in if isProviderBookmarked(provider) { Text(provider.rawValue).tag(provider) } } } .padding(.bottom) .padding(.horizontal) .pickerStyle(.menu) } #endif if allowBookmarking { listContent #if os(iOS) .searchable(text: $searchText, placement: .navigationBarDrawer) #endif } else { listContent.searchable(text: $searchText) } } } } .navigationTitle(title) .refreshable { settings.syncFromCloud() } .toolbar { #if os(macOS) ToolbarItem { Button(action: { settings.syncFromCloud() }) { Label("Refresh", systemImage: "arrow.clockwise") } } #endif #if os(macOS) ToolbarItem { Button( role: .destructive, action: { isShowingRemoveAllConfirmation = true } ) { Label("Remove All", systemImage: "trash") } } #endif #if os(iOS) ToolbarItemGroup(placement: .secondaryAction) { if !allowBookmarking { Menu { Picker("Sort By", selection: $sort) { ForEach(SettingsBookmarkSort.allCases, id: \.rawValue) { sortMode in Text(sortMode.rawValue).tag(sortMode) } } } label: { Label("Sort By", systemImage: "arrow.up.arrow.down") } Menu { Picker("Collection", selection: $selectedFolder) { Text("All").tag(nil as UUID?) if items.contains(where: { $0.folder == nil }) { Text("Uncategorised").tag(UUID.nilUUID()) } ForEach(settings.folders, id: \.id) { folder in Text(folder.name) .tag(folder.id) .selectionDisabled(!isCollectionPopulated(folder.id)) } } } label: { Label("Collection", systemImage: "folder") } Menu { Picker("Provider", selection: $selectedProvider) { Text("All").tag(nil as BooruProvider?) ForEach(BooruProvider.allCases, id: \.rawValue) { provider in Text(provider.rawValue) .tag(provider) .selectionDisabled(!isProviderBookmarked(provider)) } } } label: { Label("Provider", systemImage: "globe") } Button( role: .destructive, action: { isShowingRemoveAllConfirmation = true } ) { Label("Remove All", systemImage: "trash") } } } #endif } .alert( removeAllMessage, isPresented: $isShowingRemoveAllConfirmation ) { Button(removeAllButtonText) { removeAllAction() } Button("Cancel", role: .cancel) { () } } .alert( "New Collection", isPresented: $isNewCollectionAlertPresented ) { TextField("Collection Name", text: $newCollectionName) Button("Cancel") { newCollectionName = "" isNewCollectionAlertPresented = false } Button("Create") { if newCollectionName.isEmpty { isCollectionErrorAlertPresented = true } else { let newFolder = SettingsFolder(name: newCollectionName) settings.folders.append(newFolder) if let id = itemPendingCollectionAssignment { settings.updateBookmarkFolder(withID: id, folder: newFolder.id) } itemPendingCollectionAssignment = nil newCollectionName = "" isNewCollectionAlertPresented = false } } } .alert( "Error", isPresented: $isCollectionErrorAlertPresented, ) { Button("OK", role: .cancel) { () } } message: { Text("Collection name cannot be empty.") } } func itemButtonContent(item: T) -> some View { Button(action: { let previousProvider = settings.preferredBooru settings.preferredBooru = item.provider manager.searchText = item.tags.joined(separator: " ") selectedTab = 0 isPresented.toggle() if previousProvider == settings.preferredBooru { let localManager = manager let localSettings = settings Task(priority: .userInitiated) { await localManager.performSearch(settings: localSettings) } } if !allowBookmarking { settings.incrementBookmarkVisitCount(withID: item.id) settings.updateBookmarkLastVisit(withID: item.id) } }) { GenericItemView( item: item, removeAction: removeActionUUID ) } .contextMenu { Button(action: { manager.searchText += " \(item.tags.joined(separator: " "))" manager.selectedPost = nil let localManager = manager let localSettings = settings isPresented.toggle() Task(priority: .userInitiated) { await localManager.performSearch(settings: localSettings) } }) { Label("Add to Search", systemImage: "plus") } Menu { ForEach(settings.folders, id: \.id) { folder in if item.folder != folder.id { Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: folder.id) }) { Label(folder.name, systemImage: "folder") } } } Button(action: { itemPendingCollectionAssignment = item.id isNewCollectionAlertPresented = true }) { Label("New Collection", systemImage: "plus") } } label: { Label("\(item.folder != nil ? "Move" : "Add") to Collection", systemImage: "folder") } if item.folder != nil { Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: nil) }) { Label("Remove from Collection", systemImage: "folder.badge.minus") } } if allowBookmarking { let isBookmarked = settings.bookmarks.contains { $0.tags == item.tags } Button(action: { if isBookmarked { settings.removeBookmark(withTags: item.tags) } else { settings.addBookmark(provider: settings.preferredBooru, tags: item.tags) } }) { if isBookmarked { Label("Unbookmark Tag\(item.tags.count == 1 ? "" : "s")", systemImage: "bookmark.fill") } else { Label("Bookmark Tag\(item.tags.count == 1 ? "" : "s")", systemImage: "bookmark") } } } Button { removeActionUUID(item.id) } label: { Label("Remove", systemImage: "trash") } } #if os(macOS) .buttonStyle(.plain) #endif } }