summaryrefslogtreecommitdiff
path: root/Sora/Views/Generic/GenericListView.swift
diff options
context:
space:
mode:
Diffstat (limited to 'Sora/Views/Generic/GenericListView.swift')
-rw-r--r--Sora/Views/Generic/GenericListView.swift174
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)
}) {