// swiftlint:disable file_length import SwiftUI // swiftlint:disable:next type_body_length struct GenericListView: View { @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 selectedCollectionOption: CollectionPickerOption = .all @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 = { switch selectedCollectionOption { case .all: return true case .uncategorized: return item.folder == nil case .folder(let folderId): return item.folder == folderId } }() 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 || !(selectedCollectionOption == .all)) { 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: $selectedCollectionOption) { Text(CollectionPickerOption.all.name(settings: settings)) .tag(CollectionPickerOption.all) if items.contains(where: { $0.folder == nil }) { Text(CollectionPickerOption.uncategorized.name(settings: settings)) .tag(CollectionPickerOption.uncategorized) } ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in Text(folder.name).tag(CollectionPickerOption.folder(folder.id)) } let topLevelFolders = settings.folders .reduce(into: [String: [SettingsFolder]]()) { result, folder in guard let topLevelName = folder.topLevelName else { return } result[topLevelName, default: []].append(folder) } ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in Menu { ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in Button(action: { selectedCollectionOption = .folder(folder.id) }) { Text(folder.shortName) } } } label: { Text(topLevelName) } } } .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: $selectedCollectionOption) { Text(CollectionPickerOption.all.name(settings: settings)) .tag(CollectionPickerOption.all) if items.contains(where: { $0.folder == nil }) { Text(CollectionPickerOption.uncategorized.name(settings: settings)) .tag(CollectionPickerOption.uncategorized) } ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in Text(folder.name).tag(CollectionPickerOption.folder(folder.id)) } let topLevelFolders = settings.folders .reduce(into: [String: [SettingsFolder]]()) { result, folder in guard let topLevelName = folder.topLevelName else { return } result[topLevelName, default: []].append(folder) } ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in Menu { ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in Button(action: { selectedCollectionOption = .folder(folder.id) }) { Text(folder.shortName) } } } label: { Text(topLevelName) } } } } 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("Delete All", systemImage: "trash") } } } #endif } .alert( removeAllMessage, isPresented: $isShowingRemoveAllConfirmation ) { Button(removeAllButtonText) { removeAllAction() } Button("Cancel", role: .cancel) { () } } .collectionAlerts( isNewCollectionAlertPresented: $isNewCollectionAlertPresented, newCollectionName: $newCollectionName, isCollectionErrorAlertPresented: $isCollectionErrorAlertPresented ) { newCollectionName in let newFolder = SettingsFolder(name: newCollectionName) settings.folders.append(newFolder) if let id = itemPendingCollectionAssignment { settings.updateBookmarkFolder(withID: id, folder: newFolder.id) } itemPendingCollectionAssignment = nil } } 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") } 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("Remove Bookmark", systemImage: "bookmark.fill") } else { Label("Bookmark Tag\(item.tags.count == 1 ? "" : "s")", systemImage: "bookmark") } } } Menu { ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: folder.id) }) { Label(folder.name, systemImage: "folder") } .disabled(item.folder == folder.id) } let topLevelFolders = settings.folders .reduce(into: [String: [SettingsFolder]]()) { result, folder in guard let topLevelName = folder.topLevelName else { return } result[topLevelName, default: []].append(folder) } ForEach(topLevelFolders.keys.sorted(), id: \.self) { topLevelName in Menu { ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: folder.id) }) { Text(folder.shortName) } .disabled(item.folder == folder.id) } } label: { Text(topLevelName) } } 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") } } Button { removeActionUUID(item.id) } label: { Label("Delete", systemImage: "trash") } } #if os(macOS) .buttonStyle(.plain) #endif } }