import SwiftUI public struct UploadsListView: View { @State private var attachments: [AttachmentRecord] = [] @State private var isLoading = false @State private var error: String? public let client: APIClient public init(client: APIClient) { self.client = client } public var body: some View { VStack(spacing: 0) { // Header HStack { Text("Uploaded Files") .font(.system(size: 16, weight: .semibold)) Spacer() Button(action: loadAttachments) { Image(systemName: "arrow.clockwise") .font(.system(size: 13)) } .buttonStyle(.plain) .disabled(isLoading) } .padding(.horizontal, 20) .padding(.vertical, 16) Divider() // Content if isLoading { Spacer() ProgressView() .scaleEffect(0.8) Spacer() } else if let error = error { Spacer() VStack(spacing: 8) { Image(systemName: "exclamationmark.triangle") .font(.system(size: 32)) .foregroundColor(.orange) Text("Failed to load uploads") .font(.headline) Text(error) .font(.caption) .foregroundColor(.secondary) .multilineTextAlignment(.center) } .padding() Spacer() } else if attachments.isEmpty { Spacer() VStack(spacing: 12) { Image(systemName: "tray") .font(.system(size: 40)) .foregroundColor(.secondary.opacity(0.5)) Text("No uploads yet") .font(.system(size: 14, weight: .medium)) .foregroundColor(.secondary) Text("Drop files on the menubar icon to upload") .font(.caption) .foregroundColor(.secondary.opacity(0.7)) } Spacer() } else { List(attachments) { attachment in AttachmentRow(attachment: attachment, client: client) .listRowSeparator(.visible) .listRowInsets(EdgeInsets(top: 8, leading: 20, bottom: 8, trailing: 20)) } .listStyle(.plain) } } .frame(width: 400, height: 500) .onAppear { loadAttachments() } } private func loadAttachments() { isLoading = true error = nil Task { do { let items = try await client.listAttachments() await MainActor.run { attachments = items isLoading = false } } catch { await MainActor.run { self.error = error.localizedDescription isLoading = false } } } } } struct AttachmentRow: View { let attachment: AttachmentRecord let client: APIClient @State private var showingCopied = false var body: some View { HStack(spacing: 12) { // File icon fileIcon // Info VStack(alignment: .leading, spacing: 3) { Text(attachment.originalName) .font(.system(size: 13, weight: .medium)) .lineLimit(1) HStack(spacing: 8) { Text(attachment.contentType) .font(.caption2) .foregroundColor(.secondary) Text("·") .font(.caption2) .foregroundColor(.secondary.opacity(0.5)) Text(formatBytes(attachment.sizeBytes)) .font(.caption2) .foregroundColor(.secondary) Text("·") .font(.caption2) .foregroundColor(.secondary.opacity(0.5)) Text(formatDate(attachment.createdAt)) .font(.caption2) .foregroundColor(.secondary) } } Spacer() // Copy button Button(action: copyURL) { Image(systemName: showingCopied ? "checkmark" : "doc.on.doc") .font(.system(size: 12)) .foregroundColor(showingCopied ? .green : .accentColor) .frame(width: 28, height: 28) .background(showingCopied ? Color.green.opacity(0.1) : Color.accentColor.opacity(0.1)) .clipShape(Circle()) } .buttonStyle(.plain) } .contextMenu { Button("Copy URL") { copyURL() } Button("Open in Browser") { if let url = URL(string: attachment.url, relativeTo: client.baseURL)?.absoluteURL { NSWorkspace.shared.open(url) } } } } private var fileIcon: some View { ZStack { RoundedRectangle(cornerRadius: 6) .fill(iconColor.opacity(0.1)) .frame(width: 36, height: 36) Image(systemName: iconName) .font(.system(size: 16)) .foregroundColor(iconColor) } } private var iconName: String { switch attachment.contentType { case let type where type.hasPrefix("image/"): return "photo" case let type where type.hasPrefix("video/"): return "film" case let type where type.hasPrefix("audio/"): return "music.note" case "application/pdf": return "doc.text" case let type where type.hasPrefix("text/"): return "doc.plaintext" default: return "doc" } } private var iconColor: Color { switch attachment.contentType { case let type where type.hasPrefix("image/"): return .purple case let type where type.hasPrefix("video/"): return .red case let type where type.hasPrefix("audio/"): return .pink case "application/pdf": return .red case let type where type.hasPrefix("text/"): return .blue default: return .gray } } private func copyURL() { let url = URL(string: attachment.url, relativeTo: client.baseURL)?.absoluteURL ?? client.baseURL.appendingPathComponent("attachments/\(attachment.hash)") NSPasteboard.general.clearContents() NSPasteboard.general.setString(url.absoluteString, forType: .string) showingCopied = true DispatchQueue.main.asyncAfter(deadline: .now() + 2) { showingCopied = false } } private func formatBytes(_ bytes: Int) -> String { let formatter = ByteCountFormatter() formatter.countStyle = .file return formatter.string(fromByteCount: Int64(bytes)) } private func formatDate(_ date: Date) -> String { let formatter = RelativeDateTimeFormatter() formatter.unitsStyle = .abbreviated return formatter.localizedString(for: date, relativeTo: Date()) } } #Preview { Text("UploadsListView Preview") }