Settings now swaps the actual app sidebar's content (search field, collections tree, account footer) for a section list, instead of SettingsView drawing its own nested sidebar inside the detail pane. Done, wherever it's triggered from, restores the collections tree and detail content exactly as they were. AppNavigation gains selectedSettingsSection (and the SettingsSection enum moves there, shared by the new list and the sidebar host) so the sidebar's list and the detail pane agree on which section is showing. Also deferred the profile menu's "Settings…" action by one runloop tick — setting isShowingSettings synchronously in the same call that dismisses the popover was very likely the source of the AppKit "CA commit" transaction warnings in the console.
134 lines
5.0 KiB
Swift
134 lines
5.0 KiB
Swift
#if os(macOS)
|
|
import SwiftUI
|
|
import OutlineKit
|
|
|
|
/// Uses a plain `Button` + `.popover` rather than `Menu`. A `Menu` whose label
|
|
/// contains the avatar image reliably broke its own sizing on click (even after
|
|
/// several hardening attempts) — that's specifically `Menu`'s AppKit-bridged
|
|
/// label rendering, which a `Button` doesn't go through.
|
|
struct AccountFooter: View {
|
|
@Environment(SessionStore.self) private var session
|
|
@Environment(AppNavigation.self) private var navigation
|
|
@Environment(\.openURL) private var openURL
|
|
@Environment(\.openWindow) private var openWindow
|
|
@AppStorage("outpost.appearance") private var appearance: AppAppearance = .system
|
|
@AppStorage(CachingOutlineAPIClient.offlineModeDefaultsKey) private var isOfflineModeEnabled = false
|
|
@State private var isMenuPresented = false
|
|
@State private var isShowingLogoutConfirmation = false
|
|
@State private var isShowingProfile = false
|
|
|
|
private let repositoryURL = URL(string: "https://git.psmattas.com/psmattas/Outpost")!
|
|
private let issuesURL = URL(string: "https://git.psmattas.com/psmattas/Outpost/issues")!
|
|
|
|
private var apiDocumentationURL: URL? {
|
|
session.serverURL?.appendingPathComponent("developers")
|
|
}
|
|
|
|
var body: some View {
|
|
Button {
|
|
isMenuPresented = true
|
|
} label: {
|
|
HStack(spacing: 10) {
|
|
AvatarBadge(avatarURL: session.userAvatarURL, size: 28, placeholderSystemImage: "person.crop.circle.fill")
|
|
|
|
VStack(alignment: .leading, spacing: 1) {
|
|
Text(session.userName ?? "Signed In")
|
|
.font(.subheadline.weight(.medium))
|
|
.lineLimit(1)
|
|
if let email = session.userEmail {
|
|
Text(email)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.lineLimit(1)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, 12)
|
|
.padding(.vertical, 8)
|
|
.contentShape(Rectangle())
|
|
}
|
|
.buttonStyle(.plain)
|
|
.frame(maxWidth: .infinity)
|
|
.padding(.bottom, 16)
|
|
.popover(isPresented: $isMenuPresented, arrowEdge: .trailing) {
|
|
menuContent
|
|
}
|
|
.logoutConfirmationDialog(isPresented: $isShowingLogoutConfirmation, session: session)
|
|
.sheet(isPresented: $isShowingProfile) {
|
|
ProfileView()
|
|
}
|
|
}
|
|
|
|
private var menuContent: some View {
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
menuItem("Keyboard Shortcuts…") { openWindow(id: "keyboard-shortcuts") }
|
|
|
|
Divider()
|
|
|
|
menuItem("Documentation") { openURL(repositoryURL) }
|
|
if let apiDocumentationURL {
|
|
menuItem("API Documentation") { openURL(apiDocumentationURL) }
|
|
}
|
|
menuItem("Changelog") { openURL(repositoryURL) }
|
|
|
|
Divider()
|
|
|
|
menuItem("Send Us Feedback") { openURL(issuesURL) }
|
|
menuItem("Report a Bug") { openURL(issuesURL) }
|
|
|
|
Divider()
|
|
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text("Appearance")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
Picker("Appearance", selection: $appearance) {
|
|
ForEach(AppAppearance.allCases) { option in
|
|
Text(option.label).tag(option)
|
|
}
|
|
}
|
|
.pickerStyle(.segmented)
|
|
.labelsHidden()
|
|
}
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 4)
|
|
|
|
Toggle("Offline Mode", isOn: $isOfflineModeEnabled)
|
|
.padding(.horizontal, 6)
|
|
.padding(.vertical, 4)
|
|
|
|
menuItem("Profile…") { isShowingProfile = true }
|
|
// Deferred a tick: setting this synchronously in the same call
|
|
// that dismisses this popover collides two AppKit window/layer
|
|
// transactions in the same runloop turn (visible in the console
|
|
// as "Invalid attempt to open a new transaction during CA
|
|
// commit") — letting the popover's dismissal finish first avoids it.
|
|
menuItem("Settings…") { Task { @MainActor in navigation.isShowingSettings = true } }
|
|
|
|
Divider()
|
|
|
|
menuItem("Log Out", role: .destructive) { isShowingLogoutConfirmation = true }
|
|
}
|
|
.padding(8)
|
|
.frame(width: 240)
|
|
}
|
|
|
|
private func menuItem(_ title: String, role: ButtonRole? = nil, action: @escaping () -> Void) -> some View {
|
|
Button(role: role) {
|
|
isMenuPresented = false
|
|
action()
|
|
} label: {
|
|
Text(title)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundStyle(role == .destructive ? Color.red : Color.primary)
|
|
.padding(.vertical, 4)
|
|
.padding(.horizontal, 6)
|
|
.contentShape(Rectangle())
|
|
}
|
|
}
|
|
#endif
|