diff --git a/MyPass/ViewModels/GroupFilterViewModel.swift b/MyPass/ViewModels/GroupFilterViewModel.swift new file mode 100644 index 0000000..5271c09 --- /dev/null +++ b/MyPass/ViewModels/GroupFilterViewModel.swift @@ -0,0 +1,27 @@ +import Foundation +import Combine +import MyPassCore + +@MainActor +final class GroupFilterViewModel: ObservableObject { + @Published var selectedGroupIds: Set = [] + + private let session: VaultSession + + init(session: VaultSession) { + self.session = session + } + + func isSelected(_ group: MyPassCore.Group) -> Bool { + selectedGroupIds.contains(group.id) + } + + func toggle(_ group: MyPassCore.Group) { + guard let root = session.database?.root else { return } + selectedGroupIds = GroupSelection.toggling(group.id, in: root, current: selectedGroupIds) + } + + func clear() { + selectedGroupIds = [] + } +} diff --git a/MyPass/Views/GroupFilterView.swift b/MyPass/Views/GroupFilterView.swift new file mode 100644 index 0000000..2e1cbc2 --- /dev/null +++ b/MyPass/Views/GroupFilterView.swift @@ -0,0 +1,49 @@ +import SwiftUI +import MyPassCore + +struct GroupFilterView: View { + @ObservedObject var vm: GroupFilterViewModel + let rootGroup: MyPassCore.Group + + var body: some View { + List { + if !vm.selectedGroupIds.isEmpty { + Button("Clear Filter", action: vm.clear) + } + GroupFilterNode(group: rootGroup, vm: vm) + } + .navigationTitle("Groups") + } +} + +private struct GroupFilterNode: View { + let group: MyPassCore.Group + @ObservedObject var vm: GroupFilterViewModel + + var body: some View { + if group.subgroups.isEmpty { + row + } else { + DisclosureGroup { + ForEach(group.subgroups) { sub in + GroupFilterNode(group: sub, vm: vm) + } + } label: { + row + } + } + } + + private var row: some View { + Button { + vm.toggle(group) + } label: { + HStack { + Image(systemName: vm.isSelected(group) ? "checkmark.circle.fill" : "circle") + .foregroundStyle(vm.isSelected(group) ? Color.accentColor : Color.secondary) + Label(group.name, systemImage: "folder") + } + } + .buttonStyle(.plain) + } +}