diff options
Diffstat (limited to 'Sora/Views/Generic')
| -rw-r--r-- | Sora/Views/Generic/GenericListView.swift | 174 |
1 files changed, 96 insertions, 78 deletions
diff --git a/Sora/Views/Generic/GenericListView.swift b/Sora/Views/Generic/GenericListView.swift index 6c7ba3e..3195624 100644 --- a/Sora/Views/Generic/GenericListView.swift +++ b/Sora/Views/Generic/GenericListView.swift @@ -30,68 +30,59 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { let removeAction: (IndexSet) -> Void let removeActionUUID: (UUID) -> Void let removeAllAction: () -> Void + @State private var folderHierarchy = FolderHierarchy(folders: []) + @State private var displayedItems: [T] = [] - var filteredItems: [T] { - items.filter { item in - let matchesFolder: Bool = { - switch selectedCollectionOption { - case .all: - return true + private func refreshFolderHierarchy() { + folderHierarchy = FolderHierarchy(folders: settings.folders) + } - case .uncategorized: - return item.folder == nil + private func matchesSelectedCollection(for item: T) -> Bool { + switch selectedCollectionOption { + case .all: + return true - case .folder(let folderId): - return item.folder == folderId + case .uncategorized: + return item.folder == nil - case .topLevelFolder(let topLevelName): - guard let itemFolderId = item.folder, - let itemFolder = settings.folders.first(where: { $0.id == itemFolderId }) - else { - return false - } + case .folder(let folderIdentifier): + return item.folder == folderIdentifier - return itemFolder.topLevelName == topLevelName + case .topLevelFolder(let topLevelName): + guard let itemFolderIdentifier = item.folder, + let itemFolder = folderHierarchy.folder(for: itemFolderIdentifier) + else { + return false + } - case .topLevelUncategorized(let topLevelName): - guard let itemFolderId = item.folder, - let itemFolder = settings.folders.first(where: { $0.id == itemFolderId }) - else { - return false - } + return itemFolder.topLevelName == topLevelName - return itemFolder.name == topLevelName - } - }() - let matchesSearch = - searchText.isEmpty - || item.tags - .joined(separator: " ") - .lowercased() - .contains(searchText.lowercased()) - let matchesProvider = - selectedProvider == nil - || item.provider == selectedProvider + case .topLevelUncategorized(let topLevelName): + guard let itemFolderIdentifier = item.folder, + let itemFolder = folderHierarchy.folder(for: itemFolderIdentifier) + else { + return false + } - return matchesFolder && matchesSearch && matchesProvider + return itemFolder.name == topLevelName } } - var sortedFilteredItems: [T] { + private func sortedItems(from unsortedItems: [T]) -> [T] { if allowBookmarking { - return filteredItems.sorted { $0.date > $1.date } + return unsortedItems.sorted { $0.date > $1.date } } - return filteredItems.sorted { (lhs: T, rhs: T) in + return unsortedItems.sorted { (leftItem: T, rightItem: T) in switch sort { case .dateAdded: - return lhs.date > rhs.date + return leftItem.date > rightItem.date case .lastVisited: - return lhs.lastVisit > rhs.lastVisit + return leftItem.lastVisit > rightItem.lastVisit case .visitCount: - return lhs.visitedCount > rhs.visitedCount + return leftItem.visitedCount > rightItem.visitedCount case .shuffle: return Bool.random() @@ -99,19 +90,38 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { } } + private func refreshDisplayedItems() { + let normalizedSearchText = searchText.lowercased() + let filteredItems = items.filter { item in + let matchesSearch = + normalizedSearchText.isEmpty + || item.tags + .joined(separator: " ") + .lowercased() + .contains(normalizedSearchText) + let matchesProvider = + selectedProvider == nil + || item.provider == selectedProvider + + return matchesSelectedCollection(for: item) && matchesSearch && matchesProvider + } + + displayedItems = sortedItems(from: filteredItems) + } + @ViewBuilder private var listContent: some View { List { - if filteredItems.isEmpty, !searchText.isEmpty || selectedCollectionOption != .all { + if displayedItems.isEmpty, !searchText.isEmpty || selectedCollectionOption != .all { Text("No matching items found") .foregroundColor(.secondary) } - ForEach(sortedFilteredItems, id: \.id) { item in + ForEach(displayedItems, id: \.id) { item in itemButtonContent(item: item) } .onDelete { offsets in let itemsToRemove = offsets.compactMap { index in - index < sortedFilteredItems.count ? sortedFilteredItems[index] : nil + index < displayedItems.count ? displayedItems[index] : nil } let originalIndices = IndexSet( itemsToRemove.compactMap { itemToRemove in @@ -161,18 +171,11 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { .tag(CollectionPickerOption.uncategorized) } - ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in + ForEach(folderHierarchy.rootFolders, 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 + ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in Menu { Button(action: { selectedCollectionOption = .topLevelFolder(topLevelName) @@ -180,7 +183,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { Text("All \(topLevelName)") } - if settings.folders.contains(where: { $0.name == topLevelName }) { + if folderHierarchy.hasTopLevelUncategorized(forTopLevelName: topLevelName) { Button(action: { selectedCollectionOption = .topLevelUncategorized(topLevelName) }) { @@ -190,7 +193,10 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { Divider() - ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in + ForEach( + folderHierarchy.folders(forTopLevelName: topLevelName), + id: \.id + ) { folder in Button(action: { selectedCollectionOption = .folder(folder.id) }) { @@ -286,7 +292,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { } } - ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in + ForEach(folderHierarchy.rootFolders, id: \.id) { folder in Button(action: { selectedCollectionOption = .folder(folder.id) }) { @@ -294,14 +300,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { } } - 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 + ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in Menu { Button(action: { selectedCollectionOption = .topLevelFolder(topLevelName) @@ -309,7 +308,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { Label("All \(topLevelName)", systemImage: "folder.fill") } - if settings.folders.contains(where: { $0.name == topLevelName }) { + if folderHierarchy.hasTopLevelUncategorized(forTopLevelName: topLevelName) { Button(action: { selectedCollectionOption = .topLevelUncategorized(topLevelName) }) { @@ -319,7 +318,10 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { Divider() - ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in + ForEach( + folderHierarchy.folders(forTopLevelName: topLevelName), + id: \.id + ) { folder in Button(action: { selectedCollectionOption = .folder(folder.id) }) { @@ -360,6 +362,29 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { } #endif } + .onAppear { + refreshFolderHierarchy() + refreshDisplayedItems() + } + .onChange(of: items) { + refreshDisplayedItems() + } + .onChange(of: searchText) { + refreshDisplayedItems() + } + .onChange(of: selectedCollectionOption) { + refreshDisplayedItems() + } + .onChange(of: selectedProvider) { + refreshDisplayedItems() + } + .onChange(of: sort) { + refreshDisplayedItems() + } + .onChange(of: settings.folders) { + refreshFolderHierarchy() + refreshDisplayedItems() + } .alert( removeAllMessage, isPresented: $isShowingRemoveAllConfirmation @@ -452,7 +477,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { } Menu { - ForEach(settings.folders.filter { $0.topLevelName == nil }, id: \.id) { folder in + ForEach(folderHierarchy.rootFolders, id: \.id) { folder in Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: folder.id) }) { @@ -461,16 +486,9 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { .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 + ForEach(folderHierarchy.sortedTopLevelNames, id: \.self) { topLevelName in Menu { - let topLevelFolder = settings.folders.first { $0.name == topLevelName } + let topLevelFolder = folderHierarchy.rootFolders.first { $0.name == topLevelName } if let topLevelFolder { Button(action: { @@ -492,7 +510,7 @@ struct GenericListView<T: Identifiable & Hashable & GenericItem>: View { Divider() - ForEach(topLevelFolders[topLevelName] ?? [], id: \.id) { folder in + ForEach(folderHierarchy.folders(forTopLevelName: topLevelName), id: \.id) { folder in Button(action: { settings.updateBookmarkFolder(withID: item.id, folder: folder.id) }) { |