Sidebar footer opens a popover (not Menu — its AppKit-bridged label rendering broke on click with an avatar image in it) with profile, keyboard shortcuts, docs/feedback links, and log out. About panel and a minimal Preferences window round out the app-menu affordances.
123 lines
4.3 KiB
Swift
123 lines
4.3 KiB
Swift
#if os(macOS)
|
|
import SwiftUI
|
|
|
|
/// 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(\.openURL) private var openURL
|
|
@Environment(\.openWindow) private var openWindow
|
|
@Environment(\.openSettings) private var openSettings
|
|
@AppStorage("outpost.appearance") private var appearance: AppAppearance = .system
|
|
@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)
|
|
|
|
menuItem("Profile…") { isShowingProfile = true }
|
|
menuItem("Preferences…") { openSettings() }
|
|
|
|
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
|