diff --git a/core/Sources/BookmarksCore/Model/TagsContentViewModel.swift b/core/Sources/BookmarksCore/Model/TagsContentViewModel.swift index dec0f424..bf042b0b 100644 --- a/core/Sources/BookmarksCore/Model/TagsContentViewModel.swift +++ b/core/Sources/BookmarksCore/Model/TagsContentViewModel.swift @@ -63,22 +63,23 @@ class TagsContentViewModel: ObservableObject, Runnable { cancellables.removeAll() } - @MainActor func delete(tags scope: SelectionScope) { - - let tags: Set + @MainActor private func tags(_ scope: SelectionScope) -> Set { switch scope { case .items(let items): - tags = items + return items case .selection: // Counter-intuitively, we need to generate the intersection of our tags and the current visible filtered // tags as SwiftUI doesn't update the selection set as the items in the list change when filtering. It might // be cleaner to update the selection ourselves as the list is filtered, but doing it here we avoid thinking // too hard about race conditions and only do work when we need the selection updated. let visibleTags = Set(filteredTags.map { $0.name }) - tags = selection + return selection .filter { visibleTags.contains($0) } } + } + @MainActor func delete(tags scope: SelectionScope) { + let tags = tags(scope) let title: String if tags.count < 5 { let summary = tags @@ -101,4 +102,14 @@ class TagsContentViewModel: ObservableObject, Runnable { } } + @MainActor func open(tags scope: SelectionScope) { + let tags = tags(scope) + guard tags.count == 1, + let tag = tags.first, + let actionURL = URL(forOpeningTag: tag) else { + return + } + Application.open(actionURL) + } + } diff --git a/core/Sources/BookmarksCore/Views/ContentView.swift b/core/Sources/BookmarksCore/Views/ContentView.swift index f19f5a1b..9ba1bd8a 100644 --- a/core/Sources/BookmarksCore/Views/ContentView.swift +++ b/core/Sources/BookmarksCore/Views/ContentView.swift @@ -69,7 +69,7 @@ public struct ContentView: View { .sheet(item: $sceneState.sheet) { sheet in switch sheet { case .tags: - PhoneTagsView() + PhoneTagsView(applicationModel: applicationModel) case .settings: PhoneSettingsView(settings: applicationModel.settings) case .edit(let id): diff --git a/core/Sources/BookmarksCore/Views/PhoneTagsView.swift b/core/Sources/BookmarksCore/Views/PhoneTagsView.swift index 387a31f1..ed9d8ecc 100644 --- a/core/Sources/BookmarksCore/Views/PhoneTagsView.swift +++ b/core/Sources/BookmarksCore/Views/PhoneTagsView.swift @@ -24,15 +24,50 @@ import SwiftUI public struct PhoneTagsView: View { - @Environment(\.dismiss) var dismiss - @EnvironmentObject var applicationModel: ApplicationModel + @EnvironmentObject var settings: Settings + + @StateObject var model: TagsContentViewModel + + public init(applicationModel: ApplicationModel) { + _model = StateObject(wrappedValue: TagsContentViewModel(applicationModel: applicationModel)) + } public var body: some View { NavigationView { - TagsContentView(applicationModel: applicationModel) - .navigationBarTitle("Tags", displayMode: .inline) - .dismissable(.close) + List(selection: $model.selection) { + ForEach(model.filteredTags) { tag in + HStack { + Image(systemName: "circle.fill") + .foregroundColor(tag.name.color()) + Text(tag.name) + Spacer() + Text(tag.count.formatted()) + Toggle(isOn: $settings.favoriteTags.contains(tag.name)) + .foregroundColor(.accentColor) + .toggleStyle(.favorite) + } + } + } + .contextMenu(forSelectionType: String.ID.self) { selection in + Button(role: .destructive) { + model.delete(tags: .items(selection)) + } label: { + Label("Delete", systemImage: "trash") + } + } primaryAction: { selection in + model.open(tags: .items(selection)) + } + .onDeleteCommand { + model.delete(tags: .selection) + } + .listStyle(.plain) + .searchable(text: $model.filter) + .navigationBarTitle("Tags", displayMode: .inline) + .dismissable(.close) + .presents($model.confirmation) + .presents($model.error) + .runs(model) } } diff --git a/core/Sources/BookmarksCore/Views/TagsContentView.swift b/core/Sources/BookmarksCore/Views/TagsContentView.swift index 2d2cb615..80f80512 100644 --- a/core/Sources/BookmarksCore/Views/TagsContentView.swift +++ b/core/Sources/BookmarksCore/Views/TagsContentView.swift @@ -28,16 +28,6 @@ public struct TagsContentView: View { static let indicatorSize = 16.0 } -#if os(iOS) - @Environment(\.horizontalSizeClass) private var horizontalSizeClass - private var isCompact: Bool { horizontalSizeClass == .compact } -#else - private let isCompact = false -#endif - - @Environment(\.openURL) var openURL - - @EnvironmentObject var applicationModel: ApplicationModel @EnvironmentObject var settings: Settings @StateObject var model: TagsContentViewModel @@ -49,21 +39,10 @@ public struct TagsContentView: View { public var body: some View { Table(of: Database.Tag.self, selection: $model.selection) { TableColumn("") { tag in - if isCompact { - HStack { - Image(systemName: "circle.fill") - .foregroundColor(tag.name.color()) - Text(tag.name) - Spacer() - Text(tag.count.formatted()) - Toggle(isOn: $settings.favoriteTags.contains(tag.name)) - } - } else { - Image(systemName: "circle.fill") - .foregroundColor(tag.name.color()) - } + Image(systemName: "circle.fill") + .foregroundColor(tag.name.color()) } - .width(isCompact ? .none : LayoutMetrics.indicatorSize) + .width(LayoutMetrics.indicatorSize) TableColumn("Tag") { tag in Text(tag.name) } @@ -85,13 +64,7 @@ public struct TagsContentView: View { Label("Delete", systemImage: "trash") } } primaryAction: { selection in - guard selection.count == 1, - let tag = selection.first, - let actionURL = URL(forOpeningTag: tag) else { - return - } - print(actionURL.absoluteString) - openURL(actionURL) + model.open(tags: .items(selection)) } .onDeleteCommand { model.delete(tags: .selection)