This commit is contained in:
Max Goedjen 2025-09-03 00:16:01 -07:00
parent 9ca859fa3f
commit 34aad09126
No known key found for this signature in database
4 changed files with 376 additions and 0 deletions

View File

@ -0,0 +1,59 @@
import SwiftUI
struct ConfigurationItemView<Content: View>: View {
enum Action: Hashable {
case copy(String)
case revealInFinder(String)
}
let title: LocalizedStringResource
let content: Content
let action: Action?
init(title: LocalizedStringResource, value: String, action: Action? = nil) where Content == Text {
self.title = title
self.content = Text(value)
.font(.subheadline)
.foregroundStyle(.secondary)
self.action = action
}
init(title: LocalizedStringResource, action: Action? = nil, content: () -> Content) {
self.title = title
self.content = content()
self.action = action
}
var body: some View {
VStack(alignment: .leading) {
HStack {
Text(title)
Spacer()
switch action {
case .copy(let string):
Button(.copyableClickToCopyButton, systemImage: "document.on.document") {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(string, forType: .string)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case .revealInFinder(let rawPath):
Button(.revealInFinderButton, systemImage: "folder") {
// All foundation-based normalization methods replace this with the container directly.
let processedPath = rawPath.replacingOccurrences(of: "~", with: "/Users/\(NSUserName())")
let url = URL(filePath: processedPath)
let folder = url.deletingLastPathComponent().path()
NSWorkspace.shared.selectFile(processedPath, inFileViewerRootedAtPath: folder)
}
.labelStyle(.iconOnly)
.buttonStyle(.borderless)
case nil:
EmptyView()
}
}
content
}
}
}

View File

@ -0,0 +1,71 @@
import SwiftUI
import SecretKit
struct EmptyStoreView: View {
@State var store: AnySecretStore?
var body: some View {
if store is AnySecretStoreModifiable {
EmptyStoreModifiableView()
} else {
EmptyStoreImmutableView()
}
}
}
struct EmptyStoreImmutableView: View {
var body: some View {
VStack {
Text(.emptyStoreNonmodifiableTitle).bold()
Text(.emptyStoreNonmodifiableDescription)
Text(.emptyStoreNonmodifiableSupportedKeyTypes)
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
struct EmptyStoreModifiableView: View {
var body: some View {
GeometryReader { windowGeometry in
VStack {
GeometryReader { g in
Path { path in
path.move(to: CGPoint(x: g.size.width / 2, y: g.size.height))
path.addCurve(to:
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)), control1:
CGPoint(x: g.size.width / 2, y: g.size.height * (1/2)), control2:
CGPoint(x: g.size.width * (3/4), y: g.size.height * (1/2)))
path.addCurve(to:
CGPoint(x: g.size.width - 13, y: 0), control1:
CGPoint(x: g.size.width - 13 , y: g.size.height * (1/2)), control2:
CGPoint(x: g.size.width - 13, y: 0))
}.stroke(style: StrokeStyle(lineWidth: 5, lineCap: .round))
Path { path in
path.move(to: CGPoint(x: g.size.width - 23, y: 0))
path.addLine(to: CGPoint(x: g.size.width - 13, y: -10))
path.addLine(to: CGPoint(x: g.size.width - 3, y: 0))
}.fill()
}.frame(height: (windowGeometry.size.height/2) - 20).padding()
Text(.emptyStoreModifiableClickHereTitle).bold()
Text(.emptyStoreModifiableClickHereDescription)
Spacer()
}.frame(maxWidth: .infinity, maxHeight: .infinity)
}
}
}
#if DEBUG
struct EmptyStoreModifiableView_Previews: PreviewProvider {
static var previews: some View {
Group {
EmptyStoreImmutableView()
EmptyStoreModifiableView()
}
}
}
#endif

View File

@ -0,0 +1,24 @@
import SwiftUI
struct NoStoresView: View {
var body: some View {
VStack {
Text(.noSecureStorageTitle)
.bold()
Text(.noSecureStorageDescription)
Link(.noSecureStorageYubicoLink, destination: URL(string: "https://www.yubico.com/products/compare-yubikey-5-series/")!)
}.padding()
}
}
#if DEBUG
struct NoStoresView_Previews: PreviewProvider {
static var previews: some View {
NoStoresView()
}
}
#endif

View File

@ -0,0 +1,222 @@
import SwiftUI
import SecretKit
import SecureEnclaveSecretKit
import SmartCardSecretKit
import Brief
struct ContentView: View {
@Binding var showingCreation: Bool
@Binding var runningSetup: Bool
@Binding var hasRunSetup: Bool
@State var showingAgentInfo = false
@State var activeSecret: AnySecret?
@Environment(\.colorScheme) var colorScheme
@Environment(\.secretStoreList) private var storeList
@Environment(\.updater) private var updater: any UpdaterProtocol
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State private var selectedUpdate: Release?
@State private var showingAppPathNotice = false
var body: some View {
VStack {
if storeList.anyAvailable {
StoreListView(activeSecret: $activeSecret)
} else {
NoStoresView()
}
}
.frame(minWidth: 640, minHeight: 320)
.toolbar {
toolbarItem(updateNoticeView, id: "update")
toolbarItem(runningOrRunSetupView, id: "setup")
toolbarItem(appPathNoticeView, id: "appPath")
toolbarItem(newItemView, id: "new")
}
.sheet(isPresented: $runningSetup) {
SetupView(setupComplete: $hasRunSetup)
}
}
}
extension ContentView {
@ToolbarContentBuilder
func toolbarItem(_ view: some View, id: String) -> some ToolbarContent {
if #available(macOS 26.0, *) {
ToolbarItem(id: id) { view }
.sharedBackgroundVisibility(.hidden)
} else {
ToolbarItem(id: id) { view }
}
}
var needsSetup: Bool {
runningSetup || !hasRunSetup
}
/// Item either showing a "everything's good, here's more info" or "something's wrong, re-run setup" message
/// These two are mutually exclusive
@ViewBuilder
var runningOrRunSetupView: some View {
if needsSetup {
setupNoticeView
} else {
agentStatusToolbarView
}
}
var updateNoticeContent: (LocalizedStringResource, Color)? {
guard let update = updater.update else { return nil }
if update.critical {
return (.updateCriticalNoticeTitle, .red)
} else {
if updater.testBuild {
return (.updateTestNoticeTitle, .blue)
} else {
return (.updateNormalNoticeTitle, .orange)
}
}
}
@ViewBuilder
var updateNoticeView: some View {
if let update = updater.update, let (text, color) = updateNoticeContent {
Button(action: {
selectedUpdate = update
}, label: {
Text(text)
.font(.headline)
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: color))
.sheet(item: $selectedUpdate) { update in
UpdateDetailView(update: update)
}
}
}
@ViewBuilder
var newItemView: some View {
if storeList.modifiableStore?.isAvailable ?? false {
Button(.appMenuNewSecretButton, systemImage: "plus") {
showingCreation = true
}
.menuButton()
.sheet(isPresented: $showingCreation) {
if let modifiable = storeList.modifiableStore {
CreateSecretView(store: modifiable) { created in
if let created {
activeSecret = created
}
}
}
}
}
}
@ViewBuilder
var setupNoticeView: some View {
Button(action: {
runningSetup = true
}, label: {
if !hasRunSetup {
Text(.agentSetupNoticeTitle)
.font(.headline)
}
})
.buttonStyle(ToolbarButtonStyle(color: .orange))
}
@ViewBuilder
var agentStatusToolbarView: some View {
Button(action: {
showingAgentInfo = true
}, label: {
HStack {
if agentStatusChecker.running {
Text(.agentRunningNoticeTitle)
.font(.headline)
.foregroundColor(colorScheme == .light ? Color(white: 0.3) : .white)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.green)
} else {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
Circle()
.frame(width: 10, height: 10)
.foregroundColor(Color.red)
}
}
})
.buttonStyle(
ToolbarButtonStyle(
lightColor: agentStatusChecker.running ? .black.opacity(0.05) : .red.opacity(0.75),
darkColor: agentStatusChecker.running ? .white.opacity(0.05) : .red.opacity(0.5),
)
)
.popover(isPresented: $showingAgentInfo, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
AgentStatusView()
}
}
@ViewBuilder
var appPathNoticeView: some View {
if !ApplicationDirectoryController().isInApplicationsDirectory {
Button(action: {
showingAppPathNotice = true
}, label: {
Group {
Text(.appNotInApplicationsNoticeTitle)
}
.font(.headline)
.foregroundColor(.white)
})
.buttonStyle(ToolbarButtonStyle(color: .orange))
.popover(isPresented: $showingAppPathNotice, attachmentAnchor: attachmentAnchor, arrowEdge: .bottom) {
VStack {
Image(systemName: "exclamationmark.triangle")
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 64)
Text(.appNotInApplicationsNoticeDetailDescription)
.frame(maxWidth: 300)
}
.padding()
}
}
}
var attachmentAnchor: PopoverAttachmentAnchor {
.rect(.bounds)
}
}
#if DEBUG
struct ContentView_Previews: PreviewProvider {
static var previews: some View {
Group {
// Empty on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store(numberOfRandomSecrets: 0)], modifiableStores: [Preview.StoreModifiable(numberOfRandomSecrets: 0)]))
.environment(PreviewUpdater())
// 5 items on modifiable and nonmodifiable
ContentView(showingCreation: .constant(false), runningSetup: .constant(false), hasRunSetup: .constant(true))
.environment(Preview.storeList(stores: [Preview.Store()], modifiableStores: [Preview.StoreModifiable()]))
.environment(PreviewUpdater())
}
}
}
#endif