sync app filled in
This commit is contained in:
@@ -0,0 +1,306 @@
|
||||
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.createNoteFromClipboard()
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
Reference in New Issue
Block a user