Files
Outpost/Outpost/Features/Account/AccountFooter.swift
T
Puranjay Savar Mattas d1ed52b825 redesign(settings): reuse the real sidebar instead of a mini one
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.
2026-08-15 00:36:28 +01:00

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