New setup (#657)

* WIP

* WIP

* WIP

* Tweaks.

* WIP

* WIP

* WIP

* WIP

* WIP

* Cleanup

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* WIP

* REmove setup menu item

* WIP

* .

* .

* .

* Cleaup.
This commit is contained in:
Max Goedjen
2025-09-03 00:20:24 -07:00
committed by GitHub
parent ddcb2a36ec
commit 147f4d9908
37 changed files with 1856 additions and 932 deletions

View File

@@ -0,0 +1,153 @@
import SwiftUI
struct AgentStatusView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
if agentStatusChecker.running {
AgentRunningView()
} else {
AgentNotRunningView()
}
}
}
struct AgentRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
var body: some View {
Form {
Section {
if let process = agentStatusChecker.process {
ConfigurationItemView(
title: .agentDetailsLocationTitle,
value: process.bundleURL!.path(),
action: .revealInFinder(process.bundleURL!.path()),
)
ConfigurationItemView(
title: .agentDetailsSocketPathTitle,
value: URL.socketPath,
action: .copy(URL.socketPath),
)
ConfigurationItemView(
title: .agentDetailsVersionTitle,
value: Bundle(url: process.bundleURL!)!.infoDictionary!["CFBundleShortVersionString"] as! String
)
if let launchDate = process.launchDate {
ConfigurationItemView(
title: .agentDetailsRunningSinceTitle,
value: launchDate.formatted()
)
}
}
} header: {
Text(.agentRunningNoticeDetailTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentRunningNoticeDetailDescription)
HStack {
Spacer()
Menu(.agentDetailsRestartAgentButton) {
Button(.agentDetailsDisableAgentButton) {
Task {
_ = await LaunchAgentController()
.uninstall()
agentStatusChecker.check()
}
}
} primaryAction: {
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
}
}
}
}
.padding(.vertical)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
struct AgentNotRunningView: View {
@Environment(\.agentStatusChecker) private var agentStatusChecker: any AgentStatusCheckerProtocol
@State var triedRestart = false
@State var loading = false
var body: some View {
Form {
Section {
} header: {
Text(.agentNotRunningNoticeTitle)
.font(.headline)
.padding(.top)
} footer: {
VStack(alignment: .leading, spacing: 10) {
Text(.agentNotRunningNoticeDetailDescription)
HStack {
if !triedRestart {
Spacer()
Button {
guard !loading else { return }
loading = true
Task {
let controller = LaunchAgentController()
let installed = await controller.install()
if !installed {
_ = await controller.forceLaunch()
}
agentStatusChecker.check()
loading = false
if !agentStatusChecker.running {
triedRestart = true
}
}
} label: {
if !loading {
Text(.agentDetailsStartAgentButton)
} else {
HStack {
Text(.agentDetailsStartAgentButtonStarting)
ProgressView()
.controlSize(.mini)
}
}
}
.primaryButton()
} else {
Text(.agentDetailsCouldNotStartError)
.bold()
.foregroundStyle(.red)
}
}
}
.padding(.bottom)
}
}
.formStyle(.grouped)
.frame(width: 400)
}
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: false))
}
#Preview {
AgentStatusView()
.environment(\.agentStatusChecker, PreviewAgentStatusChecker(running: true, process: .current))
}

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

View File

@@ -0,0 +1,179 @@
import SwiftUI
import UniformTypeIdentifiers
struct CopyableView: View {
var title: LocalizedStringResource
var image: Image
var text: String
@State private var interactionState: InteractionState = .normal
var content: some View {
VStack(alignment: .leading) {
HStack {
image
.renderingMode(.template)
.imageScale(.large)
.foregroundColor(primaryTextColor)
Text(title)
.font(.headline)
.foregroundColor(primaryTextColor)
Spacer()
if interactionState != .normal {
hoverIcon
.bold()
.textCase(.uppercase)
.foregroundColor(secondaryTextColor)
.transition(.opacity)
}
}
.padding(EdgeInsets(top: 20, leading: 20, bottom: 10, trailing: 20))
Divider()
Text(text)
.fixedSize(horizontal: false, vertical: true)
.foregroundColor(primaryTextColor)
.padding(EdgeInsets(top: 10, leading: 20, bottom: 20, trailing: 20))
.multilineTextAlignment(.leading)
.font(.system(.body, design: .monospaced))
}
._background(interactionState: interactionState)
.frame(minWidth: 150, maxWidth: .infinity)
}
var body: some View {
content
.onHover { hovering in
withAnimation {
interactionState = hovering ? .hovering : .normal
}
}
.onDrag({
NSItemProvider(item: NSData(data: text.data(using: .utf8)!), typeIdentifier: UTType.utf8PlainText.identifier)
}, preview: {
content
._background(interactionState: .dragging)
})
.onTapGesture {
copy()
withAnimation {
interactionState = .clicking
}
}
.gesture(
TapGesture()
.onEnded {
withAnimation {
interactionState = .normal
}
}
)
}
@ViewBuilder
var hoverIcon: some View {
switch interactionState {
case .hovering:
Image(systemName: "document.on.document")
.accessibilityLabel(String(localized: .copyableClickToCopyButton))
case .clicking:
Image(systemName: "checkmark.circle.fill")
.accessibilityLabel(String(localized: .copyableCopied))
case .normal, .dragging:
EmptyView()
}
}
var primaryTextColor: Color {
switch interactionState {
case .normal, .hovering, .dragging:
return Color(.textColor)
case .clicking:
return .white
}
}
var secondaryTextColor: Color {
switch interactionState {
case .normal, .hovering, .dragging:
return Color(.secondaryLabelColor)
case .clicking:
return .white
}
}
func copy() {
NSPasteboard.general.declareTypes([.string], owner: nil)
NSPasteboard.general.setString(text, forType: .string)
}
}
fileprivate enum InteractionState {
case normal, hovering, clicking, dragging
}
extension View {
fileprivate func _background(interactionState: InteractionState) -> some View {
modifier(BackgroundViewModifier(interactionState: interactionState))
}
}
fileprivate struct BackgroundViewModifier: ViewModifier {
@Environment(\.colorScheme) private var colorScheme
@Environment(\.appearsActive) private var appearsActive
let interactionState: InteractionState
func body(content: Content) -> some View {
if interactionState == .dragging {
content
.background(backgroundColor(interactionState: interactionState), in: RoundedRectangle(cornerRadius: 15))
} else {
if #available(macOS 26.0, *) {
content
// Very thin opacity lets user hover anywhere over the view, glassEffect doesn't allow.
.background(.white.opacity(0.01), in: RoundedRectangle(cornerRadius: 15))
.glassEffect(.regular.tint(backgroundColor(interactionState: interactionState)), in: RoundedRectangle(cornerRadius: 15))
} else {
content
.background(backgroundColor(interactionState: interactionState))
.cornerRadius(10)
}
}
}
func backgroundColor(interactionState: InteractionState) -> Color {
guard appearsActive else { return Color.clear }
switch interactionState {
case .normal:
return colorScheme == .dark ? Color(white: 0.2) : Color(white: 0.885)
case .hovering, .dragging:
return colorScheme == .dark ? Color(white: 0.275) : Color(white: 0.82)
case .clicking:
return .accentColor
}
}
}
#if DEBUG
struct CopyableView_Previews: PreviewProvider {
static var previews: some View {
Group {
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Hello world.")
.padding()
CopyableView(title: .secretDetailSha256FingerprintLabel, image: Image(systemName: "figure.wave"), text: "Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. Long text. ")
.padding()
}
}
}
#endif

View File

@@ -0,0 +1,63 @@
import SwiftUI
import Brief
struct UpdateDetailView: View {
@Environment(\.updater) var updater: any UpdaterProtocol
let update: Release
var body: some View {
VStack {
Text(.updateVersionName(updateName: update.name)).font(.title)
GroupBox(label: Text(.updateReleaseNotesTitle)) {
ScrollView {
attributedBody
}
}
HStack {
if !update.critical {
Button(.updateIgnoreButton) {
Task {
await updater.ignore(release: update)
}
}
Spacer()
}
Button(.updateUpdateButton) {
NSWorkspace.shared.open(update.html_url)
}
.keyboardShortcut(.defaultAction)
}
}
.padding()
.frame(maxWidth: 500)
}
var attributedBody: Text {
var text = Text(verbatim: "")
for line in update.body.split(whereSeparator: \.isNewline) {
let attributed: Text
let split = line.split(separator: " ")
let unprefixed = split.dropFirst().joined(separator: " ")
if let prefix = split.first {
switch prefix {
case "#":
attributed = Text(unprefixed).font(.title) + Text(verbatim: "\n")
case "##":
attributed = Text(unprefixed).font(.title2) + Text(verbatim: "\n")
case "###":
attributed = Text(unprefixed).font(.title3) + Text(verbatim: "\n")
default:
attributed = Text(line) + Text(verbatim: "\n\n")
}
} else {
attributed = Text(line) + Text(verbatim: "\n\n")
}
text = text + attributed
}
return text
}
}