// // ContentView.swift // Ventry Upload Watcher // // Created by Jan-Marlon Leibl on 18.01.25. // import SwiftUI import AppKit import UserNotifications import ServiceManagement import CoreServices extension NSWindow { open override func awakeFromNib() { super.awakeFromNib() self.styleMask.remove(.resizable) self.styleMask.remove(.fullScreen) } } extension Color { static let accentPurple = Color(hex: 0x8331FD) } extension Color { init(hex: UInt) { self.init( .sRGB, red: Double((hex >> 16) & 0xff) / 255, green: Double((hex >> 8) & 0xff) / 255, blue: Double(hex & 0xff) / 255, opacity: 1 ) } } // MARK: - Upload Manager class UploadManager { private static let endpoint = URL(string: "https://ventry.host/api/upload")! static func uploadFile(_ fileURL: URL, apiKey: String, completion: @escaping (Result) -> Void) { var request = URLRequest(url: endpoint) request.httpMethod = "POST" request.setValue(apiKey, forHTTPHeaderField: "Authorization") let boundary = "Boundary-\(UUID().uuidString)" request.setValue("multipart/form-data; boundary=\(boundary)", forHTTPHeaderField: "Content-Type") URLSession.shared.uploadTask(with: request, from: createMultipartFormData(fileURL: fileURL, boundary: boundary)) { data, response, error in if let error = error { return completion(.failure(error)) } guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode), let data = data, let responseString = String(data: data, encoding: .utf8) else { return completion(.failure(NSError(domain: "", code: (response as? HTTPURLResponse)?.statusCode ?? -1))) } completion(.success(responseString)) }.resume() } private static func createMultipartFormData(fileURL: URL, boundary: String) -> Data { var data = Data() let filename = fileURL.lastPathComponent.replacingOccurrences(of: " ", with: "%20") data.append("--\(boundary)\r\nContent-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\nContent-Type: \(fileURL.mimeType())\r\n\r\n".data(using: .utf8)!) if let fileData = try? Data(contentsOf: fileURL) { data.append(fileData) data.append("\r\n--\(boundary)--\r\n".data(using: .utf8)!) } return data } } extension URL { func mimeType() -> String { let mimeTypes = ["jpg": "image/jpeg", "jpeg": "image/jpeg", "png": "image/png", "gif": "image/gif", "webp": "image/webp", "mp4": "video/mp4", "mov": "video/quicktime"] return mimeTypes[pathExtension.lowercased()] ?? "application/octet-stream" } } // MARK: - Folder Monitor class FolderMonitor: ObservableObject { @Published private(set) var isWatching = false private var timer: Timer? private var currentPaths: [String] = [] private var knownFiles = Set() var onFileDetected: ((URL) -> Void)? deinit { stopWatching() } func startWatching(paths: [String]) { stopWatching() currentPaths = paths for path in paths { let url = URL(fileURLWithPath: path) guard url.startAccessingSecurityScopedResource() else { NotificationManager.send(title: "Permission Required", message: "Please grant folder access") continue } saveBookmark(for: url) } updateKnownFiles() startMonitoring() } private func startMonitoring() { timer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in self?.checkForNewFiles() } isWatching = true } private func checkForNewFiles() { for path in currentPaths { do { let contents = try FileManager.default.contentsOfDirectory(atPath: path) let newFiles = Set(contents).subtracting(knownFiles) for file in newFiles { let fileURL = URL(fileURLWithPath: path).appendingPathComponent(file) if fileURL.lastPathModified.timeIntervalSinceNow > -2 { onFileDetected?(fileURL) } } knownFiles = knownFiles.union(newFiles) } catch { print("Error checking files in \(path): \(error)") } } } private func updateKnownFiles() { var allFiles = Set() for path in currentPaths { guard let contents = try? FileManager.default.contentsOfDirectory(atPath: path) else { continue } allFiles = allFiles.union(Set(contents.map { path + "/" + $0 })) } knownFiles = allFiles } private func saveBookmark(for url: URL) { guard let bookmarkData = try? url.bookmarkData( options: .withSecurityScope, includingResourceValuesForKeys: nil, relativeTo: nil ) else { return } UserDefaults.standard.set(bookmarkData, forKey: "FolderBookmark-\(url.path)") } func stopWatching() { timer?.invalidate() timer = nil isWatching = false currentPaths.removeAll() knownFiles.removeAll() } func addPath(_ path: String) { if !currentPaths.contains(path) { currentPaths.append(path) let url = URL(fileURLWithPath: path) guard url.startAccessingSecurityScopedResource() else { NotificationManager.send(title: "Permission Required", message: "Please grant folder access") return } saveBookmark(for: url) updateKnownFiles() } } } // MARK: - Notification Manager class NotificationManager { static func send(title: String, message: String) { let content = UNMutableNotificationContent() content.title = title content.body = message content.sound = .default UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound]) { granted, _ in if granted { UNUserNotificationCenter.current().add(UNNotificationRequest(identifier: UUID().uuidString, content: content, trigger: nil)) } } } } // MARK: - Haptics class HapticManager { static func play(_ type: NSHapticFeedbackManager.FeedbackPattern) { NSHapticFeedbackManager.defaultPerformer.perform(type, performanceTime: .default) } static func success() { play(.levelChange) } static func error() { play(.generic) } static func tap() { play(.alignment) } } // MARK: - Views struct CardView: View { let content: Content init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { content .padding() .background(Color(.windowBackgroundColor)) .cornerRadius(12) } } struct StatusBadge: View { let status: String let color: Color var body: some View { Text(status) .font(.caption.bold()) .padding(.horizontal, 8) .padding(.vertical, 4) .background(color.opacity(0.2)) .foregroundColor(color) .cornerRadius(6) } } class LaunchAtLoginHelper { static func setLaunchAtLogin(_ enabled: Bool) { let service = SMAppService.mainApp do { if enabled { try service.register() } else { try service.unregister() } print("Successfully \(enabled ? "enabled" : "disabled") launch at login") } catch { print("Failed to \(enabled ? "enable" : "disable") launch at login: \(error)") } } } struct SettingsView: View { @Binding var isPresented: Bool @Binding var apiKey: String @Environment(\.colorScheme) var colorScheme @FocusState private var isAPIKeyFocused: Bool @AppStorage("launchAtLogin") private var launchAtLogin = false var body: some View { VStack(spacing: Design.padding) { HStack { Text("Settings") .font(.system(size: 24, weight: .bold)) Spacer() Button(action: { withAnimation(Animation.spring) { isPresented = false } }) { Circle() .fill(Color(.windowBackgroundColor).opacity(0.8)) .frame(width: 32, height: 32) .overlay( Image(systemName: "xmark") .font(.system(size: 12, weight: .medium)) .foregroundColor(.secondary) ) } .buttonStyle(.plain) } VStack(alignment: .leading, spacing: Design.spacing) { Text("API Key") .font(.system(size: 15, weight: .semibold)) SecureField("Enter your ventry.host API key", text: $apiKey) .textFieldStyle(.plain) .padding(12) .background( RoundedRectangle(cornerRadius: Design.cornerRadius, style: .continuous) .fill(Color(.textBackgroundColor)) ) .focused($isAPIKeyFocused) Text("Get your API key from ventry.host") .font(.system(size: 12)) .foregroundColor(.secondary) } .padding(.bottom, 8) VStack(alignment: .leading, spacing: Design.spacing) { Text("Startup Options") .font(.system(size: 15, weight: .semibold)) .padding(.bottom, 4) Toggle(isOn: $launchAtLogin) { VStack(alignment: .leading, spacing: 2) { Text("Launch at login") .font(.system(size: 14)) Text("Automatically start the app when you log in") .font(.system(size: 12)) .foregroundColor(.secondary) } } .onChange(of: launchAtLogin) { _, newValue in LaunchAtLoginHelper.setLaunchAtLogin(newValue) } } Spacer() Button(action: { HapticManager.success() withAnimation(Animation.spring) { isPresented = false } }) { Text("Save") .font(.system(size: 15, weight: .semibold)) .foregroundColor(.white) .frame(maxWidth: .infinity) .padding(.vertical, 12) .background( RoundedRectangle(cornerRadius: Design.cornerRadius, style: .continuous) .fill(Color.accentPurple) ) } .buttonStyle(.plain) .keyboardShortcut(.return) } .padding(24) .frame(width: 400, height: 360) .background(colorScheme == .dark ? Color(.windowBackgroundColor) : .white) .onAppear { isAPIKeyFocused = true } } } @propertyWrapper struct UserDefaultsArray { private let key: String init(key: String) { self.key = key } var wrappedValue: [T] { get { guard let data = UserDefaults.standard.data(forKey: key), let array = try? JSONDecoder().decode([T].self, from: data) else { return [] } return array } set { guard let data = try? JSONEncoder().encode(newValue) else { return } UserDefaults.standard.set(data, forKey: key) } } } struct HeaderView: View { @Binding var showingSettings: Bool var body: some View { HStack(spacing: Design.padding) { VStack(alignment: .leading, spacing: 2) { Text("ventry.host") .font(.system(size: 24, weight: .bold)) Text("Tool for uploading files to ventry.host") .font(.system(size: 13, weight: .medium)) .foregroundColor(.secondary) } Spacer() Button(action: { HapticManager.tap() withAnimation(Animation.spring) { showingSettings.toggle() } }) { Image(systemName: "gear") .font(.system(size: Design.iconSize)) .foregroundColor(.secondary) .frame(width: 32, height: 32) .background(Color(.controlBackgroundColor)) .clipShape(Circle()) } .buttonStyle(.plain) } } } struct StatusCardView: View { let folderMonitor: FolderMonitor let uploadStatus: String let lastUpload: String var body: some View { VStack(spacing: Design.padding) { HStack { ZStack { Circle() .fill(folderMonitor.isWatching ? Color.green.opacity(0.15) : Color.red.opacity(0.15)) .frame(width: 24, height: 24) Circle() .fill(folderMonitor.isWatching ? Color.green : Color.red) .frame(width: 8, height: 8) .blur(radius: folderMonitor.isWatching ? 2 : 0) } Text(folderMonitor.isWatching ? "Watching" : "Inactive") .font(.system(size: Design.iconSize, weight: .medium)) .foregroundColor(folderMonitor.isWatching ? .green : .red) Spacer() } VStack(spacing: Design.spacing) { statusRow( icon: "arrow.up.circle", title: "Status", value: uploadStatus ) Divider() .background(Color.primary.opacity(Design.opacity)) statusRow( icon: "clock", title: "Last Upload", value: lastUpload ) } } .padding(Design.padding) .background(Color(.controlBackgroundColor)) .cornerRadius(Design.cornerRadius) .animation(Animation.spring, value: folderMonitor.isWatching) } private func statusRow(icon: String, title: String, value: String) -> some View { HStack(spacing: Design.spacing) { Image(systemName: icon) .font(.system(size: Design.iconSize)) .foregroundColor(.secondary) .frame(width: 16) Text(title) .font(.system(size: Design.iconSize)) .foregroundColor(.secondary) Spacer() Text(value) .font(.system(size: Design.iconSize, weight: .medium)) .multilineTextAlignment(.trailing) } } } struct WatchedFoldersView: View { @Binding var watchedFolders: [String] let folderMonitor: FolderMonitor let onAddFolder: () -> Void let onStopWatching: () -> Void var body: some View { VStack(spacing: Design.spacing) { List { ForEach(watchedFolders, id: \.self) { path in folderCard(path) .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) .listRowBackground(Color.clear) .listRowSeparator(.hidden) .swipeActions(edge: .trailing, allowsFullSwipe: true) { Button(role: .destructive) { withAnimation(Animation.spring) { watchedFolders.removeAll { $0 == path } if watchedFolders.isEmpty { folderMonitor.stopWatching() } else { folderMonitor.startWatching(paths: watchedFolders) } } HapticManager.tap() } label: { Label("Remove", systemImage: "trash") } } } } .listStyle(.plain) .scrollContentBackground(.hidden) .frame(height: CGFloat(watchedFolders.count * 60)) .padding(.horizontal, -8) VStack(spacing: Design.spacing) { Button(action: { HapticManager.tap() onAddFolder() }) { HStack(spacing: Design.spacing) { Image(systemName: "folder.badge.plus") .font(.system(size: Design.iconSize)) Text("Add Another Folder") .font(.system(size: Design.iconSize)) } .frame(maxWidth: .infinity) .padding(12) .background( RoundedRectangle(cornerRadius: Design.cornerRadius) .fill(Color.accentPurple.opacity(Design.opacity)) ) .foregroundColor(.accentPurple) } .buttonStyle(.plain) Button(action: { HapticManager.tap() onStopWatching() }) { HStack(spacing: Design.spacing) { Image(systemName: "stop.circle.fill") .font(.system(size: Design.iconSize)) Text("Stop Watching") .font(.system(size: Design.iconSize)) } .frame(maxWidth: .infinity) .padding(12) .background( RoundedRectangle(cornerRadius: Design.cornerRadius) .fill(Color.red.opacity(Design.opacity)) ) .foregroundColor(.red) } .buttonStyle(.plain) } } } private func folderCard(_ path: String) -> some View { HStack(spacing: Design.spacing) { Image(systemName: "folder.fill") .font(.system(size: Design.iconSize)) .foregroundColor(.accentPurple) Text(path) .font(.system(size: Design.iconSize)) .lineLimit(1) .truncationMode(.middle) .frame(maxWidth: .infinity, alignment: .leading) } .padding(.vertical, 12) .padding(.horizontal, Design.padding) .frame(maxWidth: .infinity) .background( RoundedRectangle(cornerRadius: Design.cornerRadius, style: .continuous) .fill(Color.accentPurple.opacity(Design.opacity)) ) .transition(.move(edge: .bottom).combined(with: .opacity)) } } struct LeftSideView: View { let folderMonitor: FolderMonitor let uploadStatus: String let lastUpload: String @Binding var showingSettings: Bool @Binding var watchedFolders: [String] let apiKey: String let onToggleWatching: () -> Void let onAddFolder: () -> Void var body: some View { ScrollView { VStack(spacing: 16) { HeaderView(showingSettings: $showingSettings) StatusCardView(folderMonitor: folderMonitor, uploadStatus: uploadStatus, lastUpload: lastUpload) if !watchedFolders.isEmpty { WatchedFoldersView( watchedFolders: $watchedFolders, folderMonitor: folderMonitor, onAddFolder: onAddFolder, onStopWatching: onToggleWatching ) } if watchedFolders.isEmpty { ActionButtonView(folderMonitor: folderMonitor, apiKey: apiKey, onToggleWatching: onToggleWatching) } Spacer(minLength: 0) } .padding(24) } .frame(width: 360) .background(Color(.windowBackgroundColor)) } } struct RightSideView: View { let isUploading: Bool @Binding var uploadHistory: [(date: Date, url: String, filePath: String)] @Binding var copiedURL: String? let folderMonitor: FolderMonitor var body: some View { VStack(alignment: .leading, spacing: 16) { Text("Recent Uploads") .font(.system(size: 20, weight: .bold)) if isUploading { UploadingCardView() } UploadHistoryListView( uploadHistory: $uploadHistory, copiedURL: $copiedURL, folderMonitor: folderMonitor ) } .padding(24) .frame(maxWidth: .infinity) .background(Color(.textBackgroundColor)) } } struct ActionButtonView: View { let folderMonitor: FolderMonitor let apiKey: String let onToggleWatching: () -> Void var body: some View { Button(action: { HapticManager.tap() onToggleWatching() }) { HStack(spacing: 8) { Image(systemName: folderMonitor.isWatching ? "stop.circle.fill" : "folder.badge.plus") Text(folderMonitor.isWatching ? "Stop Watching" : "Add Folders...") .font(.system(size: 15, weight: .semibold)) } .frame(maxWidth: .infinity) .frame(height: 44) .background(folderMonitor.isWatching ? Color.red : Color(.controlAccentColor)) .cornerRadius(12) .foregroundColor(.white) } .buttonStyle(.plain) .disabled(apiKey.isEmpty) .opacity(apiKey.isEmpty ? 0.5 : 1) .help(folderMonitor.isWatching ? "Stop watching all folders" : "Select multiple folders to watch") } } struct UploadingCardView: View { var body: some View { HStack(spacing: 12) { ProgressView() .scaleEffect(0.8) Text("Uploading file...") .font(.system(size: 14, weight: .medium)) Spacer() } .padding(16) .background( ZStack { RoundedRectangle(cornerRadius: 16, style: .continuous) .fill(Color.accentPurple.opacity(0.1)) RoundedRectangle(cornerRadius: 16, style: .continuous) .fill( LinearGradient( colors: [Color.accentPurple.opacity(0.1), Color.clear], startPoint: .topLeading, endPoint: .bottomTrailing ) ) } ) .transition(.move(edge: .top).combined(with: .opacity)) } } private enum Animation { static let spring = SwiftUI.Animation.spring(response: 0.3, dampingFraction: 0.7) static let copyDelay: TimeInterval = 1.5 } private enum Design { static let cornerRadius: CGFloat = 12 static let opacity = 0.1 static let iconSize: CGFloat = 14 static let spacing: CGFloat = 8 static let padding: CGFloat = 16 } struct UploadHistoryListView: View { @Binding var uploadHistory: [(date: Date, url: String, filePath: String)] @Binding var copiedURL: String? let folderMonitor: FolderMonitor @Namespace private var scrollSpace var body: some View { ScrollViewReader { proxy in List { ForEach(uploadHistory, id: \.date) { upload in lastUploadCard(upload.url, date: upload.date, filePath: upload.filePath) .listRowInsets(EdgeInsets(top: 4, leading: 0, bottom: 4, trailing: 0)) .listRowBackground(Color.clear) .listRowSeparator(.hidden) .swipeActions(edge: .trailing, allowsFullSwipe: false) { Button(role: .destructive) { withAnimation(Animation.spring) { uploadHistory.removeAll { $0.url == upload.url } } HapticManager.tap() } label: { Label("Delete", systemImage: "trash") } Button { if let url = URL(string: upload.url) { NSWorkspace.shared.open(url) HapticManager.tap() } } label: { Label("Open", systemImage: "safari") } .tint(.accentPurple) } .id(upload.date) } if uploadHistory.isEmpty && !folderMonitor.isWatching { VStack(spacing: Design.spacing) { Image(systemName: "folder.badge.plus") .font(.system(size: 32)) .foregroundColor(.secondary) Text("Select a folder to start watching") .font(.system(size: Design.iconSize)) .foregroundColor(.secondary) } .frame(maxWidth: .infinity) .frame(height: 160) .listRowInsets(EdgeInsets()) .listRowBackground(Color.clear) .listRowSeparator(.hidden) } } .listStyle(.plain) .scrollContentBackground(.hidden) .onChange(of: uploadHistory.map(\.url)) { oldValue, newValue in if !newValue.isEmpty { withAnimation(Animation.spring) { proxy.scrollTo(uploadHistory[0].date, anchor: .top) } } } } } private func lastUploadCard(_ url: String, date: Date = Date(), filePath: String? = nil) -> some View { Button(action: { copyToClipboard(url) withAnimation(Animation.spring) { copiedURL = url } HapticManager.success() DispatchQueue.main.asyncAfter(deadline: .now() + Animation.copyDelay) { withAnimation(Animation.spring) { if copiedURL == url { copiedURL = nil } } } }) { VStack(alignment: .leading, spacing: Design.spacing) { if let filePath = filePath, let nsImage = NSImage(contentsOf: URL(fileURLWithPath: filePath)) { Image(nsImage: nsImage) .resizable() .aspectRatio(contentMode: .fill) .frame(height: 120) .frame(maxWidth: .infinity) .clipShape(RoundedRectangle(cornerRadius: Design.cornerRadius)) } VStack(alignment: .leading, spacing: Design.spacing) { Text(date.formatted(date: .numeric, time: .shortened)) .font(.system(size: 12)) .foregroundColor(.secondary) HStack(spacing: Design.spacing) { Image(systemName: "link") .font(.system(size: Design.iconSize)) .foregroundColor(copiedURL == url ? .green : .accentPurple) Text(url) .font(.system(size: Design.iconSize)) .lineLimit(1) .truncationMode(.middle) Spacer() HStack(spacing: 4) { if copiedURL == url { Image(systemName: "checkmark.circle.fill") .font(.system(size: Design.iconSize)) .transition(.scale.combined(with: .opacity)) Text("Copied!") } else { Text("Copy") } } .font(.system(size: 12, weight: .medium)) .foregroundColor(copiedURL == url ? .green : .accentPurple) .animation(Animation.spring, value: copiedURL) } } } .padding(Design.padding) .background( RoundedRectangle(cornerRadius: Design.cornerRadius, style: .continuous) .fill(copiedURL == url ? Color.green.opacity(Design.opacity) : Color.accentPurple.opacity(Design.opacity)) ) } .buttonStyle(.plain) .contentShape(Rectangle()) .animation(Animation.spring, value: copiedURL) } private func copyToClipboard(_ text: String) { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) } } class WindowManager { static let shared = WindowManager() private var window: NSWindow? func setWindow(_ window: NSWindow?) { self.window = window } func hideWindow() { window?.orderOut(nil) } func showWindow() { window?.makeKeyAndOrderFront(nil) NSApp.activate(ignoringOtherApps: true) } } struct ContentView: View { @StateObject private var folderMonitor = FolderMonitor() @State private var uploadStatus = "No active uploads" @State private var lastUpload = "Never" @State private var showingSettings = false @AppStorage("apiKey") private var apiKey = "" @State private var watchedFolders: [String] = UserDefaults.standard.stringArray(forKey: "watchedFolders") ?? [] @State private var errorMessage: String? @State private var showError = false @State private var lastUploadURL: String? @State private var isUploading = false @State private var showCopiedBadge = false @Environment(\.colorScheme) var colorScheme @State private var uploadHistory: [(date: Date, url: String, filePath: String)] = [] @State private var copiedURL: String? = nil var body: some View { MainLayout { HStack(spacing: 0) { LeftSideView( folderMonitor: folderMonitor, uploadStatus: uploadStatus, lastUpload: lastUpload, showingSettings: $showingSettings, watchedFolders: $watchedFolders, apiKey: apiKey, onToggleWatching: toggleWatching, onAddFolder: selectFolder ) RightSideView( isUploading: isUploading, uploadHistory: $uploadHistory, copiedURL: $copiedURL, folderMonitor: folderMonitor ) } } .frame(width: 800, height: 400) .background(Color(.windowBackgroundColor)) .alert("Error", isPresented: $showError) { Button("OK", role: .cancel) {} } message: { Text(errorMessage ?? "Unknown error") } .sheet(isPresented: $showingSettings) { SettingsView(isPresented: $showingSettings, apiKey: $apiKey) } .onAppear { setupFolderMonitor() restoreWatching() } .onChange(of: watchedFolders) { oldValue, newValue in UserDefaults.standard.set(newValue, forKey: "watchedFolders") if !folderMonitor.isWatching && !newValue.isEmpty { folderMonitor.startWatching(paths: newValue) } } } private func toggleWatching() { if folderMonitor.isWatching { folderMonitor.stopWatching() uploadStatus = "Watching stopped" watchedFolders.removeAll() } else { selectFolder() } } private func selectFolder() { let panel = NSOpenPanel() panel.allowsMultipleSelection = true panel.canChooseDirectories = true panel.canChooseFiles = false panel.message = "Select folders to watch for screenshots" panel.prompt = "Watch Folders" panel.begin { response in guard response == .OK else { return } DispatchQueue.main.async { for url in panel.urls { guard url.startAccessingSecurityScopedResource() else { NotificationManager.send(title: "Permission Required", message: "Please grant folder access for \(url.path)") continue } if !watchedFolders.contains(url.path) { watchedFolders.append(url.path) if let bookmarkData = try? url.bookmarkData(options: .withSecurityScope) { UserDefaults.standard.set(bookmarkData, forKey: "FolderBookmark-\(url.path)") } } } if !watchedFolders.isEmpty { if !folderMonitor.isWatching { uploadStatus = "Starting to watch folders..." folderMonitor.startWatching(paths: watchedFolders) } else { for url in panel.urls { folderMonitor.addPath(url.path) } } uploadStatus = "Watching for new files..." NotificationManager.send(title: "Folders Added", message: "Now watching \(panel.urls.count) new folder(s)") } } } } private func setupFolderMonitor() { folderMonitor.onFileDetected = handleNewFile } private func handleNewFile(_ fileURL: URL) { uploadStatus = "Uploading \(fileURL.lastPathComponent)..." NotificationManager.send(title: "Uploading File", message: "Starting upload of \(fileURL.lastPathComponent)") withAnimation { isUploading = true } UploadManager.uploadFile(fileURL, apiKey: apiKey) { result in DispatchQueue.main.async { withAnimation { isUploading = false } handleUploadResult(result) } } } private func handleUploadResult(_ result: Result) { switch result { case .success(let response): handleSuccessfulUpload(response) case .failure(let error): handleFailedUpload(error) } } private func handleSuccessfulUpload(_ response: String) { do { if let jsonData = response.data(using: .utf8), let json = try JSONSerialization.jsonObject(with: jsonData) as? [String: Any], let url = json["url"] as? String, let currentFile = lastUploadedFile?.path { withAnimation(.spring()) { uploadStatus = "Upload successful" lastUpload = Date().formatted(date: .numeric, time: .shortened) lastUploadURL = url uploadHistory.insert((date: Date(), url: url, filePath: currentFile), at: 0) if uploadHistory.count > 50 { uploadHistory.removeLast() } } copyToClipboard(url) HapticManager.success() NotificationManager.send(title: "Upload Successful", message: "URL copied to clipboard") } else { throw NSError(domain: "", code: -1) } } catch { handleFailedUpload(error) } } private func handleFailedUpload(_ error: Error) { withAnimation { errorMessage = error.localizedDescription showError = true uploadStatus = "Upload failed" } HapticManager.error() NotificationManager.send(title: "Upload Failed", message: error.localizedDescription) } private var lastUploadedFile: URL? { guard let path = watchedFolders.first else { return nil } let fm = FileManager.default let contents = try? fm.contentsOfDirectory(atPath: path) return contents?.compactMap { URL(fileURLWithPath: path).appendingPathComponent($0) } .sorted { $0.lastPathModified > $1.lastPathModified } .first } private func copyToClipboard(_ text: String) { NSPasteboard.general.clearContents() NSPasteboard.general.setString(text, forType: .string) uploadStatus = "URL copied to clipboard" } private func restoreWatching() { guard !apiKey.isEmpty else { return } var validPaths: [String] = [] for path in watchedFolders { guard FileManager.default.fileExists(atPath: path), let bookmarkData = UserDefaults.standard.data(forKey: "FolderBookmark-\(path)") else { continue } var isStale = false do { let url = try URL(resolvingBookmarkData: bookmarkData, options: .withSecurityScope, relativeTo: nil, bookmarkDataIsStale: &isStale) guard url.startAccessingSecurityScopedResource() else { print("Failed to access security scoped resource for path: \(path)") continue } validPaths.append(url.path) } catch { print("Failed to resolve bookmark for path: \(path), error: \(error)") } } if !validPaths.isEmpty { DispatchQueue.main.async { watchedFolders = validPaths folderMonitor.startWatching(paths: validPaths) uploadStatus = "Watching for new files..." NotificationManager.send(title: "Ventry Upload Watcher", message: "Resumed watching \(validPaths.count) folder(s)") } } } } struct MainLayout: View { let content: Content @Environment(\.colorScheme) var colorScheme init(@ViewBuilder content: () -> Content) { self.content = content() } var body: some View { ZStack { Color(colorScheme == .dark ? .windowBackgroundColor : .white) .ignoresSafeArea() content } } } #Preview { ContentView() } extension URL { var lastPathModified: Date { (try? resourceValues(forKeys: [.contentModificationDateKey]))?.contentModificationDate ?? Date.distantPast } } class AppDelegate: NSObject, NSApplicationDelegate { var statusItem: NSStatusItem? func applicationDidFinishLaunching(_ notification: Notification) { setupStatusItem() if let window = NSApplication.shared.windows.first { WindowManager.shared.setWindow(window) } } private func setupStatusItem() { statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem?.button { let icon = NSImage(named: "ventry-logo-white") icon?.isTemplate = true icon?.size = NSSize(width: 18, height: 18) button.image = icon let menu = NSMenu() menu.addItem(NSMenuItem(title: "Show Window", action: #selector(showWindow), keyEquivalent: "")) menu.addItem(NSMenuItem.separator()) menu.addItem(NSMenuItem(title: "Quit", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")) statusItem?.menu = menu } } @objc private func showWindow() { WindowManager.shared.showWindow() } }