mirror of
https://github.com/maxgoedjen/secretive.git
synced 2025-09-20 11:20:57 +00:00
.
This commit is contained in:
parent
9ca859fa3f
commit
34aad09126
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
71
Sources/Secretive/Views/Secrets/EmptyStoreView.swift
Normal file
71
Sources/Secretive/Views/Secrets/EmptyStoreView.swift
Normal 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
|
24
Sources/Secretive/Views/Secrets/NoStoresView.swift
Normal file
24
Sources/Secretive/Views/Secrets/NoStoresView.swift
Normal 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
|
222
Sources/Secretive/Views/Views/ContentView.swift
Normal file
222
Sources/Secretive/Views/Views/ContentView.swift
Normal 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
|
||||||
|
|
Loading…
Reference in New Issue
Block a user