2026-05-21 22:04:13 +02:00
|
|
|
//
|
|
|
|
|
// ContentView.swift
|
2026-09-20 15:05:19 +02:00
|
|
|
// KeeVault
|
2026-05-21 22:04:13 +02:00
|
|
|
//
|
|
|
|
|
// Created by Christophe Vila on 21/05/2026.
|
|
|
|
|
//
|
|
|
|
|
|
|
|
|
|
import SwiftUI
|
2026-09-20 15:05:19 +02:00
|
|
|
import KeeVaultCore
|
2026-09-19 16:02:23 +02:00
|
|
|
#if os(macOS)
|
|
|
|
|
import AppKit
|
|
|
|
|
#endif
|
2026-05-21 22:04:13 +02:00
|
|
|
|
|
|
|
|
struct ContentView: View {
|
2026-09-19 16:02:23 +02:00
|
|
|
@StateObject private var session = VaultSession()
|
2026-05-21 22:04:13 +02:00
|
|
|
|
|
|
|
|
var body: some View {
|
2026-09-19 16:02:23 +02:00
|
|
|
SwiftUI.Group {
|
|
|
|
|
if session.isLocked {
|
|
|
|
|
UnlockView(vm: UnlockViewModel(session: session))
|
|
|
|
|
} else {
|
2026-09-19 17:13:34 +02:00
|
|
|
VaultRootView(session: session)
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-20 15:05:19 +02:00
|
|
|
.background(Color("VaultBackground"))
|
2026-09-19 16:02:23 +02:00
|
|
|
.onReceive(
|
|
|
|
|
NotificationCenter.default.publisher(for: sceneBackgroundNotification)
|
|
|
|
|
) { _ in
|
|
|
|
|
session.lock()
|
|
|
|
|
}
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|
|
|
|
|
|
2026-09-19 16:02:23 +02:00
|
|
|
private var sceneBackgroundNotification: Notification.Name {
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
UIScene.didEnterBackgroundNotification
|
|
|
|
|
#else
|
|
|
|
|
NSApplication.didResignActiveNotification
|
|
|
|
|
#endif
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-19 17:13:34 +02:00
|
|
|
/// Owns the vault/group-filter view models for as long as the vault stays unlocked -- their
|
|
|
|
|
/// state (search query, group filter selection) persists across re-renders but resets if the
|
|
|
|
|
/// vault locks and is unlocked again (a fresh VaultRootView is created), which matches the
|
|
|
|
|
/// expected "locking clears your filter" behavior.
|
|
|
|
|
private struct VaultRootView: View {
|
|
|
|
|
@StateObject private var vaultVM: VaultViewModel
|
|
|
|
|
@StateObject private var filterVM: GroupFilterViewModel
|
2026-09-19 17:31:46 +02:00
|
|
|
#if os(macOS)
|
2026-09-19 17:13:34 +02:00
|
|
|
@State private var columnVisibility: NavigationSplitViewVisibility = .all
|
2026-09-19 17:31:46 +02:00
|
|
|
#else
|
|
|
|
|
@State private var showFilterSheet = false
|
|
|
|
|
#endif
|
2026-09-19 16:02:23 +02:00
|
|
|
|
2026-09-19 17:13:34 +02:00
|
|
|
init(session: VaultSession) {
|
|
|
|
|
_vaultVM = StateObject(wrappedValue: VaultViewModel(session: session))
|
|
|
|
|
_filterVM = StateObject(wrappedValue: GroupFilterViewModel(session: session))
|
|
|
|
|
}
|
2026-05-21 22:04:13 +02:00
|
|
|
|
|
|
|
|
var body: some View {
|
2026-09-19 17:31:46 +02:00
|
|
|
content
|
|
|
|
|
.onChange(of: filterVM.selectedGroupIds) { _, newValue in
|
|
|
|
|
vaultVM.selectedGroupIds = newValue
|
|
|
|
|
}
|
|
|
|
|
.onChange(of: filterVM.lastToggledGroupId) { _, newValue in
|
|
|
|
|
vaultVM.lastToggledGroupId = newValue
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if os(macOS)
|
|
|
|
|
// macOS has room for a persistent sidebar; NavigationSplitView's columnVisibility
|
|
|
|
|
// toggle works correctly at regular width.
|
|
|
|
|
private var content: some View {
|
2026-09-19 17:13:34 +02:00
|
|
|
NavigationSplitView(columnVisibility: $columnVisibility) {
|
|
|
|
|
if let root = vaultVM.session.database?.root {
|
|
|
|
|
GroupFilterView(vm: filterVM, rootGroup: root)
|
2026-09-19 16:02:23 +02:00
|
|
|
}
|
2026-05-21 22:04:13 +02:00
|
|
|
} detail: {
|
2026-09-19 17:13:34 +02:00
|
|
|
NavigationStack {
|
|
|
|
|
EntryListView(vm: vaultVM)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .navigation) {
|
|
|
|
|
Button {
|
|
|
|
|
columnVisibility = columnVisibility == .all ? .detailOnly : .all
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "line.3.horizontal")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 16:02:23 +02:00
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 17:31:46 +02:00
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
// NavigationSplitView's compact-width collapse only pushes to detail when the
|
|
|
|
|
// sidebar drives navigation via row selection. Our sidebar is a multi-select
|
|
|
|
|
// filter, not a picker, so on iPhone it would just show the sidebar forever with
|
|
|
|
|
// no way to reach the entry list. Use a plain stack with the entry list as the
|
|
|
|
|
// root, and present the group filter as a sheet instead.
|
|
|
|
|
private var content: some View {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
EntryListView(vm: vaultVM)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .navigation) {
|
|
|
|
|
Button {
|
|
|
|
|
showFilterSheet = true
|
|
|
|
|
} label: {
|
|
|
|
|
Image(systemName: "line.3.horizontal")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|
2026-09-19 17:31:46 +02:00
|
|
|
.sheet(isPresented: $showFilterSheet) {
|
|
|
|
|
NavigationStack {
|
|
|
|
|
if let root = vaultVM.session.database?.root {
|
|
|
|
|
GroupFilterView(vm: filterVM, rootGroup: root)
|
|
|
|
|
.toolbar {
|
|
|
|
|
ToolbarItem(placement: .confirmationAction) {
|
|
|
|
|
Button("Done") { showFilterSheet = false }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-19 17:25:06 +02:00
|
|
|
}
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|
2026-09-19 17:31:46 +02:00
|
|
|
#endif
|
2026-05-21 22:04:13 +02:00
|
|
|
}
|