307 lines
9.1 KiB
Swift
307 lines
9.1 KiB
Swift
import SwiftUI
|
|
|
|
public struct StatusPopoverView: View {
|
|
@ObservedObject public var controller: MenuBarController
|
|
|
|
public init(controller: MenuBarController) {
|
|
self.controller = controller
|
|
}
|
|
|
|
public var body: some View {
|
|
VStack(spacing: 0) {
|
|
// Header with status
|
|
headerSection
|
|
|
|
Divider()
|
|
|
|
// Quick actions
|
|
actionsSection
|
|
|
|
Divider()
|
|
|
|
// Recent uploads preview (last 3)
|
|
recentUploadsSection
|
|
|
|
Divider()
|
|
|
|
// Footer
|
|
footerSection
|
|
}
|
|
.frame(width: 320)
|
|
.padding(.vertical, 12)
|
|
}
|
|
|
|
// MARK: - Sections
|
|
|
|
private var headerSection: some View {
|
|
VStack(spacing: 8) {
|
|
HStack {
|
|
statusIndicator
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(controller.syncStatus)
|
|
.font(.system(size: 13, weight: .medium))
|
|
|
|
if let lastSync = controller.lastSyncTime {
|
|
Text("Last sync: \(timeAgo(lastSync))")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
} else {
|
|
Text("Never synced")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
|
|
Spacer()
|
|
|
|
if controller.isSyncing {
|
|
ProgressView()
|
|
.scaleEffect(0.6)
|
|
}
|
|
}
|
|
|
|
if controller.pendingChanges > 0 {
|
|
Text("\(controller.pendingChanges) pending changes")
|
|
.font(.caption)
|
|
.foregroundColor(.orange)
|
|
.frame(maxWidth: .infinity, alignment: .leading)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
}
|
|
|
|
private var statusIndicator: some View {
|
|
Circle()
|
|
.fill(statusColor)
|
|
.frame(width: 8, height: 8)
|
|
.overlay(
|
|
Circle()
|
|
.stroke(statusColor.opacity(0.3), lineWidth: 2)
|
|
.frame(width: 14, height: 14)
|
|
)
|
|
}
|
|
|
|
private var statusColor: Color {
|
|
if controller.isSyncing { return .blue }
|
|
if controller.syncStatus.contains("fail") || controller.syncStatus.contains("error") {
|
|
return .red
|
|
}
|
|
if controller.pendingChanges > 0 { return .orange }
|
|
return .green
|
|
}
|
|
|
|
private var actionsSection: some View {
|
|
HStack(spacing: 16) {
|
|
ActionButton(
|
|
icon: "arrow.clockwise",
|
|
label: "Sync",
|
|
color: .blue
|
|
) {
|
|
Task {
|
|
await controller.performSync()
|
|
}
|
|
}
|
|
|
|
ActionButton(
|
|
icon: "arrow.up.doc",
|
|
label: "Upload",
|
|
color: .purple
|
|
) {
|
|
controller.quickUpload()
|
|
}
|
|
|
|
ActionButton(
|
|
icon: "doc.badge.plus",
|
|
label: "Note",
|
|
color: .green
|
|
) {
|
|
controller.createNote()
|
|
}
|
|
|
|
ActionButton(
|
|
icon: "square.and.arrow.down",
|
|
label: "Drop",
|
|
color: .orange
|
|
) {
|
|
controller.showDropZone()
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 12)
|
|
}
|
|
|
|
@ViewBuilder
|
|
private var recentUploadsSection: some View {
|
|
VStack(alignment: .leading, spacing: 8) {
|
|
HStack {
|
|
Text("Recent Uploads")
|
|
.font(.caption)
|
|
.fontWeight(.semibold)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
Button("View All") {
|
|
controller.showUploads()
|
|
}
|
|
.font(.caption)
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(.accentColor)
|
|
}
|
|
|
|
if controller.isLoadingRecentUploads && controller.recentUploads.isEmpty {
|
|
ProgressView()
|
|
.scaleEffect(0.6)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.vertical, 8)
|
|
} else if controller.recentUploads.isEmpty {
|
|
Text(controller.recentUploadsError == nil ? "No recent uploads" : "Unable to load recent uploads")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
.frame(maxWidth: .infinity, alignment: .center)
|
|
.padding(.vertical, 8)
|
|
} else {
|
|
ForEach(controller.recentUploads) { upload in
|
|
RecentUploadRow(upload: upload, baseURL: controller.config.serverBaseURL)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
private var footerSection: some View {
|
|
HStack(spacing: 12) {
|
|
Button {
|
|
controller.showSettings()
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "gear")
|
|
Text("Settings")
|
|
}
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(.secondary)
|
|
|
|
Button {
|
|
NSApp.terminate(nil)
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Image(systemName: "power")
|
|
.font(.caption2)
|
|
Text("Quit")
|
|
}
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(.secondary)
|
|
|
|
Spacer()
|
|
|
|
if let url = controller.config.serverBaseURL {
|
|
Button {
|
|
NSWorkspace.shared.open(url)
|
|
} label: {
|
|
HStack(spacing: 4) {
|
|
Text("Open Server")
|
|
Image(systemName: "arrow.up.right.square")
|
|
.font(.caption2)
|
|
}
|
|
.font(.caption)
|
|
}
|
|
.buttonStyle(.plain)
|
|
.foregroundColor(.accentColor)
|
|
}
|
|
}
|
|
.padding(.horizontal, 16)
|
|
.padding(.top, 8)
|
|
}
|
|
|
|
// MARK: - Helpers
|
|
|
|
private func timeAgo(_ date: Date) -> String {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .abbreviated
|
|
return formatter.localizedString(for: date, relativeTo: Date())
|
|
}
|
|
}
|
|
|
|
private struct RecentUploadRow: View {
|
|
let upload: AttachmentRecord
|
|
let baseURL: URL?
|
|
|
|
var body: some View {
|
|
Button(action: openUpload) {
|
|
HStack(spacing: 8) {
|
|
Image(systemName: upload.kind == "document" ? "doc.text" : "paperclip")
|
|
.foregroundColor(.accentColor)
|
|
|
|
VStack(alignment: .leading, spacing: 2) {
|
|
Text(upload.originalName)
|
|
.font(.caption)
|
|
.lineLimit(1)
|
|
Text(timeAgo(upload.createdAt))
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
|
|
Spacer()
|
|
|
|
Image(systemName: "arrow.up.right.square")
|
|
.font(.caption2)
|
|
.foregroundColor(.secondary)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
|
|
private func openUpload() {
|
|
guard let baseURL,
|
|
let url = URL(string: upload.url, relativeTo: baseURL)?.absoluteURL else {
|
|
return
|
|
}
|
|
NSWorkspace.shared.open(url)
|
|
}
|
|
|
|
private func timeAgo(_ date: Date) -> String {
|
|
let formatter = RelativeDateTimeFormatter()
|
|
formatter.unitsStyle = .abbreviated
|
|
return formatter.localizedString(for: date, relativeTo: Date())
|
|
}
|
|
}
|
|
|
|
// MARK: - Action Button
|
|
|
|
struct ActionButton: View {
|
|
let icon: String
|
|
let label: String
|
|
let color: Color
|
|
let action: () -> Void
|
|
|
|
var body: some View {
|
|
Button(action: action) {
|
|
VStack(spacing: 6) {
|
|
Image(systemName: icon)
|
|
.font(.system(size: 20, weight: .medium))
|
|
.foregroundColor(color)
|
|
.frame(width: 40, height: 40)
|
|
.background(color.opacity(0.1))
|
|
.clipShape(RoundedRectangle(cornerRadius: 10))
|
|
|
|
Text(label)
|
|
.font(.caption2)
|
|
.fontWeight(.medium)
|
|
.foregroundColor(.primary)
|
|
}
|
|
}
|
|
.buttonStyle(.plain)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
// Preview won't work without a real controller
|
|
Text("StatusPopoverView Preview")
|
|
}
|