NSStatusItem Integration
The widget uses a persistent NSStatusItem with a custom popover interface:MenuBarManager.swift:16-27
Copy
Ask AI
@MainActor
final class MenuBarManager: NSObject {
private var statusItem: NSStatusItem!
private var popover: NSPopover!
private let viewModel: AppViewModel
init(viewModel: AppViewModel) {
self.viewModel = viewModel
super.init()
setupStatusItem()
setupPopover()
}
}
MenuBarManager.swift:31-39
Copy
Ask AI
private func setupStatusItem() {
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.squareLength)
if let button = statusItem.button {
button.image = NSImage(systemSymbolName: "arrow.up.to.line.compact",
accessibilityDescription: "R2 Vault")
button.action = #selector(togglePopover)
button.target = self
}
}
The menu bar icon is always visible, providing one-click access to r2Vault from anywhere on your Mac.
Popover UI
The popover uses.applicationDefined behavior to remain visible when the app loses focus:
MenuBarManager.swift:41-53
Copy
Ask AI
private func setupPopover() {
popover = NSPopover()
popover.contentSize = NSSize(width: 300, height: 400)
// .applicationDefined = popover stays open when app loses focus
popover.behavior = .applicationDefined
popover.animates = true
let hostingController = AlwaysActiveHostingController(
rootView: MenuBarView()
.environment(viewModel)
)
popover.contentViewController = hostingController
}
The popover stays open even when you switch to other apps, making it easy to drag files from Finder while keeping r2Vault accessible.
Always Active Window
A custom hosting controller ensures the popover always appears active:MenuBarManager.swift:4-11
Copy
Ask AI
private final class AlwaysActiveHostingController<Content: View>: NSHostingController<Content> {
override func viewDidAppear() {
super.viewDidAppear()
view.window?.makeKey()
}
}
Drop Zone for Uploads
The popover includes a prominent drop zone for quick file uploads:MenuBarView.swift:135-184
Copy
Ask AI
private var dropZone: some View {
ZStack {
RoundedRectangle(cornerRadius: 10)
.fill(isDropTargeted
? Color.accentColor.opacity(0.12)
: Color(NSColor.quaternaryLabelColor).opacity(0.4))
.modifier(GlassDropZoneModifier(isTargeted: isDropTargeted))
if !isDropTargeted {
RoundedRectangle(cornerRadius: 10)
.strokeBorder(
Color(NSColor.separatorColor),
style: StrokeStyle(lineWidth: 1.5, dash: [6, 4])
)
}
VStack(spacing: 8) {
ZStack {
Circle()
.fill(isDropTargeted ? Color.accentColor.opacity(0.15) : Color(NSColor.quaternaryLabelColor).opacity(0.3))
.frame(width: 36, height: 36)
Image(systemName: isDropTargeted ? "arrow.down.circle.fill" : "arrow.up.circle.fill")
.font(.system(size: 18))
.foregroundStyle(isDropTargeted ? Color.accentColor : Color(NSColor.secondaryLabelColor))
}
VStack(spacing: 2) {
Text(isDropTargeted ? "Release to Upload" : "Drop files here")
.font(.system(size: 12, weight: .medium))
.foregroundStyle(isDropTargeted ? Color.accentColor : Color(NSColor.labelColor))
if !isDropTargeted {
Text("or click to browse")
.font(.system(size: 11))
.foregroundStyle(Color(NSColor.secondaryLabelColor))
}
}
}
.padding(.vertical, 16)
}
.frame(height: 100)
.contentShape(Rectangle())
.onTapGesture { openFilePicker() }
.dropDestination(for: URL.self) { urls, _ in
guard viewModel.hasCredentials else { return false }
viewModel.handleDroppedURLs(urls)
return true
} isTargeted: { isDropTargeted = $0 }
.animation(.spring(response: 0.2, dampingFraction: 0.8), value: isDropTargeted)
}
Drag and Drop
Drag files directly onto the drop zone
Click to Browse
Click the drop zone to open a file picker
Active Upload Progress
While uploads are running, the widget shows real-time progress:MenuBarView.swift:9-24
Copy
Ask AI
private var activeUploads: [FileUploadTask] {
viewModel.uploadTasks.filter { $0.status == .uploading || $0.status == .pending }
}
// Live upload progress — shown while uploads are active
if !activeUploads.isEmpty {
uploadProgressSection
}
MenuBarView.swift:188-221
Copy
Ask AI
private var uploadProgressSection: some View {
VStack(spacing: 0) {
Divider()
HStack {
HStack(spacing: 5) {
ProgressView()
.scaleEffect(0.55)
.frame(width: 14, height: 14)
Text("Uploading \(activeUploads.filter { $0.status == .uploading }.count) of \(activeUploads.count)…")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(.primary)
}
Spacer()
Button {
activeUploads.forEach { $0.cancel() }
} label: {
Text("Cancel All")
.font(.system(size: 10))
}
.modifier(GlassButtonModifier())
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, 6)
VStack(spacing: 1) {
ForEach(activeUploads) { task in
MenuBarUploadRow(task: task)
}
}
.padding(.bottom, 8)
}
}
MenuBarView.swift:484-532
Copy
Ask AI
private struct MenuBarUploadRow: View {
let task: FileUploadTask
var body: some View {
HStack(spacing: 10) {
Image(systemName: task.status == .pending ? "clock" : "arrow.up.circle")
.font(.system(size: 12))
.foregroundStyle(task.status == .pending ? Color.secondary : Color.accentColor)
.frame(width: 16)
VStack(alignment: .leading, spacing: 3) {
HStack {
Text(task.fileName)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.primary)
.lineLimit(1)
.truncationMode(.middle)
Spacer()
Text(task.status == .pending ? "Waiting" : "\(Int(task.progress * 100))%")
.font(.system(size: 10))
.foregroundStyle(.secondary)
.monospacedDigit()
}
if task.status == .uploading {
ProgressView(value: task.progress)
.progressViewStyle(.linear)
.tint(Color.accentColor)
.frame(height: 3)
} else {
Rectangle()
.fill(Color(NSColor.separatorColor))
.frame(height: 3)
.clipShape(Capsule())
}
}
Button { task.cancel() } label: {
Image(systemName: "xmark.circle.fill")
.font(.system(size: 13))
.foregroundStyle(.secondary)
}
.buttonStyle(.borderless)
.help("Cancel")
}
.padding(.horizontal, 14)
.padding(.vertical, 5)
}
}
Progress updates in real-time as files upload, with a cancel button for each individual file.
Recent Uploads (25 Most Recent)
The widget displays the 25 most recent successful uploads:MenuBarView.swift:225-258
Copy
Ask AI
private var recentUploadsSection: some View {
VStack(spacing: 0) {
Divider()
HStack {
Text("Recent Uploads")
.font(.system(size: 11, weight: .semibold))
.foregroundStyle(Color(NSColor.secondaryLabelColor))
.textCase(.uppercase)
Spacer()
Button {
openMainWindow()
} label: {
Image(systemName: "arrow.up.right.square")
.font(.system(size: 12))
}
.modifier(GlassButtonModifier())
.help("Open main window")
}
.padding(.horizontal, 14)
.padding(.top, 10)
.padding(.bottom, 6)
ScrollView {
VStack(spacing: 0) {
ForEach(viewModel.historyStore.items.prefix(25)) { item in
MenuBarHistoryRow(item: item)
}
}
}
.frame(maxHeight: 260)
.padding(.bottom, 6)
}
}
Only the 25 most recent uploads are shown in the widget for quick access. The full history is available in the main window.
History Row Actions
Each history row provides quick actions on hover:MenuBarView.swift:353-454
Copy
Ask AI
private struct MenuBarHistoryRow: View {
let item: UploadItem
@Environment(AppViewModel.self) private var viewModel
@State private var isHovered = false
@State private var copied = false
@State private var showDeleteConfirm = false
var body: some View {
HStack(spacing: 10) {
// File type icon badge
ZStack {
RoundedRectangle(cornerRadius: 7)
.fill(iconColor.opacity(0.12))
.frame(width: 34, height: 34)
.overlay(
RoundedRectangle(cornerRadius: 7)
.strokeBorder(iconColor.opacity(0.18), lineWidth: 0.5)
)
Image(systemName: fileIcon)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(iconColor)
}
.shadow(color: iconColor.opacity(0.15), radius: 4, x: 0, y: 2)
VStack(alignment: .leading, spacing: 1) {
Text(item.fileName)
.font(.system(size: 12, weight: .medium))
.lineLimit(1)
.truncationMode(.middle)
Text(item.formattedFileSize)
.font(.system(size: 11))
.foregroundStyle(Color(NSColor.secondaryLabelColor))
}
Spacer()
// Action buttons — visible on hover
if isHovered {
HStack(spacing: 4) {
Button {
viewModel.copyToClipboard(item.publicURL.absoluteString)
withAnimation { copied = true }
DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) {
withAnimation { copied = false }
}
} label: {
Image(systemName: copied ? "checkmark" : "link")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(copied ? .green : .secondary)
.frame(width: 24, height: 24)
}
.modifier(GlassButtonModifier())
.help("Copy URL")
Button {
viewModel.downloadHistoryItem(item)
} label: {
Image(systemName: "arrow.down.circle")
.font(.system(size: 11, weight: .medium))
.frame(width: 24, height: 24)
}
.modifier(GlassButtonModifier())
.help("Download to Downloads folder")
Button {
showDeleteConfirm = true
} label: {
Image(systemName: "trash")
.font(.system(size: 11, weight: .medium))
.foregroundStyle(.red)
.frame(width: 24, height: 24)
}
.modifier(GlassButtonModifier())
.help("Delete from R2")
}
.transition(.opacity.combined(with: .scale(scale: 0.9)))
}
}
.padding(.horizontal, 12)
.padding(.vertical, 6)
.background(
RoundedRectangle(cornerRadius: 8)
.fill(isHovered ? Color(NSColor.quaternaryLabelColor).opacity(0.5) : Color.clear)
.padding(.horizontal, 4)
)
.contentShape(Rectangle())
.onHover { isHovered = $0 }
.animation(.easeOut(duration: 0.12), value: isHovered)
.animation(.easeOut(duration: 0.15), value: copied)
}
}
Copy URL
Copy the public URL to clipboard
Download
Download file to Downloads folder
Delete
Delete file from R2
File Type Icons
History rows display color-coded icons based on file type:MenuBarView.swift:456-479
Copy
Ask AI
private var fileIcon: String {
let ext = (item.fileName as NSString).pathExtension.lowercased()
switch ext {
case "jpg", "jpeg", "png", "gif", "webp", "heic", "heif", "bmp", "tiff", "svg": return "photo.fill"
case "mp4", "mov", "avi", "mkv", "webm", "m4v": return "film.fill"
case "mp3", "m4a", "wav", "aac", "flac", "ogg": return "music.note"
case "pdf": return "doc.richtext.fill"
case "zip", "tar", "gz", "bz2", "7z", "rar": return "archivebox.fill"
case "swift", "py", "js", "ts", "json", "xml", "html", "css", "sh": return "chevron.left.forwardslash.chevron.right"
default: return "doc.fill"
}
}
private var iconColor: Color {
let ext = (item.fileName as NSString).pathExtension.lowercased()
switch ext {
case "jpg", "jpeg", "png", "gif", "webp", "heic", "heif", "bmp", "tiff", "svg": return .purple
case "mp4", "mov", "avi", "mkv", "webm", "m4v": return .pink
case "mp3", "m4a", "wav", "aac", "flac", "ogg": return .orange
case "pdf": return .red
case "zip", "tar", "gz", "bz2", "7z", "rar": return Color(NSColor.brown)
default: return .blue
}
}
File type colors make it easy to visually distinguish between images, videos, documents, and other file types at a glance.
Link Copied Toast
When an upload completes, a toast notification appears at the bottom:MenuBarView.swift:32-40
Copy
Ask AI
if viewModel.clipboardToastFileName != nil {
linkCopiedToast
.transition(.move(edge: .bottom).combined(with: .opacity))
.zIndex(10)
}
MenuBarView.swift:42-65
Copy
Ask AI
private var linkCopiedToast: some View {
HStack(spacing: 6) {
Image(systemName: "checkmark.circle.fill")
.font(.system(size: 13, weight: .semibold))
.foregroundStyle(.green)
VStack(alignment: .leading, spacing: 1) {
Text("Link copied!")
.font(.system(size: 12, weight: .semibold))
if let name = viewModel.clipboardToastFileName {
Text(name)
.font(.system(size: 10))
.foregroundStyle(.secondary)
.lineLimit(1)
.truncationMode(.middle)
}
}
Spacer()
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.modifier(GlassToastModifier())
.padding(.horizontal, 10)
.padding(.bottom, 10)
}
The public URL is automatically copied to your clipboard after every successful upload.
Glass Effects (macOS 26+)
On macOS 26 and later, the widget uses modern glass effects:MenuBarView.swift:302-349
Copy
Ask AI
// Glass toast background — green tint on macOS 26+, solid green fill on older.
private struct GlassToastModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(.regular.tint(.green), in: .rect(cornerRadius: 10))
} else {
content
.background(
RoundedRectangle(cornerRadius: 10)
.fill(Color.green.opacity(0.15))
.overlay(RoundedRectangle(cornerRadius: 10).strokeBorder(Color.green.opacity(0.3), lineWidth: 1))
)
}
}
}
// Glass gear button — interactive glass on macOS 26+, subtle fill on older.
private struct GlassGearModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(.regular.interactive(), in: .rect(cornerRadius: 7))
} else {
content.background(RoundedRectangle(cornerRadius: 7).fill(Color(NSColor.quaternaryLabelColor).opacity(0.5)))
}
}
}
// Glass drop zone — tinted glass on macOS 26+, plain fill on older.
private struct GlassDropZoneModifier: ViewModifier {
let isTargeted: Bool
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.glassEffect(isTargeted ? .regular.tint(.accentColor) : .regular, in: .rect(cornerRadius: 10))
} else {
content // fill already applied on the RoundedRectangle
}
}
}
// Glass button style — .glass on macOS 26+, .borderless on older.
private struct GlassButtonModifier: ViewModifier {
func body(content: Content) -> some View {
if #available(macOS 26.0, *) {
content.buttonStyle(.glass)
} else {
content.buttonStyle(.borderless)
}
}
}
The widget automatically adapts its visual style based on your macOS version, providing modern glass effects on macOS 26+ while maintaining compatibility with older versions.
Bucket Switching
The settings menu allows quick switching between multiple configured buckets:MenuBarView.swift:85-109
Copy
Ask AI
Menu {
// Bucket section
if !viewModel.credentialsList.isEmpty {
if viewModel.credentialsList.count > 1 {
Section("Switch Bucket") {
ForEach(viewModel.credentialsList) { creds in
Button {
viewModel.selectCredentials(id: creds.id)
} label: {
HStack {
Text(creds.bucketName)
if creds.id == viewModel.credentials?.id {
Image(systemName: "checkmark")
}
}
}
}
}
Divider()
} else if let bucket = viewModel.credentials?.bucketName {
Text(bucket)
.foregroundStyle(.secondary)
Divider()
}
}
}
If you manage multiple R2 buckets, you can quickly switch between them without opening the main app window.