Repository: nuance-dev/achico Branch: master Commit: 85a726791483 Files: 35 Total size: 119.2 KB Directory structure: gitextract_q763oxjx/ ├── Achico/ │ ├── Achico.entitlements │ ├── App/ │ │ ├── AchicoApp.swift │ │ └── AppDelegate.swift │ ├── Info.plist │ ├── Preview Content/ │ │ └── Preview Assets.xcassets/ │ │ ├── AppIcon-1024.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-128.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-16.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-256.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-32.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-512.imageset/ │ │ │ └── Contents.json │ │ ├── AppIcon-64.imageset/ │ │ │ └── Contents.json │ │ └── Contents.json │ ├── Processor/ │ │ ├── CacheManager.swift │ │ ├── FileProcessor.swift │ │ └── VideoProcessor.swift │ ├── Resources/ │ │ ├── Assets.xcassets/ │ │ │ ├── AccentColor.colorset/ │ │ │ │ └── Contents.json │ │ │ ├── AppIcon.appiconset/ │ │ │ │ └── Contents.json │ │ │ └── Contents.json │ │ ├── CompressionSettings.swift │ │ ├── FileDropDelegate.swift │ │ ├── MenuBarController.swift │ │ └── UpdateChecker.swift │ ├── UI Components/ │ │ ├── ButtonGroup.swift │ │ ├── GlassButtonStyle.swift │ │ ├── TitleBarAccessory.swift │ │ ├── VisualEffectBlur.swift │ │ └── WindowAccessor.swift │ └── Views/ │ ├── ContentView.swift │ ├── DropZoneView.swift │ ├── MenuBarView.swift │ ├── MultiFileProcessingView.swift │ └── ResultView.swift ├── Achico.xcodeproj/ │ └── project.pbxproj ├── LICENSE └── README.md ================================================ FILE CONTENTS ================================================ ================================================ FILE: Achico/Achico.entitlements ================================================ com.apple.security.app-sandbox com.apple.security.files.user-selected.read-only com.apple.security.files.user-selected.read-write com.apple.security.network.client ================================================ FILE: Achico/App/AchicoApp.swift ================================================ import SwiftUI import AppKit @main struct AchicoApp: App { @NSApplicationDelegateAdaptor(AppDelegate.self) var appDelegate @AppStorage("isDarkMode") private var isDarkMode = false @StateObject private var menuBarController = MenuBarController() @State private var showingUpdateSheet = false var body: some Scene { WindowGroup { ContentView() .frame(minWidth: 400, minHeight: 500) .preferredColorScheme(isDarkMode ? .dark : .light) .background(WindowAccessor()) .environmentObject(menuBarController) .sheet(isPresented: $showingUpdateSheet) { MenuBarView(updater: menuBarController.updater) .environmentObject(menuBarController) } .onAppear { // Check for updates when app launches menuBarController.updater.checkForUpdates() // Set up observer for update availability menuBarController.updater.onUpdateAvailable = { showingUpdateSheet = true } } } .windowStyle(.hiddenTitleBar) .commands { CommandGroup(after: .appInfo) { Button("Check for Updates...") { showingUpdateSheet = true menuBarController.updater.checkForUpdates() } .keyboardShortcut("U", modifiers: [.command]) if menuBarController.updater.updateAvailable { Button("Download Update") { if let url = menuBarController.updater.downloadURL { NSWorkspace.shared.open(url) } } } Divider() } } } } ================================================ FILE: Achico/App/AppDelegate.swift ================================================ import Cocoa class AppDelegate: NSObject, NSApplicationDelegate { func applicationWillTerminate(_ notification: Notification) { CacheManager.shared.cleanupOldFiles() } } ================================================ FILE: Achico/Info.plist ================================================ ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-1024.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-1024.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-128.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-128.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-16.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-16.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-256.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-256.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-32.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-32.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-512.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-512.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/AppIcon-64.imageset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-64.png", "idiom" : "universal", "scale" : "1x" }, { "idiom" : "universal", "scale" : "2x" }, { "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Preview Content/Preview Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Processor/CacheManager.swift ================================================ // CacheManager.swift import Foundation import AppKit class CacheManager { static let shared = CacheManager() private let cacheDirectory: URL private let maxCacheAge: TimeInterval = 24 * 60 * 60 // 24 hours private init() { let cacheDir = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask).first! cacheDirectory = cacheDir.appendingPathComponent("com.achico.filecache", isDirectory: true) do { try FileManager.default.createDirectory(at: cacheDirectory, withIntermediateDirectories: true) } catch { print("Failed to create cache directory: \(error)") } // Setup automatic cleanup setupAutomaticCleanup() } func createTemporaryURL(for filename: String) throws -> URL { // Clean the filename let cleanFilename = filename.components(separatedBy: "_").last ?? filename let tempFilename = "\(UUID().uuidString).\(cleanFilename)" let fileURL = cacheDirectory.appendingPathComponent(tempFilename) // Check if file already exists and remove it if FileManager.default.fileExists(atPath: fileURL.path) { try FileManager.default.removeItem(at: fileURL) } return fileURL } func cleanupOldFiles() { let fileManager = FileManager.default let resourceKeys: [URLResourceKey] = [.creationDateKey, .isDirectoryKey] guard let enumerator = fileManager.enumerator( at: cacheDirectory, includingPropertiesForKeys: resourceKeys, options: .skipsHiddenFiles ) else { return } let cutoffDate = Date().addingTimeInterval(-maxCacheAge) while let fileURL = enumerator.nextObject() as? URL { do { let resourceValues = try fileURL.resourceValues(forKeys: Set(resourceKeys)) if let creationDate = resourceValues.creationDate, let isDirectory = resourceValues.isDirectory, !isDirectory && creationDate < cutoffDate { try fileManager.removeItem(at: fileURL) } } catch { print("Error cleaning up file at \(fileURL): \(error)") } } } private func setupAutomaticCleanup() { // Clean up on app launch cleanupOldFiles() // Register for app termination notification NotificationCenter.default.addObserver( forName: NSApplication.willTerminateNotification, object: nil, queue: .main ) { [weak self] _ in self?.cleanupOldFiles() } } } ================================================ FILE: Achico/Processor/FileProcessor.swift ================================================ import Foundation import PDFKit import UniformTypeIdentifiers import AppKit import CoreGraphics import AVFoundation enum CompressionError: LocalizedError { case unsupportedFormat case conversionFailed case compressionFailed case invalidInput case videoProcessingFailed var errorDescription: String? { switch self { case .unsupportedFormat: return "This file format is not supported" case .conversionFailed: return "Failed to convert the file" case .compressionFailed: return "Failed to compress the file" case .invalidInput: return "The input file is invalid or corrupted" case .videoProcessingFailed: return "Failed to process video file" } } } class FileProcessor: ObservableObject { // MARK: - Published Properties @Published var isProcessing = false @Published var progress: Double = 0 @Published var processingResult: ProcessingResult? // MARK: - Private Properties private let processingQueue = DispatchQueue(label: "com.achico.fileprocessing", qos: .userInitiated) private let cacheManager = CacheManager.shared private let videoProcessor = VideoProcessor() struct ProcessingResult { let originalSize: Int64 let compressedSize: Int64 let compressedURL: URL let fileName: String let originalFileName: String var savedPercentage: Int { guard originalSize > 0 else { return 0 } let percentage = Int(((Double(originalSize) - Double(compressedSize)) / Double(originalSize)) * 100) return max(0, percentage) } var suggestedFileName: String { // Remove UUID from filename if present let cleanFilename = originalFileName .replacingOccurrences(of: #"[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}\."#, with: "", options: .regularExpression) let fileURL = URL(fileURLWithPath: cleanFilename) let filenameWithoutExt = fileURL.deletingPathExtension().lastPathComponent let fileExtension = compressedURL.pathExtension return "\(filenameWithoutExt)_compressed.\(fileExtension)" } } private func processAudio(from url: URL, to tempURL: URL, settings: CompressionSettings) async throws { let asset = AVURLAsset(url: url) let fileType = FileType(url: url) // Create a composition to work with the audio let composition = AVMutableComposition() // Try to get the audio track from the asset guard let audioTrack = try? await asset.loadTracks(withMediaType: .audio).first, let compositionTrack = composition.addMutableTrack( withMediaType: .audio, preferredTrackID: kCMPersistentTrackID_Invalid) else { throw CompressionError.compressionFailed } // Get asset duration using the new API let duration = try await asset.load(.duration) // Insert the audio track into the composition try compositionTrack.insertTimeRange( CMTimeRange(start: .zero, duration: duration), of: audioTrack, at: .zero ) // Determine preset and output file type let (presetName, outputFileType): (String, AVFileType) = { switch fileType { case .mp3, .wav, .aiff, .m4a: return (AVAssetExportPresetAppleM4A, .m4a) default: return (AVAssetExportPresetAppleM4A, .m4a) } }() // Create export session with the composition guard let exportSession = AVAssetExportSession( asset: composition, presetName: presetName ) else { throw CompressionError.compressionFailed } // Create output URL with correct extension let outputURL = tempURL.deletingPathExtension().appendingPathExtension("m4a") // Remove any existing file at the output URL if FileManager.default.fileExists(atPath: outputURL.path) { try? FileManager.default.removeItem(at: outputURL) } // Ensure the output directory exists try FileManager.default.createDirectory( at: outputURL.deletingLastPathComponent(), withIntermediateDirectories: true, attributes: nil ) // Configure export session exportSession.outputURL = outputURL exportSession.outputFileType = outputFileType // Monitor progress in a separate task let progressTask = Task { while !Task.isCancelled && (exportSession.status == .waiting || exportSession.status == .exporting) { await MainActor.run { self.progress = Double(exportSession.progress) } try? await Task.sleep(nanoseconds: 100_000_000) // Sleep for 0.1 seconds } } // Start exporting and await completion try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in exportSession.exportAsynchronously { switch exportSession.status { case .completed: // Verify the file exists after export if FileManager.default.fileExists(atPath: outputURL.path) { continuation.resume() } else { print("Export completed but file not found at: \(outputURL.path)") continuation.resume(throwing: CompressionError.compressionFailed) } case .failed: if let error = exportSession.error { print("Export Session Failed: \(error.localizedDescription)") print("Error Details: \(String(describing: error))") continuation.resume(throwing: error) } else { print("Export Session Failed without error details") continuation.resume(throwing: CompressionError.compressionFailed) } case .cancelled: print("Export Session Cancelled") continuation.resume(throwing: CompressionError.compressionFailed) default: print("Export Session ended with status: \(exportSession.status.rawValue)") continuation.resume(throwing: CompressionError.compressionFailed) } } } // Cancel the progress monitoring task progressTask.cancel() // Verify the file exists one final time guard FileManager.default.fileExists(atPath: outputURL.path) else { throw CompressionError.compressionFailed } // Make sure we're returning the correct path if outputURL != tempURL { try? FileManager.default.removeItem(at: tempURL) try FileManager.default.moveItem(at: outputURL, to: tempURL) } await MainActor.run { self.progress = 1.0 } } // MARK: - Lifecycle deinit { cleanup() } // MARK: - Public Methods @MainActor func processFile(url: URL, settings: CompressionSettings? = nil, originalFileName: String? = nil) async throws { isProcessing = true progress = 0 processingResult = nil do { let result = try await processInBackground( url: url, settings: settings, originalFileName: originalFileName ?? url.lastPathComponent ) self.processingResult = result } catch { isProcessing = false throw error } isProcessing = false progress = 1.0 } func cleanup() { processingResult = nil cacheManager.cleanupOldFiles() } // MARK: - Private Methods - Main Processing private func processInBackground(url: URL, settings: CompressionSettings? = nil, originalFileName: String) async throws -> ProcessingResult { let originalSize = try FileManager.default.attributesOfItem(atPath: url.path)[.size] as? Int64 ?? 0 let originalExtension = url.pathExtension let tempURL = try cacheManager.createTemporaryURL(for: originalFileName) let compressionSettings = settings ?? CompressionSettings() let fileType = FileType(url: url) if fileType.isVideo { try await processVideo(from: url, to: tempURL, settings: compressionSettings) } else if fileType.isAudio { try await processAudio(from: url, to: tempURL, settings: compressionSettings) } else { switch url.pathExtension.lowercased() { case "pdf": try processPDF(from: url, to: tempURL) case "jpg", "jpeg": try processJPEG(from: url, to: tempURL, settings: compressionSettings) case "png": try processPNG(from: url, to: tempURL, settings: compressionSettings) case "heic": try processHEIC(from: url, to: tempURL, settings: compressionSettings) case "tiff", "tif": try processTIFF(from: url, to: tempURL, settings: compressionSettings) case "gif": try processGIF(from: url, to: tempURL) case "bmp": try processBMP(from: url, to: tempURL, settings: compressionSettings) case "webp": try processWebP(from: url, to: tempURL, settings: compressionSettings) case "svg": try processSVG(from: url, to: tempURL, settings: compressionSettings) case "raw", "cr2", "nef", "arw": try processRAW(from: url, to: tempURL, settings: compressionSettings) case "ico": try processICO(from: url, to: tempURL, settings: compressionSettings) default: throw CompressionError.unsupportedFormat } } let compressedSize = try FileManager.default.attributesOfItem(atPath: tempURL.path)[.size] as? Int64 ?? 0 print("Debug - Creating ProcessingResult:") print("Debug - Temp URL last component: \(tempURL.lastPathComponent)") print("Debug - Original filename being used: \(originalFileName)") return ProcessingResult( originalSize: originalSize, compressedSize: compressedSize, compressedURL: tempURL, fileName: tempURL.lastPathComponent, originalFileName: originalFileName ) } private func processVideo(from url: URL, to tempURL: URL, settings: CompressionSettings) async throws { let videoSettings = VideoProcessor.VideoCompressionSettings( quality: Float(settings.quality), maxWidth: settings.maxDimension != nil ? Int(settings.maxDimension!) : nil, bitrateMultiplier: 0.7, frameRate: 30, audioEnabled: true ) do { try await videoProcessor.compressVideo( inputURL: url, outputURL: tempURL, settings: videoSettings ) { [weak self] progress in Task { @MainActor in self?.progress = Double(progress) } } } catch { throw CompressionError.videoProcessingFailed } } private func processICO(from url: URL, to tempURL: URL, settings: CompressionSettings) throws { guard let image = NSImage(contentsOf: url) else { throw CompressionError.conversionFailed } guard let compressedData = compressImage(image, format: .png, settings: settings) else { throw CompressionError.compressionFailed } let pngURL = tempURL.deletingPathExtension().appendingPathExtension("png") try compressedData.write(to: pngURL) } private func processSVG(from url: URL, to tempURL: URL, settings: CompressionSettings) throws { guard let data = try? Data(contentsOf: url), let svgString = String(data: data, encoding: .utf8) else { throw CompressionError.conversionFailed } let cleanedSVG = svgString .replacingOccurrences(of: "", with: "", options: .regularExpression) .replacingOccurrences(of: "\\s+", with: " ", options: .regularExpression) .replacingOccurrences(of: "> <", with: "><") .trimmingCharacters(in: .whitespacesAndNewlines) try cleanedSVG.data(using: .utf8)?.write(to: tempURL) } private func processRAW(from url: URL, to tempURL: URL, settings: CompressionSettings) throws { guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else { throw CompressionError.conversionFailed } let options: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: settings.maxDimension ?? 2048 ] guard let cgImage = CGImageSourceCreateThumbnailAtIndex(imageSource, 0, options as CFDictionary) else { throw CompressionError.conversionFailed } let image = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width, height: cgImage.height)) guard let compressedData = compressImage(image, format: .jpeg, settings: settings) else { throw CompressionError.compressionFailed } let jpegURL = tempURL.deletingPathExtension().appendingPathExtension("jpg") try compressedData.write(to: jpegURL) } // MARK: - Private Methods - Format-Specific Processing private func processPDF(from url: URL, to tempURL: URL) throws { guard let document = PDFDocument(url: url) else { throw CompressionError.conversionFailed } let newDocument = PDFDocument() let totalPages = document.pageCount for i in 0.. 1 { try compressAnimatedGIF(imageSource, frameCount: frameCount, to: tempURL) } else { guard let image = NSImage(contentsOf: url) else { throw CompressionError.conversionFailed } let settings = CompressionSettings( pngCompressionLevel: 6, preserveMetadata: true, maxDimension: 2048, optimizeForWeb: true ) guard let compressedData = compressImage(image, format: .png, settings: settings) else { throw CompressionError.compressionFailed } let pngURL = tempURL.deletingPathExtension().appendingPathExtension("png") try compressedData.write(to: pngURL) } } private func processBMP(from url: URL, to tempURL: URL, settings: CompressionSettings) throws { guard let image = NSImage(contentsOf: url) else { throw CompressionError.conversionFailed } guard let compressedData = compressImage(image, format: .png, settings: settings) else { throw CompressionError.compressionFailed } let pngURL = tempURL.deletingPathExtension().appendingPathExtension("png") try compressedData.write(to: pngURL) } private func processWebP(from url: URL, to tempURL: URL, settings: CompressionSettings) throws { guard let image = NSImage(contentsOf: url) else { throw CompressionError.conversionFailed } let hasAlpha = imageHasAlpha(image) let format: NSBitmapImageRep.FileType = hasAlpha ? .png : .jpeg guard let compressedData = compressImage(image, format: format, settings: settings) else { throw CompressionError.compressionFailed } let newExt = hasAlpha ? "png" : "jpg" let newURL = tempURL.deletingPathExtension().appendingPathExtension(newExt) try compressedData.write(to: newURL) } // MARK: - Private Methods - Helper Functions private func compressPDFPage(_ page: PDFPage) throws -> PDFPage? { let pageRect = page.bounds(for: .mediaBox) let image = NSImage(size: pageRect.size) image.lockFocus() if let context = NSGraphicsContext.current?.cgContext { page.draw(with: .mediaBox, to: context) } image.unlockFocus() guard let tiffData = image.tiffRepresentation, let bitmap = NSBitmapImageRep(data: tiffData), let compressedData = bitmap.representation( using: .jpeg, properties: [.compressionFactor: 0.5] ), let compressedImage = NSImage(data: compressedData) else { return nil } return PDFPage(image: compressedImage) } private func compressImage(_ image: NSImage, format: NSBitmapImageRep.FileType, settings: CompressionSettings) -> Data? { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return nil } let processedCGImage: CGImage if let maxDimension = settings.maxDimension { if let resized = resizeImage(cgImage, maxDimension: maxDimension) { processedCGImage = resized } else { processedCGImage = cgImage } } else { processedCGImage = cgImage } let bitmapRep = NSBitmapImageRep(cgImage: processedCGImage) var compressionProperties: [NSBitmapImageRep.PropertyKey: Any] = [:] switch format { case .jpeg: compressionProperties[.compressionFactor] = settings.quality case .png: compressionProperties[.compressionFactor] = 1.0 default: break } return bitmapRep.representation(using: format, properties: compressionProperties) } private func imageHasAlpha(_ image: NSImage) -> Bool { guard let cgImage = image.cgImage(forProposedRect: nil, context: nil, hints: nil) else { return false } let alphaInfo = cgImage.alphaInfo return alphaInfo != .none && alphaInfo != .noneSkipLast && alphaInfo != .noneSkipFirst } private func resizeImage(_ cgImage: CGImage, maxDimension: CGFloat) -> CGImage? { let currentWidth = CGFloat(cgImage.width) let currentHeight = CGFloat(cgImage.height) // Calculate scale factor to maintain aspect ratio let scaleFactor = min(maxDimension / currentWidth, maxDimension / currentHeight) // Only resize if the image is larger than maxDimension if scaleFactor >= 1.0 { return cgImage } let newWidth = Int(currentWidth * scaleFactor) let newHeight = Int(currentHeight * scaleFactor) // Create a bitmap context with the proper color space and bitmap info let bitmapInfo: UInt32 if cgImage.alphaInfo == .none { bitmapInfo = CGImageAlphaInfo.noneSkipLast.rawValue } else { bitmapInfo = CGImageAlphaInfo.premultipliedLast.rawValue } guard let context = CGContext( data: nil, width: newWidth, height: newHeight, bitsPerComponent: 8, bytesPerRow: 0, space: CGColorSpace(name: CGColorSpace.sRGB)!, bitmapInfo: bitmapInfo ) else { print("Debug - Failed to create context") return nil } // Set high quality image interpolation context.interpolationQuality = .high // Draw the image in the new size let newRect = CGRect(x: 0, y: 0, width: newWidth, height: newHeight) context.draw(cgImage, in: newRect) // Get the resized image guard let resizedImage = context.makeImage() else { return nil } return resizedImage } private func compressAnimatedGIF(_ imageSource: CGImageSource, frameCount: Int, to url: URL) throws { guard let destination = CGImageDestinationCreateWithURL( url as CFURL, UTType.gif.identifier as CFString, frameCount, nil ) else { throw CompressionError.compressionFailed } if let properties = CGImageSourceCopyProperties(imageSource, nil) { CGImageDestinationSetProperties(destination, properties) } let options: [CFString: Any] = [ kCGImageSourceCreateThumbnailFromImageAlways: true, kCGImageSourceCreateThumbnailWithTransform: true, kCGImageSourceThumbnailMaxPixelSize: 1024 ] for i in 0.. // Get delay time for the frame let defaultDelay = 0.1 let delayTime: Double if let delay = gifProperties?[kCGImagePropertyGIFDelayTime] as? Double { delayTime = delay } else { delayTime = defaultDelay } // Create optimized frame guard let frameImage = CGImageSourceCreateImageAtIndex(imageSource, i, options as CFDictionary) else { return } // Prepare frame properties for destination let destFrameProperties: [CFString: Any] = [ kCGImagePropertyGIFDictionary: [ kCGImagePropertyGIFDelayTime: delayTime ] ] CGImageDestinationAddImage(destination, frameImage, destFrameProperties as CFDictionary) } DispatchQueue.main.async { self.progress = Double(i + 1) / Double(frameCount) } } if !CGImageDestinationFinalize(destination) { throw CompressionError.compressionFailed } } // MARK: - Metadata Handling private func extractMetadata(from imageSource: CGImageSource) -> [CFString: Any]? { guard let properties = CGImageSourceCopyProperties(imageSource, nil) as? [CFString: Any] else { return nil } var metadata: [CFString: Any] = [:] // Extract relevant metadata while excluding unnecessary data let keysToPreserve: [CFString] = [ kCGImagePropertyOrientation, kCGImagePropertyDPIHeight, kCGImagePropertyDPIWidth, kCGImagePropertyPixelHeight, kCGImagePropertyPixelWidth, kCGImagePropertyProfileName ] for key in keysToPreserve { if let value = properties[key] { metadata[key] = value } } return metadata } // MARK: - Quality and Optimization private func determineOptimalSettings(for image: NSImage, format: NSBitmapImageRep.FileType) -> CompressionSettings { let size = image.size let totalPixels = size.width * size.height // Base settings var settings = CompressionSettings() // Adjust quality based on image size if totalPixels > 4_000_000 { // 2000x2000 pixels settings.maxDimension = 2048 settings.quality = 0.7 } else if totalPixels > 1_000_000 { // 1000x1000 pixels settings.maxDimension = 1500 settings.quality = 0.8 } else { settings.maxDimension = nil settings.quality = 0.9 } // Format-specific adjustments switch format { case .jpeg: // JPEG-specific optimizations if totalPixels > 8_000_000 { settings.quality = 0.6 } settings.preserveMetadata = true case .png: // PNG-specific optimizations if imageHasAlpha(image) { settings.pngCompressionLevel = 7 } else { settings.pngCompressionLevel = 9 } settings.preserveMetadata = true default: settings.preserveMetadata = false } return settings } // MARK: - Progress Tracking private func updateProgress(_ progress: Double) { DispatchQueue.main.async { self.progress = min(max(progress, 0), 1) } } } // MARK: - Extensions extension FileProcessor { enum FileType { case pdf case jpeg case png case heic case gif case tiff case bmp case webp case svg case raw case ico case mp4 case mov case avi case mpeg2 case quickTime case mp3 case wav case m4a case aiff case unknown init(url: URL) { switch url.pathExtension.lowercased() { case "pdf": self = .pdf case "jpg", "jpeg": self = .jpeg case "png": self = .png case "heic": self = .heic case "gif": self = .gif case "tiff", "tif": self = .tiff case "bmp": self = .bmp case "webp": self = .webp case "svg": self = .svg case "raw", "cr2", "nef", "arw": self = .raw case "ico": self = .ico case "mp4": self = .mp4 case "mov": self = .mov case "avi": self = .avi case "mpg", "mpeg": self = .mpeg2 case "qt": self = .quickTime case "mp3": self = .mp3 case "wav": self = .wav case "m4a": self = .m4a case "aiff", "aif": self = .aiff default: self = .unknown } } var isAudio: Bool { switch self { case .mp3, .wav, .m4a, .aiff: return true default: return false } } var isVideo: Bool { switch self { case .mp4, .mov, .avi, .mpeg2, .quickTime: return true default: return false } } var defaultOutputExtension: String { switch self { case .pdf: return "pdf" case .jpeg: return "jpg" case .png: return "png" case .heic: return "jpg" case .gif: return "gif" case .tiff: return "jpg" case .bmp: return "png" case .webp: return "jpg" case .svg: return "svg" case .raw: return "jpg" case .ico: return "png" case .mp4, .mov, .avi, .mpeg2, .quickTime: return "mp4" case .mp3: return "mp3" case .wav: return "wav" case .m4a: return "m4a" case .aiff: return "aiff" case .unknown: return "" } } } } ================================================ FILE: Achico/Processor/VideoProcessor.swift ================================================ import Foundation import AVFoundation @available(macOS 12.0, *) actor VideoProcessor { enum VideoError: LocalizedError { case exportFailed case invalidInput case compressionFailed var errorDescription: String? { switch self { case .exportFailed: return "Failed to export video" case .invalidInput: return "Invalid input video" case .compressionFailed: return "Video compression failed" } } } struct VideoCompressionSettings: Sendable { let quality: Float let maxWidth: Int? let bitrateMultiplier: Float let frameRate: Int? let audioEnabled: Bool init( quality: Float = 0.7, maxWidth: Int? = nil, bitrateMultiplier: Float = 0.7, frameRate: Int? = 30, audioEnabled: Bool = true ) { self.quality = quality self.maxWidth = maxWidth self.bitrateMultiplier = bitrateMultiplier self.frameRate = frameRate self.audioEnabled = audioEnabled } } func compressVideo( inputURL: URL, outputURL: URL, settings: VideoCompressionSettings, progressHandler: @Sendable @escaping (Float) -> Void ) async throws { let asset = AVAsset(url: inputURL) // Get video track guard let videoTrack = try? await asset.loadTracks(withMediaType: .video).first else { throw VideoError.invalidInput } // Get original dimensions and apply size limit if needed let originalSize = try await videoTrack.load(.naturalSize) var targetSize = originalSize if let maxWidth = settings.maxWidth { let scale = Float(maxWidth) / Float(originalSize.width) if scale < 1.0 { targetSize = CGSize( width: CGFloat(maxWidth), height: CGFloat(Float(originalSize.height) * scale) ) } } // Get original frame rate let nominalFrameRate = try await videoTrack.load(.nominalFrameRate) let targetFrameRate = Float(settings.frameRate ?? Int(nominalFrameRate)) // Create export session guard let exportSession = AVAssetExportSession( asset: asset, presetName: AVAssetExportPresetHighestQuality ) else { throw VideoError.exportFailed } // Calculate bitrate let originalBitrate = try await estimateBitrate(for: videoTrack) let targetBitrate = Int(Float(originalBitrate) * settings.bitrateMultiplier) // Configure compression let compressionProperties: [String: Any] = [ AVVideoAverageBitRateKey: targetBitrate, AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel, AVVideoH264EntropyModeKey: AVVideoH264EntropyModeCABAC ] // Video settings let videoSettings: [String: Any] = [ AVVideoCodecKey: AVVideoCodecType.h264, AVVideoWidthKey: Int(targetSize.width), AVVideoHeightKey: Int(targetSize.height), AVVideoCompressionPropertiesKey: compressionProperties ] // Create and configure video composition let composition = AVMutableVideoComposition() composition.renderSize = targetSize composition.frameDuration = CMTime(value: 1, timescale: CMTimeScale(targetFrameRate)) let instruction = AVMutableVideoCompositionInstruction() instruction.timeRange = CMTimeRange( start: .zero, duration: try await asset.load(.duration) ) let layerInstruction = AVMutableVideoCompositionLayerInstruction(assetTrack: videoTrack) // Calculate transform for proper resizing let originalTransform = try await videoTrack.load(.preferredTransform) var finalTransform = originalTransform let scaleX = targetSize.width / originalSize.width let scaleY = targetSize.height / originalSize.height // Apply scaling transform finalTransform = finalTransform.concatenating(CGAffineTransform(scaleX: scaleX, y: scaleY)) layerInstruction.setTransform(finalTransform, at: .zero) instruction.layerInstructions = [layerInstruction] composition.instructions = [instruction] // Configure export session exportSession.videoComposition = composition exportSession.outputURL = outputURL exportSession.outputFileType = .mp4 // Use Task for progress monitoring let progressTask = Task { @MainActor in repeat { progressHandler(exportSession.progress) try await Task.sleep(nanoseconds: 100_000_000) // 0.1 second } while exportSession.status == .exporting } // Start export and wait for completion try await withCheckedThrowingContinuation { continuation in exportSession.exportAsynchronously { progressTask.cancel() // Stop progress monitoring switch exportSession.status { case .completed: continuation.resume() case .failed: let error = exportSession.error ?? VideoError.exportFailed continuation.resume(throwing: error) default: continuation.resume(throwing: VideoError.compressionFailed) } } } } private func estimateBitrate(for videoTrack: AVAssetTrack) async throws -> Int { let duration = try await videoTrack.load(.timeRange).duration.seconds let size = try await videoTrack.load(.totalSampleDataLength) return Int(Double(size) * 8 / duration) // bits per second } } ================================================ FILE: Achico/Resources/Assets.xcassets/AccentColor.colorset/Contents.json ================================================ { "colors" : [ { "color" : { "color-space" : "display-p3", "components" : { "alpha" : "1.000", "blue" : "0.850", "green" : "0.470", "red" : "0.250" } }, "idiom" : "universal" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json ================================================ { "images" : [ { "filename" : "AppIcon-16.png", "idiom" : "mac", "scale" : "1x", "size" : "16x16" }, { "filename" : "AppIcon-32 1.png", "idiom" : "mac", "scale" : "2x", "size" : "16x16" }, { "filename" : "AppIcon-32.png", "idiom" : "mac", "scale" : "1x", "size" : "32x32" }, { "filename" : "AppIcon-64.png", "idiom" : "mac", "scale" : "2x", "size" : "32x32" }, { "filename" : "AppIcon-128.png", "idiom" : "mac", "scale" : "1x", "size" : "128x128" }, { "filename" : "AppIcon-256 1.png", "idiom" : "mac", "scale" : "2x", "size" : "128x128" }, { "filename" : "AppIcon-256.png", "idiom" : "mac", "scale" : "1x", "size" : "256x256" }, { "filename" : "AppIcon-512 1.png", "idiom" : "mac", "scale" : "2x", "size" : "256x256" }, { "filename" : "AppIcon-512.png", "idiom" : "mac", "scale" : "1x", "size" : "512x512" }, { "filename" : "AppIcon-1024.png", "idiom" : "mac", "scale" : "2x", "size" : "512x512" } ], "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Resources/Assets.xcassets/Contents.json ================================================ { "info" : { "author" : "xcode", "version" : 1 } } ================================================ FILE: Achico/Resources/CompressionSettings.swift ================================================ import Foundation import CoreGraphics public struct CompressionSettings { var quality: CGFloat = 0.7 // JPEG/HEIC quality (0.0-1.0) var pngCompressionLevel: Int = 6 // PNG compression (0-9) var preserveMetadata: Bool = false var maxDimension: CGFloat? = nil // Downsample if larger var optimizeForWeb: Bool = true // Additional optimizations for web use var audioBitRate: Int? // In bits per second var audioSampleRate: Double? // In Hertz public init( quality: CGFloat = 0.7, pngCompressionLevel: Int = 6, preserveMetadata: Bool = false, maxDimension: CGFloat? = nil, optimizeForWeb: Bool = true ) { self.quality = quality self.pngCompressionLevel = pngCompressionLevel self.preserveMetadata = preserveMetadata self.maxDimension = maxDimension self.optimizeForWeb = optimizeForWeb } } ================================================ FILE: Achico/Resources/FileDropDelegate.swift ================================================ import SwiftUI import UniformTypeIdentifiers struct FileDropDelegate: DropDelegate { @Binding var isDragging: Bool let supportedTypes: [UTType] let handleDrop: ([NSItemProvider]) -> Void func validateDrop(info: DropInfo) -> Bool { return info.hasItemsConforming(to: supportedTypes.map(\.identifier)) } func dropEntered(info: DropInfo) { isDragging = true } func dropExited(info: DropInfo) { isDragging = false } func performDrop(info: DropInfo) -> Bool { isDragging = false let providers = info.itemProviders(for: supportedTypes.map(\.identifier)) handleDrop(providers) return true } } ================================================ FILE: Achico/Resources/MenuBarController.swift ================================================ import SwiftUI import AppKit class MenuBarController: NSObject, ObservableObject { @Published private(set) var updater = UpdateChecker() private var statusItem: NSStatusItem! override init() { super.init() // Initialize status item on main queue DispatchQueue.main.async { self.setupMenuBar() self.updater.checkForUpdates() } } private func setupMenuBar() { // Create the status item statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) if let button = statusItem.button { button.image = NSImage(systemSymbolName: "checkmark.circle", accessibilityDescription: "Update Status") // Create the menu let menu = NSMenu() menu.delegate = self // Set the menu statusItem.menu = menu // Update the button image when the status changes updater.onStatusChange = { [weak self] newIcon in guard self != nil else { return } DispatchQueue.main.async { button.image = NSImage(systemSymbolName: newIcon, accessibilityDescription: "Update Status") } } } } @objc private func checkForUpdates() { updater.checkForUpdates() } @objc private func downloadUpdate() { if let url = updater.downloadURL { NSWorkspace.shared.open(url) } } } extension MenuBarController: NSMenuDelegate { func menuWillOpen(_ menu: NSMenu) { // Clear existing items menu.removeAllItems() // Add version let versionItem = NSMenuItem(title: "Achico v\(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0")", action: nil, keyEquivalent: "") versionItem.isEnabled = false menu.addItem(versionItem) menu.addItem(NSMenuItem.separator()) // Add status if updater.isChecking { let checkingItem = NSMenuItem(title: "Checking for updates...", action: nil, keyEquivalent: "") checkingItem.isEnabled = false menu.addItem(checkingItem) } else if updater.updateAvailable { if let version = updater.latestVersion { let availableItem = NSMenuItem(title: "Version \(version) Available", action: nil, keyEquivalent: "") availableItem.isEnabled = false menu.addItem(availableItem) } let downloadItem = NSMenuItem(title: "Download Update", action: #selector(downloadUpdate), keyEquivalent: "") downloadItem.target = self menu.addItem(downloadItem) } else { let upToDateItem = NSMenuItem(title: "App is up to date", action: nil, keyEquivalent: "") upToDateItem.isEnabled = false menu.addItem(upToDateItem) } menu.addItem(NSMenuItem.separator()) // Add check for updates item let checkItem = NSMenuItem(title: "Check for Updates...", action: #selector(checkForUpdates), keyEquivalent: "u") checkItem.target = self menu.addItem(checkItem) } func menuDidClose(_ menu: NSMenu) { // Optional: Handle menu closing } func numberOfItems(in menu: NSMenu) -> Int { // Let the menu build dynamically return menu.numberOfItems } } ================================================ FILE: Achico/Resources/UpdateChecker.swift ================================================ import Foundation struct GitHubRelease: Codable { let tagName: String let name: String let body: String let htmlUrl: String enum CodingKeys: String, CodingKey { case tagName = "tag_name" case name case body case htmlUrl = "html_url" } } class UpdateChecker: ObservableObject { @Published var updateAvailable = false @Published var latestVersion: String? @Published var releaseNotes: String? @Published var downloadURL: URL? @Published var isChecking = false @Published var error: String? @Published var statusIcon: String = "checkmark.circle" var onStatusChange: ((String) -> Void)? var onUpdateAvailable: (() -> Void)? private let currentVersion: String private let githubRepo: String private var updateCheckTimer: Timer? init() { self.currentVersion = Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0" self.githubRepo = "nuance-dev/Achico" setupTimer() updateStatusIcon() } private func setupTimer() { // Initial check after 2 seconds DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in self?.checkForUpdates() } // Periodic check every 24 hours updateCheckTimer = Timer.scheduledTimer(withTimeInterval: 24 * 60 * 60, repeats: true) { [weak self] _ in self?.checkForUpdates() } } private func updateStatusIcon() { DispatchQueue.main.async { [weak self] in guard let self = self else { return } if self.isChecking { self.statusIcon = "arrow.triangle.2.circlepath" } else { self.statusIcon = self.updateAvailable ? "exclamationmark.circle" : "checkmark.circle" } self.onStatusChange?(self.statusIcon) } } func checkForUpdates() { print("Checking for updates...") print("Current version: \(currentVersion)") isChecking = true updateStatusIcon() error = nil let baseURL = "https://api.github.com/repos/\(githubRepo)/releases/latest" guard let url = URL(string: baseURL) else { error = "Invalid GitHub repository URL" isChecking = false updateStatusIcon() return } var request = URLRequest(url: url) request.setValue("application/vnd.github.v3+json", forHTTPHeaderField: "Accept") request.setValue("Achico-App/\(currentVersion)", forHTTPHeaderField: "User-Agent") URLSession.shared.dataTask(with: request) { [weak self] data, response, error in DispatchQueue.main.async { self?.handleUpdateResponse(data: data, response: response as? HTTPURLResponse, error: error) } }.resume() } private func handleUpdateResponse(data: Data?, response: HTTPURLResponse?, error: Error?) { defer { isChecking = false updateStatusIcon() } if let error = error { print("Network error: \(error)") self.error = "Network error: \(error.localizedDescription)" return } guard let response = response else { print("Invalid response") self.error = "Invalid response from server" return } print("Response status code: \(response.statusCode)") guard response.statusCode == 200 else { self.error = "Server error: \(response.statusCode)" return } guard let data = data else { self.error = "No data received" return } do { let decoder = JSONDecoder() let release = try decoder.decode(GitHubRelease.self, from: data) let cleanLatestVersion = release.tagName.replacingOccurrences(of: "v", with: "") print("Latest version: \(cleanLatestVersion)") print("Current version for comparison: \(currentVersion)") updateAvailable = compareVersions(current: currentVersion, latest: cleanLatestVersion) if updateAvailable { DispatchQueue.main.async { self.onUpdateAvailable?() } } latestVersion = cleanLatestVersion releaseNotes = release.body downloadURL = URL(string: release.htmlUrl) updateAvailable = compareVersions(current: currentVersion, latest: cleanLatestVersion) print("Update available: \(updateAvailable)") } catch { print("Parsing error: \(error)") self.error = "Failed to parse response: \(error.localizedDescription)" } } private func compareVersions(current: String, latest: String) -> Bool { // Clean and split versions let currentParts = current.replacingOccurrences(of: "v", with: "") .split(separator: ".") .compactMap { Int($0) } let latestParts = latest.replacingOccurrences(of: "v", with: "") .split(separator: ".") .compactMap { Int($0) } // Ensure we have at least 3 components (major.minor.patch) let paddedCurrent = currentParts + Array(repeating: 0, count: max(3 - currentParts.count, 0)) let paddedLatest = latestParts + Array(repeating: 0, count: max(3 - latestParts.count, 0)) // Compare each version component for i in 0.. paddedCurrent[i] { return true } else if paddedLatest[i] < paddedCurrent[i] { return false } } print("Versions are equal") return false } deinit { updateCheckTimer?.invalidate() } } ================================================ FILE: Achico/UI Components/ButtonGroup.swift ================================================ import SwiftUI struct ToolbarButton: View { let title: String let icon: String let action: () -> Void let isFirst: Bool let isLast: Bool var body: some View { Button(action: action) { HStack(spacing: 6) { Image(systemName: icon) .font(.system(size: 14)) Text(title) .font(.system(size: 13, weight: .medium)) } .frame(height: 36) .padding(.horizontal, 16) .foregroundColor(.primary) .background(Color.clear) .contentShape(Rectangle()) } .buttonStyle(PlainButtonStyle()) } } struct ButtonDivider: View { var body: some View { Divider() .frame(height: 24) } } struct ButtonGroup: View { let buttons: [(title: String, icon: String, action: () -> Void)] var body: some View { HStack(spacing: 0) { ForEach(Array(buttons.enumerated()), id: \.offset) { index, button in if index > 0 { ButtonDivider() } ToolbarButton( title: button.title, icon: button.icon, action: button.action, isFirst: index == 0, isLast: index == buttons.count - 1 ) } } .background(backgroundView) } private var backgroundView: some View { ZStack { // Base background RoundedRectangle(cornerRadius: 12) .fill(Color(NSColor.windowBackgroundColor).opacity(0.5)) // Subtle border RoundedRectangle(cornerRadius: 12) .strokeBorder(Color.primary.opacity(0.1), lineWidth: 1) // Glass effect overlay RoundedRectangle(cornerRadius: 12) .stroke(Color.white.opacity(0.1), lineWidth: 1) } } } ================================================ FILE: Achico/UI Components/GlassButtonStyle.swift ================================================ import SwiftUI struct GlassButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .padding(.horizontal, 20) .padding(.vertical, 10) .background( RoundedRectangle(cornerRadius: 8) .fill(Color.primary.opacity(0.1)) .overlay( RoundedRectangle(cornerRadius: 8) .stroke(Color.primary.opacity(0.2), lineWidth: 1) ) ) .opacity(configuration.isPressed ? 0.8 : 1.0) } } ================================================ FILE: Achico/UI Components/TitleBarAccessory.swift ================================================ import SwiftUI struct TitleBarAccessory: View { @AppStorage("isDarkMode") private var isDarkMode = false var body: some View { Button(action: { isDarkMode.toggle() }) { Image(systemName: isDarkMode ? "sun.max.fill" : "moon.fill") .foregroundColor(.primary) } .buttonStyle(PlainButtonStyle()) .frame(width: 30, height: 30) } } ================================================ FILE: Achico/UI Components/VisualEffectBlur.swift ================================================ import SwiftUI struct VisualEffectBlur: NSViewRepresentable { var material: NSVisualEffectView.Material var blendingMode: NSVisualEffectView.BlendingMode func makeNSView(context: Context) -> NSVisualEffectView { let view = NSVisualEffectView() view.state = .active view.material = material view.blendingMode = blendingMode view.alphaValue = 0.9 return view } func updateNSView(_ nsView: NSVisualEffectView, context: Context) {} } ================================================ FILE: Achico/UI Components/WindowAccessor.swift ================================================ import SwiftUI struct WindowAccessor: NSViewRepresentable { func makeNSView(context: Context) -> NSView { let nsView = NSView() DispatchQueue.main.async { if let window = nsView.window { let titleBarAccessory = NSTitlebarAccessoryViewController() let hostingView = NSHostingView(rootView: TitleBarAccessory()) hostingView.frame.size = hostingView.fittingSize titleBarAccessory.view = hostingView titleBarAccessory.layoutAttribute = .trailing window.addTitlebarAccessoryViewController(titleBarAccessory) } } return nsView } func updateNSView(_ nsView: NSView, context: Context) {} } ================================================ FILE: Achico/Views/ContentView.swift ================================================ import SwiftUI import UniformTypeIdentifiers import AppKit struct ContentView: View { @StateObject private var processor = FileProcessor() @StateObject private var multiProcessor = MultiFileProcessor() @State private var isDragging = false @State private var showAlert = false @State private var alertMessage = "" @State private var shouldResize = false @State private var maxDimension = "2048" let supportedTypes: [UTType] = [ .pdf, // PDF Documents .jpeg, // JPEG Images .tiff, // TIFF Images .png, // PNG Images .heic, // HEIC Images .gif, // GIF Images .bmp, // BMP Images .webP, // WebP Images .svg, // SVG Images .rawImage, // RAW Images .ico, // ICO Images .mpeg4Movie, // MP4 Video .movie, // MOV .avi, // AVI .mpeg2Video, // MPEG-2 .quickTimeMovie, // QuickTime .mpeg4Audio, // MP4 Audio .mp3, // MP3 Audio .wav, // WAV Audio .aiff, // AIFF Audio ] var body: some View { ZStack { VisualEffectBlur(material: .headerView, blendingMode: .behindWindow) .ignoresSafeArea() VStack(spacing: 20) { if processor.isProcessing { // Single file processing view VStack(spacing: 24) { // Progress Circle ZStack { Circle() .stroke(Color.secondary.opacity(0.2), lineWidth: 4) .frame(width: 60, height: 60) Circle() .trim(from: 0, to: processor.progress) .stroke(Color.accentColor, style: StrokeStyle(lineWidth: 4, lineCap: .round)) .frame(width: 60, height: 60) .rotationEffect(.degrees(-90)) Text("\(Int(processor.progress * 100))%") .font(.system(size: 14, weight: .medium)) } VStack(spacing: 8) { Text("Compressing File") .font(.system(size: 16, weight: .semibold)) Text("This may take a moment...") .font(.system(size: 14)) .foregroundColor(.secondary) } } .frame(maxWidth: 320) .padding(32) .background( RoundedRectangle(cornerRadius: 16) .fill(Color(NSColor.windowBackgroundColor)) .opacity(0.8) .shadow(color: Color.black.opacity(0.1), radius: 20, x: 0, y: 10) ) } else if let result = processor.processingResult { ResultView(result: result) { Task { await saveCompressedFile(url: result.compressedURL, originalName: result.fileName) } } onReset: { processor.cleanup() } } else if !multiProcessor.files.isEmpty { MultiFileView( processor: multiProcessor, shouldResize: $shouldResize, maxDimension: $maxDimension, supportedTypes: supportedTypes ) } else { ZStack { DropZoneView( isDragging: $isDragging, shouldResize: $shouldResize, maxDimension: $maxDimension, onTap: selectFiles ) Rectangle() .fill(Color.clear) .frame(maxWidth: .infinity, maxHeight: .infinity) .overlay(isDragging ? Color.accentColor.opacity(0.2) : Color.clear) .onDrop(of: supportedTypes, isTargeted: $isDragging) { providers in handleDrop(providers: providers) return true } } } } .padding() } .frame(minWidth: 400, minHeight: 500) .alert("Error", isPresented: $showAlert) { Button("OK", role: .cancel) { } } message: { Text(alertMessage) } } private func selectFiles() { let panel = NSOpenPanel() panel.allowedContentTypes = supportedTypes panel.allowsMultipleSelection = true if let window = NSApp.windows.first { panel.beginSheetModal(for: window) { response in if response == .OK { if panel.urls.count == 1, let url = panel.urls.first { print("📁 Selected single file: \(url.path)") print("📝 Original filename: \(url.lastPathComponent)") handleFileSelection(url: url, originalFilename: url.lastPathComponent) // Pass originalFilename } else if panel.urls.count > 1 { print("📁 Selected multiple files: \(panel.urls.map { $0.lastPathComponent })") Task { @MainActor in multiProcessor.addFiles(panel.urls) } } } } } } private func handleDrop(providers: [NSItemProvider]) { print("🔄 Handling drop with \(providers.count) providers") if providers.count == 1 { guard let provider = providers.first else { return } handleSingleFileDrop(provider: provider) } else { handleMultiFileDrop(providers: providers) } } private func handleSingleFileDrop(provider: NSItemProvider) { for type in supportedTypes { if provider.hasItemConformingToTypeIdentifier(type.identifier) { print("📥 Processing dropped file of type: \(type.identifier)") provider.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, error in guard let url = url else { print("❌ Failed to load dropped file URL") Task { @MainActor in alertMessage = "Failed to load file" showAlert = true } return } print("📄 Original dropped file URL: \(url.path)") let originalFilename = url.lastPathComponent print("📝 Original dropped filename: \(originalFilename)") let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent(UUID().uuidString) .appendingPathExtension(url.pathExtension) print("🔄 Creating temp file at: \(tempURL.path)") do { try FileManager.default.copyItem(at: url, to: tempURL) print("✅ Successfully copied to temp location") Task { @MainActor in handleFileSelection(url: tempURL, originalFilename: originalFilename) // Pass originalFilename } } catch { print("❌ Failed to copy dropped file: \(error.localizedDescription)") Task { @MainActor in alertMessage = "Failed to process dropped file" showAlert = true } } } return } } } private func handleFileSelection(url: URL, originalFilename: String? = nil) { print("🔄 Processing file selection for URL: \(url.path)") let filename = originalFilename ?? url.lastPathComponent print("📝 Original filename: \(filename)") Task { let dimensionValue = shouldResize ? Double(maxDimension) ?? 2048 : nil let settings = CompressionSettings( quality: 0.7, pngCompressionLevel: 6, preserveMetadata: true, maxDimension: dimensionValue != nil ? CGFloat(dimensionValue!) : nil, optimizeForWeb: true ) do { try await processor.processFile(url: url, settings: settings, originalFileName: filename) } catch { print("❌ File processing error: \(error.localizedDescription)") await MainActor.run { alertMessage = error.localizedDescription showAlert = true } } } } private func handleMultiFileDrop(providers: [NSItemProvider]) { Task { print("📥 Processing multiple dropped files") var urls: [URL] = [] for (index, provider) in providers.enumerated() { for type in supportedTypes { if provider.hasItemConformingToTypeIdentifier(type.identifier) { do { let url = try await withCheckedThrowingContinuation { (continuation: CheckedContinuation) in provider.loadFileRepresentation(forTypeIdentifier: type.identifier) { url, error in if let error = error { print("❌ Error loading file \(index + 1): \(error.localizedDescription)") continuation.resume(throwing: error) } else if let url = url { print("📄 Original file \(index + 1) URL: \(url.path)") print("📝 Original filename \(index + 1): \(url.lastPathComponent)") let originalFileName = url.lastPathComponent let tempURL = FileManager.default.temporaryDirectory .appendingPathComponent("\(UUID().uuidString)_\(originalFileName)") print("🔄 Creating temp file \(index + 1) at: \(tempURL.path)") do { try FileManager.default.copyItem(at: url, to: tempURL) print("✅ Successfully copied file \(index + 1) to temp location") continuation.resume(returning: tempURL) } catch { print("❌ Failed to copy file \(index + 1): \(error.localizedDescription)") continuation.resume(throwing: error) } } else { print("❌ No URL available for file \(index + 1)") continuation.resume(throwing: NSError(domain: "", code: -1, userInfo: [NSLocalizedDescriptionKey: "Failed to load file"])) } } } urls.append(url) } catch { print("❌ Failed to process dropped file \(index + 1): \(error.localizedDescription)") } break } } } if !urls.isEmpty { print("✅ Successfully processed \(urls.count) files") print("📁 Temp URLs: \(urls.map { $0.path })") await MainActor.run { multiProcessor.addFiles(urls) } } } } private func handleFileSelection(url: URL) { print("🔄 Processing file selection for URL: \(url.path)") print("📝 Original filename: \(url.lastPathComponent)") Task { let dimensionValue = shouldResize ? Double(maxDimension) ?? 2048 : nil let settings = CompressionSettings( quality: 0.7, pngCompressionLevel: 6, preserveMetadata: true, maxDimension: dimensionValue != nil ? CGFloat(dimensionValue!) : nil, optimizeForWeb: true ) do { // Store original filename before processing let originalFileName = url.lastPathComponent try await processor.processFile(url: url, settings: settings, originalFileName: originalFileName) } catch { print("❌ File processing error: \(error.localizedDescription)") await MainActor.run { alertMessage = error.localizedDescription showAlert = true } } } } @MainActor func saveCompressedFile(url: URL, originalName: String) async { print("💾 Saving compressed file") print("📝 Original name: \(originalName)") print("📁 Compressed file URL: \(url.path)") let panel = NSSavePanel() panel.canCreateDirectories = true panel.showsTagField = false // Extract original filename without UUID let originalURL = URL(fileURLWithPath: originalName) let filenameWithoutExt = originalURL.deletingPathExtension().lastPathComponent .replacingOccurrences(of: #"[A-F0-9]{8}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{4}-[A-F0-9]{12}\."#, with: "", options: .regularExpression) let fileExtension = url.pathExtension let suggestedName = "\(filenameWithoutExt)_compressed.\(fileExtension)" print("📝 Suggested save name: \(suggestedName)") panel.nameFieldStringValue = suggestedName panel.allowedContentTypes = [UTType(filenameExtension: fileExtension)].compactMap { $0 } panel.message = "Choose where to save the compressed file" guard let window = NSApp.windows.first else { return } let response = await panel.beginSheetModal(for: window) if response == .OK, let saveURL = panel.url { print("📥 Saving to: \(saveURL.path)") do { // Check if file exists if FileManager.default.fileExists(atPath: saveURL.path) { try FileManager.default.removeItem(at: saveURL) } try FileManager.default.copyItem(at: url, to: saveURL) print("✅ File saved successfully") processor.cleanup() } catch { print("❌ Save error: \(error.localizedDescription)") alertMessage = "Failed to save file: \(error.localizedDescription)" showAlert = true } } else { print("❌ Save cancelled or window not found") } } } ================================================ FILE: Achico/Views/DropZoneView.swift ================================================ import SwiftUI struct DropZoneView: View { @Binding var isDragging: Bool @Binding var shouldResize: Bool @Binding var maxDimension: String let onTap: () -> Void var body: some View { Button(action: onTap) { ZStack(alignment: .bottom) { // Main drop zone content - centered VStack(spacing: 12) { Image(systemName: "doc.circle") .font(.system(size: 32)) .foregroundColor(.secondary) Text("Drop your file here") .font(.system(size: 14)) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, maxHeight: .infinity) // Resize controls - bottom aligned HStack(spacing: 12) { Toggle("Resize", isOn: $shouldResize) .toggleStyle(.switch) .labelsHidden() Text("Resize") .font(.system(size: 13)) .foregroundColor(.secondary) if shouldResize { TextField("px", text: $maxDimension) .frame(width: 60) .textFieldStyle(RoundedBorderTextFieldStyle()) .font(.system(size: 13)) .multilineTextAlignment(.trailing) Text("px") .font(.system(size: 13)) .foregroundColor(.secondary) } } .padding(.horizontal, 16) .padding(.vertical, 8) .background( RoundedRectangle(cornerRadius: 20) .fill(Color(NSColor.controlBackgroundColor).opacity(0.5)) ) .padding(.bottom, 24) } .frame(maxWidth: .infinity, maxHeight: .infinity) .background( RoundedRectangle(cornerRadius: 12) .strokeBorder(isDragging ? Color.accentColor : Color.secondary.opacity(0.2), style: StrokeStyle(lineWidth: 1)) .background(Color.clear) ) .padding() } .buttonStyle(PlainButtonStyle()) } } ================================================ FILE: Achico/Views/MenuBarView.swift ================================================ import SwiftUI import AppKit struct MenuBarView: View { @ObservedObject var updater: UpdateChecker @EnvironmentObject var menuBarController: MenuBarController @Environment(\.dismiss) var dismiss private var appIcon: NSImage { if let bundleIcon = NSImage(named: NSImage.applicationIconName) { return bundleIcon } return NSWorkspace.shared.icon(forFile: Bundle.main.bundlePath) } var body: some View { VStack(spacing: 16) { // App Icon and Version VStack(spacing: 8) { Image(nsImage: appIcon) .resizable() .aspectRatio(contentMode: .fit) .frame(width: 64, height: 64) Text("Version \(Bundle.main.object(forInfoDictionaryKey: "CFBundleShortVersionString") as? String ?? "1.0.0")") .font(.subheadline) .foregroundColor(.secondary) } .padding(.top, 16) // Status Section Group { if updater.isChecking { VStack(spacing: 8) { ProgressView() .scaleEffect(1.2) Text("Checking for updates...") .font(.headline) .foregroundColor(.secondary) } } else if let error = updater.error { VStack(spacing: 8) { Image(systemName: "xmark.circle.fill") .font(.system(size: 28)) .foregroundColor(.red) Text(error) .font(.subheadline) .foregroundColor(.secondary) .multilineTextAlignment(.center) } } else if updater.updateAvailable { VStack(spacing: 12) { Image(systemName: "arrow.down.circle.fill") .font(.system(size: 28)) .foregroundColor(.blue) if let version = updater.latestVersion { Text("Version \(version) Available") .font(.headline) } if let notes = updater.releaseNotes { ScrollView { Text(notes) .font(.footnote) .foregroundColor(.secondary) .multilineTextAlignment(.center) .padding(.horizontal) } .frame(maxHeight: 80) } Button { if let url = updater.downloadURL { NSWorkspace.shared.open(url) dismiss() } } label: { Text("Download Update") .frame(maxWidth: 200) } .buttonStyle(.borderedProminent) } } else { VStack(spacing: 8) { Image(systemName: "checkmark.circle.fill") .font(.system(size: 28)) .foregroundColor(.green) Text("Achico is up to date") .font(.headline) } } } .frame(maxWidth: .infinity, alignment: .center) .padding(.vertical, 8) Divider() // Bottom Buttons HStack(spacing: 16) { Button("Check Again") { updater.checkForUpdates() } .buttonStyle(.plain) .foregroundColor(.blue) Button("Close") { dismiss() } .buttonStyle(.plain) .foregroundColor(.secondary) } .padding(.bottom, 16) Text("Built by [Nuance](https://nuanc.me)") .font(.footnote) .foregroundColor(.secondary) .padding(.bottom, 8) } .padding(.horizontal) .frame(width: 300) .fixedSize(horizontal: false, vertical: true) } } ================================================ FILE: Achico/Views/MultiFileProcessingView.swift ================================================ import Foundation import UniformTypeIdentifiers import SwiftUI struct FileProcessingState: Identifiable { let id: UUID let url: URL let originalFileName: String var progress: Double var result: FileProcessor.ProcessingResult? var isProcessing: Bool var error: Error? // Add this computed property var displayFileName: String { // If the filename contains UUID prefix, remove it let filename = url.lastPathComponent if let range = filename.range(of: "_") { return String(filename[range.upperBound...]) } return originalFileName } init(url: URL) { self.id = UUID() self.url = url self.originalFileName = url.lastPathComponent self.progress = 0 self.result = nil self.isProcessing = false self.error = nil } } @MainActor class MultiFileProcessor: ObservableObject { @Published private(set) var files: [FileProcessingState] = [] @Published private(set) var isProcessingMultiple = false private var processingTasks: [UUID: Task] = [:] func addFiles(_ urls: [URL]) { let newFiles = urls.map { FileProcessingState(url: $0) } files.append(contentsOf: newFiles) // Process each new file individually for file in newFiles { processFile(with: file.id) } } func removeFile(at index: Int) { guard index < files.count else { return } let fileId = files[index].id processingTasks[fileId]?.cancel() processingTasks.removeValue(forKey: fileId) files.remove(at: index) } func clearFiles() { // Cancel all ongoing processing tasks for task in processingTasks.values { task.cancel() } processingTasks.removeAll() files.removeAll() } private func processFile(with id: UUID) { let task = Task { await processFileInternal(with: id) } processingTasks[id] = task } func saveAllFilesToFolder() async { let panel = NSOpenPanel() panel.canCreateDirectories = true panel.canChooseFiles = false panel.canChooseDirectories = true panel.message = "Choose where to save all compressed files" panel.prompt = "Select Folder" guard let window = NSApp.windows.first else { return } let response = await panel.beginSheetModal(for: window) if response == .OK, let folderURL = panel.url { for file in files { if let result = file.result { do { // Use the original filename instead of the result filename let originalURL = URL(fileURLWithPath: file.originalFileName) let filenameWithoutExt = originalURL.deletingPathExtension().lastPathComponent let fileExtension = originalURL.pathExtension let newFileName = "\(filenameWithoutExt)_compressed.\(fileExtension)" let destinationURL = folderURL.appendingPathComponent(newFileName) try FileManager.default.copyItem(at: result.compressedURL, to: destinationURL) } catch { print("Failed to save file \(file.originalFileName): \(error.localizedDescription)") } } } } } private func processFileInternal(with id: UUID) async { guard let index = files.firstIndex(where: { $0.id == id }) else { return } guard index < files.count else { return } let processor = FileProcessor() // Update the processing state files[index].isProcessing = true do { let settings = CompressionSettings( quality: 0.7, pngCompressionLevel: 6, preserveMetadata: true, optimizeForWeb: true ) try await processor.processFile(url: files[index].url, settings: settings) guard index < files.count, files[index].id == id else { return } if let processingResult = processor.processingResult { files[index].result = processingResult files[index].isProcessing = false } } catch { guard index < files.count, files[index].id == id else { return } files[index].error = error files[index].isProcessing = false } // Clean up the task processingTasks.removeValue(forKey: id) } func saveCompressedFile(url: URL, originalName: String) async { let panel = NSSavePanel() panel.canCreateDirectories = true panel.showsTagField = false // Use originalName directly instead of extracting from URL let originalURL = URL(fileURLWithPath: originalName) let filenameWithoutExt = originalURL.deletingPathExtension().lastPathComponent let fileExtension = originalURL.pathExtension panel.nameFieldStringValue = "\(filenameWithoutExt)_compressed.\(fileExtension)" panel.allowedContentTypes = [UTType(filenameExtension: url.pathExtension)].compactMap { $0 } panel.message = "Choose where to save the compressed file" guard let window = NSApp.windows.first else { return } let response = await panel.beginSheetModal(for: window) if response == .OK, let saveURL = panel.url { do { // Check if file exists if FileManager.default.fileExists(atPath: saveURL.path) { try FileManager.default.removeItem(at: saveURL) } try FileManager.default.copyItem(at: url, to: saveURL) } catch { print("Failed to save file: \(error.localizedDescription)") } } } func downloadAllFiles() async { for file in files { if let result = file.result { await saveCompressedFile(url: result.compressedURL, originalName: file.originalFileName) } } } } struct MultiFileView: View { @ObservedObject var processor: MultiFileProcessor @Binding var shouldResize: Bool @Binding var maxDimension: String let supportedTypes: [UTType] @State private var hoveredFileID: UUID? init(processor: MultiFileProcessor, shouldResize: Binding, maxDimension: Binding, supportedTypes: [UTType]) { self._processor = ObservedObject(wrappedValue: processor) self._shouldResize = shouldResize self._maxDimension = maxDimension self.supportedTypes = supportedTypes } var body: some View { VStack(spacing: 20) { // Header section with ButtonGroup HStack { Text("Files") .font(.title2) .fontWeight(.semibold) Spacer() if !processor.files.isEmpty { ButtonGroup(buttons: [ ( title: "Save All", icon: "folder.fill.badge.plus", action: { Task { await processor.saveAllFilesToFolder() } } ), ( title: "Clear All", icon: "trash.fill", action: { processor.clearFiles() } ) ]) } } .padding(.horizontal) // File list VStack(spacing: 0) { // Wrapper for consistent padding ScrollView { LazyVStack(spacing: 8) { ForEach(Array(processor.files.enumerated()), id: \.element.id) { index, file in FileRow( file: file, isHovered: hoveredFileID == file.id, onSave: { if let result = file.result { Task { await processor.saveCompressedFile( url: result.compressedURL, originalName: file.originalFileName ) } } }, onRemove: { processor.removeFile(at: index) } ) .onHover { isHovered in hoveredFileID = isHovered ? file.id : nil } } } .padding(.horizontal) .padding(.vertical) // Add vertical padding inside scroll view } } .frame(maxWidth: .infinity, maxHeight: .infinity) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .clipShape(RoundedRectangle(cornerRadius: 12)) } .padding() } } struct FileRow: View { let file: FileProcessingState let isHovered: Bool let onSave: () -> Void let onRemove: () -> Void var body: some View { HStack(spacing: 16) { // File icon with extension badge ZStack(alignment: .bottomTrailing) { getFileIcon(for: file.url.pathExtension.lowercased()) .font(.system(size: 28)) Text(file.url.pathExtension.uppercased()) .font(.system(size: 8, weight: .bold)) .padding(.horizontal, 4) .padding(.vertical, 2) .background(.ultraThinMaterial) .clipShape(RoundedRectangle(cornerRadius: 4)) } // File info VStack(alignment: .leading, spacing: 4) { Text(file.displayFileName) .font(.headline) .lineLimit(1) Group { if let result = file.result { Label( "Reduced by \(result.savedPercentage)%", systemImage: "arrow.down.circle.fill" ) .foregroundStyle(.green) } else if let error = file.error { Label( error.localizedDescription, systemImage: "exclamationmark.circle.fill" ) .foregroundStyle(.red) } else if file.isProcessing { Label( "Processing...", systemImage: "arrow.triangle.2.circlepath" ) .foregroundStyle(.blue) } } .font(.subheadline) } Spacer() // Actions HStack(spacing: 12) { if file.isProcessing { ProgressView() .controlSize(.small) } else if let _ = file.result { Button(action: onSave) { Label("Download", systemImage: "square.and.arrow.down.fill") } .buttonStyle(GlassButtonStyle()) } Button(action: onRemove) { Image(systemName: "trash.fill") .foregroundStyle(.red.opacity(0.8)) } .buttonStyle(PlainButtonStyle()) } .opacity(isHovered ? 1 : 0.7) } .padding() .background( RoundedRectangle(cornerRadius: 12) .fill(isHovered ? Color(NSColor.controlBackgroundColor).opacity(0.7) : Color.clear) .overlay( RoundedRectangle(cornerRadius: 12) .stroke(Color.primary.opacity(0.1), lineWidth: 1) ) ) .contentShape(Rectangle()) } @ViewBuilder private func getFileIcon(for extension: String) -> some View { switch `extension` { case "jpg", "jpeg", "png", "heic", "webp": Image(systemName: "photo.fill") .foregroundStyle(.blue) case "mp4", "mov", "avi": Image(systemName: "video.fill") .foregroundStyle(.purple) case "mp3", "wav", "aiff": Image(systemName: "music.note") .foregroundStyle(.pink) case "pdf": Image(systemName: "doc.fill") .foregroundStyle(.red) default: Image(systemName: "doc.fill") .foregroundStyle(.secondary) } } } ================================================ FILE: Achico/Views/ResultView.swift ================================================ import SwiftUI struct ResultView: View { let result: FileProcessor.ProcessingResult let onDownload: () -> Void let onReset: () -> Void var body: some View { VStack(spacing: 24) { // Status Icon ZStack { Circle() .fill(Color(NSColor.windowBackgroundColor)) .frame(width: 64, height: 64) .shadow(color: Color.black.opacity(0.1), radius: 10, x: 0, y: 5) Image(systemName: result.savedPercentage > 0 ? "checkmark" : "exclamationmark") .font(.system(size: 24, weight: .medium)) .foregroundColor(result.savedPercentage > 0 ? .green : .orange) } // Status Text VStack(spacing: 8) { Text(result.savedPercentage > 0 ? "Compression Complete" : "Already Optimized") .font(.system(size: 16, weight: .semibold)) if result.savedPercentage > 0 { Text("File size reduced by \(result.savedPercentage)%") .font(.system(size: 14)) .foregroundColor(.secondary) } else { Text("No further compression needed") .font(.system(size: 14)) .foregroundColor(.secondary) } } // File Size Info HStack(spacing: 32) { VStack(spacing: 4) { Text("Original") .font(.system(size: 12)) .foregroundColor(.secondary) Text(formatFileSize(result.originalSize)) .font(.system(size: 14, weight: .medium)) } VStack(spacing: 4) { Text("Compressed") .font(.system(size: 12)) .foregroundColor(.secondary) Text(formatFileSize(result.compressedSize)) .font(.system(size: 14, weight: .medium)) } } .padding(.vertical, 16) .padding(.horizontal, 24) .background( RoundedRectangle(cornerRadius: 12) .fill(Color(NSColor.controlBackgroundColor)) .opacity(0.5) ) // Action Buttons HStack(spacing: 12) { Button(action: onReset) { Text("New File") .font(.system(size: 14, weight: .medium)) .frame(maxWidth: .infinity) .frame(height: 36) } .buttonStyle(SecondaryButtonStyle()) Button(action: onDownload) { Text("Save") .font(.system(size: 14, weight: .medium)) .frame(maxWidth: .infinity) .frame(height: 36) } .buttonStyle(PrimaryButtonStyle()) } .padding(.top, 8) } .padding(32) .frame(maxWidth: 320) .background( RoundedRectangle(cornerRadius: 16) .fill(Color(NSColor.windowBackgroundColor)) .opacity(0.8) .shadow(color: Color.black.opacity(0.1), radius: 20, x: 0, y: 10) ) } private func formatFileSize(_ size: Int64) -> String { let formatter = ByteCountFormatter() formatter.allowedUnits = [.useMB] formatter.countStyle = .file return formatter.string(fromByteCount: size) } } // Add new custom button styles struct PrimaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.white) .background(Color.accentColor) .cornerRadius(8) .opacity(configuration.isPressed ? 0.9 : 1.0) } } struct SecondaryButtonStyle: ButtonStyle { func makeBody(configuration: Configuration) -> some View { configuration.label .foregroundColor(.primary) .background(Color(NSColor.controlBackgroundColor).opacity(0.5)) .cornerRadius(8) .overlay( RoundedRectangle(cornerRadius: 8) .strokeBorder(Color.primary.opacity(0.1), lineWidth: 1) ) .opacity(configuration.isPressed ? 0.9 : 1.0) } } ================================================ FILE: Achico.xcodeproj/project.pbxproj ================================================ // !$*UTF8*$! { archiveVersion = 1; classes = { }; objectVersion = 77; objects = { /* Begin PBXFileReference section */ C82F044E2CCB3DD20012C07B /* Achico.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Achico.app; sourceTree = BUILT_PRODUCTS_DIR; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ C8A16E302CE0FC6D00F427B2 /* Exceptions for "Achico" folder in "Achico" target */ = { isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Info.plist, ); target = C82F044D2CCB3DD20012C07B /* Achico */; }; /* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ C82F04502CCB3DD20012C07B /* Achico */ = { isa = PBXFileSystemSynchronizedRootGroup; exceptions = ( C8A16E302CE0FC6D00F427B2 /* Exceptions for "Achico" folder in "Achico" target */, ); path = Achico; sourceTree = ""; }; /* End PBXFileSystemSynchronizedRootGroup section */ /* Begin PBXFrameworksBuildPhase section */ C82F044B2CCB3DD20012C07B /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ C82F04452CCB3DD20012C07B = { isa = PBXGroup; children = ( C82F04502CCB3DD20012C07B /* Achico */, C82F044F2CCB3DD20012C07B /* Products */, ); sourceTree = ""; }; C82F044F2CCB3DD20012C07B /* Products */ = { isa = PBXGroup; children = ( C82F044E2CCB3DD20012C07B /* Achico.app */, ); name = Products; sourceTree = ""; }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ C82F044D2CCB3DD20012C07B /* Achico */ = { isa = PBXNativeTarget; buildConfigurationList = C82F045D2CCB3DD40012C07B /* Build configuration list for PBXNativeTarget "Achico" */; buildPhases = ( C82F044A2CCB3DD20012C07B /* Sources */, C82F044B2CCB3DD20012C07B /* Frameworks */, C82F044C2CCB3DD20012C07B /* Resources */, ); buildRules = ( ); dependencies = ( ); fileSystemSynchronizedGroups = ( C82F04502CCB3DD20012C07B /* Achico */, ); name = Achico; packageProductDependencies = ( ); productName = Achico; productReference = C82F044E2CCB3DD20012C07B /* Achico.app */; productType = "com.apple.product-type.application"; }; /* End PBXNativeTarget section */ /* Begin PBXProject section */ C82F04462CCB3DD20012C07B /* Project object */ = { isa = PBXProject; attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1600; LastUpgradeCheck = 1610; TargetAttributes = { C82F044D2CCB3DD20012C07B = { CreatedOnToolsVersion = 16.0; }; }; }; buildConfigurationList = C82F04492CCB3DD20012C07B /* Build configuration list for PBXProject "Achico" */; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( en, Base, ); mainGroup = C82F04452CCB3DD20012C07B; minimizedProjectReferenceProxies = 1; preferredProjectObjectVersion = 77; productRefGroup = C82F044F2CCB3DD20012C07B /* Products */; projectDirPath = ""; projectRoot = ""; targets = ( C82F044D2CCB3DD20012C07B /* Achico */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ C82F044C2CCB3DD20012C07B /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXResourcesBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ C82F044A2CCB3DD20012C07B /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ /* Begin XCBuildConfiguration section */ C82F045B2CCB3DD40012C07B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; GCC_OPTIMIZATION_LEVEL = 0; GCC_PREPROCESSOR_DEFINITIONS = ( "DEBUG=1", "$(inherited)", ); GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; MTL_FAST_MATH = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = macosx; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; }; name = Debug; }; C82F045C2CCB3DD40012C07B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; CLANG_ENABLE_MODULES = YES; CLANG_ENABLE_OBJC_ARC = YES; CLANG_ENABLE_OBJC_WEAK = YES; CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; CLANG_WARN_BOOL_CONVERSION = YES; CLANG_WARN_COMMA = YES; CLANG_WARN_CONSTANT_CONVERSION = YES; CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; CLANG_WARN_DOCUMENTATION_COMMENTS = YES; CLANG_WARN_EMPTY_BODY = YES; CLANG_WARN_ENUM_CONVERSION = YES; CLANG_WARN_INFINITE_RECURSION = YES; CLANG_WARN_INT_CONVERSION = YES; CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; CLANG_WARN_STRICT_PROTOTYPES = YES; CLANG_WARN_SUSPICIOUS_MOVE = YES; CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu17; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; GCC_WARN_UNDECLARED_SELECTOR = YES; GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; LOCALIZATION_PREFERS_STRING_CATALOGS = YES; MACOSX_DEPLOYMENT_TARGET = 15.0; MTL_ENABLE_DEBUG_INFO = NO; MTL_FAST_MATH = YES; SDKROOT = macosx; SWIFT_COMPILATION_MODE = wholemodule; }; name = Release; }; C82F045E2CCB3DD40012C07B /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Achico/Achico.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 26; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Achico/Preview Content\""; DEVELOPMENT_TEAM = YYMLDY74QZ; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Achico/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 3.2.1; PRODUCT_BUNDLE_IDENTIFIER = Minimal.Achico; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Debug; }; C82F045F2CCB3DD40012C07B /* Release */ = { isa = XCBuildConfiguration; buildSettings = { ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES; CODE_SIGN_ENTITLEMENTS = Achico/Achico.entitlements; "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; CURRENT_PROJECT_VERSION = 26; DEAD_CODE_STRIPPING = YES; DEVELOPMENT_ASSET_PATHS = "\"Achico/Preview Content\""; DEVELOPMENT_TEAM = YYMLDY74QZ; ENABLE_HARDENED_RUNTIME = YES; ENABLE_PREVIEWS = YES; GENERATE_INFOPLIST_FILE = YES; INFOPLIST_FILE = Achico/Info.plist; INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.productivity"; INFOPLIST_KEY_NSHumanReadableCopyright = ""; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; MARKETING_VERSION = 3.2.1; PRODUCT_BUNDLE_IDENTIFIER = Minimal.Achico; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; SWIFT_VERSION = 5.0; }; name = Release; }; /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ C82F04492CCB3DD20012C07B /* Build configuration list for PBXProject "Achico" */ = { isa = XCConfigurationList; buildConfigurations = ( C82F045B2CCB3DD40012C07B /* Debug */, C82F045C2CCB3DD40012C07B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; C82F045D2CCB3DD40012C07B /* Build configuration list for PBXNativeTarget "Achico" */ = { isa = XCConfigurationList; buildConfigurations = ( C82F045E2CCB3DD40012C07B /* Debug */, C82F045F2CCB3DD40012C07B /* Release */, ); defaultConfigurationIsVisible = 0; defaultConfigurationName = Release; }; /* End XCConfigurationList section */ }; rootObject = C82F04462CCB3DD20012C07B /* Project object */; } ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2024 nuance-dev Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: README.md ================================================ # Achico - A Free MacOS Native File Compression App A lightweight, native macOS app that intelligently compresses files while maintaining quality. Support for PDF, images, videos, and more! Simple, fast, and efficient! ![image](https://github.com/user-attachments/assets/4e10b8a7-decc-4e0b-8b56-f88198e75ec9) ## Features ### File Support - **PDFs**: Smart compression while preserving readability - **Images**: Support for JPEG, PNG, HEIC, TIFF, GIF, BMP, WebP, SVG, RAW, and ICO - **Videos**: MP4, MOV, AVI, and other common formats - **Audio**: M4V, WAV, MP3, AIFF - **File Resizing**: Optionally resize images and videos while compressing ### Core Features - **Multiple Input Methods**: Drag & drop or click to select files - **Real-time Progress**: Watch your files being compressed with a clean progress indicator - **Compression Stats**: See how much space you've saved instantly - **Dark and Light modes**: Seamlessly integrates with your system preferences - **Native Performance**: Built with SwiftUI for optimal macOS integration ### Compression Options - **Quality Control**: Adjust compression levels to balance size and quality - **Size Limits**: Set maximum dimensions for images and videos - **Format Conversion**: Automatic conversion of less efficient formats - **Metadata Handling**: Option to preserve or strip metadata ![compression-demo](https://github.com/user-attachments/assets/e494937d-7e52-4d6c-9046-d6b0d577c67e) ## 💻 Get Started Download from the [releases](https://github.com/nuance-dev/Achico/releases/) page. ## ⚡️ How it Works 1. Drop or select your files 2. Adjust compression settings (optional) 3. Watch the magic happen 4. Get your compressed files 5. That's it! 6. Update: you can now resize your images and videos 7. Update: you can now drop multiple files ![42630](https://github.com/user-attachments/assets/6def2137-fd12-4f7d-b59a-4476ae506331) ## 🛠 Technical Details - Built natively for macOS using SwiftUI - Uses specialized frameworks for each file type: - PDFKit for PDF compression - AVFoundation for video processing - Core Graphics for image optimization - Efficient memory management for handling large files - Clean, modern interface following Apple's design guidelines - Parallel processing for better performance ## 🔮 Features Coming Soon - Batch processing - Folder monitoring - Quick Look integration - Custom presets for different use cases - Additional file format support - Advanced compression options - Progress notifications ## 🤝 Contributing We welcome contributions! Here's how you can help: 1. Clone the repository 2. Create your feature branch (`git checkout -b feature/AmazingFeature`) 3. Commit your changes (`git commit -m 'Add some AmazingFeature'`) 4. Push to the branch (`git push origin feature/AmazingFeature`) 5. Open a Pull Request Please ensure your PR: - Follows the existing code style - Includes appropriate tests if applicable - Updates documentation as needed ## 📝 License This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. ## 🔗 Links - Website: [Nuance](https://nuanc.me) - Report issues: [GitHub Issues](https://github.com/nuance-dev/Achico/issues) - Follow updates: [@NuanceDev](https://twitter.com/Nuancedev) ## Requirements - macOS 14.0 or later ## Supported File Formats ### Images - JPEG/JPG - PNG - HEIC - TIFF/TIF - GIF (including animated) - BMP - WebP - SVG - RAW (CR2, NEF, ARW) - ICO ### Videos - MP4 - MOV - AVI - MPEG/MPG ### Documents - PDF