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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.security.app-sandbox</key>
<true/>
<key>com.apple.security.files.user-selected.read-only</key>
<true/>
<key>com.apple.security.files.user-selected.read-write</key>
<true/>
<key>com.apple.security.network.client</key>
<true/>
</dict>
</plist>
================================================
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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict/>
</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<Void, Error>) 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: "<!--[\\s\\S]*?-->", 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..<totalPages {
autoreleasepool {
if let page = document.page(at: i) {
if let compressedPage = try? compressPDFPage(page) {
newDocument.insert(compressedPage, at: i)
} else {
newDocument.insert(page, at: i)
}
}
}
DispatchQueue.main.async {
self.progress = Double(i + 1) / Double(totalPages)
}
}
newDocument.write(to: tempURL)
}
private func processJPEG(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: .jpeg, settings: settings) else {
throw CompressionError.compressionFailed
}
try compressedData.write(to: tempURL)
}
private func processPNG(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
}
try compressedData.write(to: tempURL)
}
private func processHEIC(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: .jpeg, settings: settings) else {
throw CompressionError.compressionFailed
}
let jpegURL = tempURL.deletingPathExtension().appendingPathExtension("jpg")
try compressedData.write(to: jpegURL)
}
private func processTIFF(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: .jpeg, settings: settings) else {
throw CompressionError.compressionFailed
}
try compressedData.write(to: tempURL)
}
private func processGIF(from url: URL, to tempURL: URL) throws {
guard let imageSource = CGImageSourceCreateWithURL(url as CFURL, nil) else {
throw CompressionError.conversionFailed
}
let frameCount = CGImageSourceGetCount(imageSource)
if frameCount > 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..<frameCount {
autoreleasepool {
guard let frameProperties = CGImageSourceCopyPropertiesAtIndex(imageSource, i, nil) else {
return
}
// Extract GIF-specific properties
let gifProperties = (frameProperties as Dictionary)[kCGImagePropertyGIFDictionary] as? Dictionary<CFString, Any>
// 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..<min(paddedCurrent.count, paddedLatest.count) {
if paddedLatest[i] > 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<URL, Error>) 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<Void, Never>] = [:]
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<Bool>, maxDimension: Binding<String>, 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 = "<group>";
};
/* 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 = "<group>";
};
C82F044F2CCB3DD20012C07B /* Products */ = {
isa = PBXGroup;
children = (
C82F044E2CCB3DD20012C07B /* Achico.app */,
);
name = Products;
sourceTree = "<group>";
};
/* 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!

## 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

## 💻 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

## 🛠 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
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
Condensed preview — 35 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (131K chars).
[
{
"path": "Achico/Achico.entitlements",
"chars": 436,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Achico/App/AchicoApp.swift",
"chars": 1963,
"preview": "import SwiftUI\nimport AppKit\n\n@main\nstruct AchicoApp: App {\n @NSApplicationDelegateAdaptor(AppDelegate.self) var appD"
},
{
"path": "Achico/App/AppDelegate.swift",
"chars": 187,
"preview": "import Cocoa\n\nclass AppDelegate: NSObject, NSApplicationDelegate {\n func applicationWillTerminate(_ notification: Not"
},
{
"path": "Achico/Info.plist",
"chars": 181,
"preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-1024.imageset/Contents.json",
"chars": 310,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-1024.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-128.imageset/Contents.json",
"chars": 309,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-128.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-16.imageset/Contents.json",
"chars": 308,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-16.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-256.imageset/Contents.json",
"chars": 309,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-256.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-32.imageset/Contents.json",
"chars": 308,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-32.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-512.imageset/Contents.json",
"chars": 309,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-512.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/AppIcon-64.imageset/Contents.json",
"chars": 308,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-64.png\",\n \"idiom\" : \"universal\",\n \"scale\" : \"1x\"\n },\n "
},
{
"path": "Achico/Preview Content/Preview Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Achico/Processor/CacheManager.swift",
"chars": 2791,
"preview": "// CacheManager.swift\nimport Foundation\nimport AppKit\n\nclass CacheManager {\n static let shared = CacheManager()\n \n"
},
{
"path": "Achico/Processor/FileProcessor.swift",
"chars": 32027,
"preview": "import Foundation\nimport PDFKit\nimport UniformTypeIdentifiers\nimport AppKit\nimport CoreGraphics\nimport AVFoundation\n\nenu"
},
{
"path": "Achico/Processor/VideoProcessor.swift",
"chars": 6083,
"preview": "import Foundation\nimport AVFoundation\n\n@available(macOS 12.0, *)\nactor VideoProcessor {\n enum VideoError: LocalizedEr"
},
{
"path": "Achico/Resources/Assets.xcassets/AccentColor.colorset/Contents.json",
"chars": 335,
"preview": "{\n \"colors\" : [\n {\n \"color\" : {\n \"color-space\" : \"display-p3\",\n \"components\" : {\n \"alpha"
},
{
"path": "Achico/Resources/Assets.xcassets/AppIcon.appiconset/Contents.json",
"chars": 1287,
"preview": "{\n \"images\" : [\n {\n \"filename\" : \"AppIcon-16.png\",\n \"idiom\" : \"mac\",\n \"scale\" : \"1x\",\n \"size\" : "
},
{
"path": "Achico/Resources/Assets.xcassets/Contents.json",
"chars": 63,
"preview": "{\n \"info\" : {\n \"author\" : \"xcode\",\n \"version\" : 1\n }\n}\n"
},
{
"path": "Achico/Resources/CompressionSettings.swift",
"chars": 930,
"preview": "import Foundation\nimport CoreGraphics\n\npublic struct CompressionSettings {\n var quality: CGFloat = 0.7 // JPEG"
},
{
"path": "Achico/Resources/FileDropDelegate.swift",
"chars": 706,
"preview": "import SwiftUI\nimport UniformTypeIdentifiers\nstruct FileDropDelegate: DropDelegate {\n @Binding var isDragging: Bool\n "
},
{
"path": "Achico/Resources/MenuBarController.swift",
"chars": 3561,
"preview": "import SwiftUI\nimport AppKit\n\nclass MenuBarController: NSObject, ObservableObject {\n @Published private(set) var upda"
},
{
"path": "Achico/Resources/UpdateChecker.swift",
"chars": 6245,
"preview": "import Foundation\n\nstruct GitHubRelease: Codable {\n let tagName: String\n let name: String\n let body: String\n "
},
{
"path": "Achico/UI Components/ButtonGroup.swift",
"chars": 2031,
"preview": "import SwiftUI\n\nstruct ToolbarButton: View {\n let title: String\n let icon: String\n let action: () -> Void\n l"
},
{
"path": "Achico/UI Components/GlassButtonStyle.swift",
"chars": 617,
"preview": "import SwiftUI\n\nstruct GlassButtonStyle: ButtonStyle {\n func makeBody(configuration: Configuration) -> some View {\n "
},
{
"path": "Achico/UI Components/TitleBarAccessory.swift",
"chars": 424,
"preview": "import SwiftUI\n\nstruct TitleBarAccessory: View {\n @AppStorage(\"isDarkMode\") private var isDarkMode = false\n \n v"
},
{
"path": "Achico/UI Components/VisualEffectBlur.swift",
"chars": 509,
"preview": "import SwiftUI\n\nstruct VisualEffectBlur: NSViewRepresentable {\n var material: NSVisualEffectView.Material\n var ble"
},
{
"path": "Achico/UI Components/WindowAccessor.swift",
"chars": 804,
"preview": "import SwiftUI\n\nstruct WindowAccessor: NSViewRepresentable {\n func makeNSView(context: Context) -> NSView {\n l"
},
{
"path": "Achico/Views/ContentView.swift",
"chars": 16532,
"preview": "import SwiftUI\nimport UniformTypeIdentifiers\nimport AppKit\n\nstruct ContentView: View {\n @StateObject private var proc"
},
{
"path": "Achico/Views/DropZoneView.swift",
"chars": 2517,
"preview": "import SwiftUI\n\nstruct DropZoneView: View {\n @Binding var isDragging: Bool\n @Binding var shouldResize: Bool\n @B"
},
{
"path": "Achico/Views/MenuBarView.swift",
"chars": 4796,
"preview": "import SwiftUI\nimport AppKit\n\nstruct MenuBarView: View {\n @ObservedObject var updater: UpdateChecker\n @Environment"
},
{
"path": "Achico/Views/MultiFileProcessingView.swift",
"chars": 13729,
"preview": "import Foundation\nimport UniformTypeIdentifiers\nimport SwiftUI\n\nstruct FileProcessingState: Identifiable {\n let id: U"
},
{
"path": "Achico/Views/ResultView.swift",
"chars": 4636,
"preview": "import SwiftUI\n\nstruct ResultView: View {\n let result: FileProcessor.ProcessingResult\n let onDownload: () -> Void\n"
},
{
"path": "Achico.xcodeproj/project.pbxproj",
"chars": 11855,
"preview": "// !$*UTF8*$!\n{\n\tarchiveVersion = 1;\n\tclasses = {\n\t};\n\tobjectVersion = 77;\n\tobjects = {\n\n/* Begin PBXFileReference secti"
},
{
"path": "LICENSE",
"chars": 1067,
"preview": "MIT License\n\nCopyright (c) 2024 nuance-dev\n\nPermission is hereby granted, free of charge, to any person obtaining a copy"
},
{
"path": "README.md",
"chars": 3525,
"preview": "# Achico - A Free MacOS Native File Compression App\n\nA lightweight, native macOS app that intelligently compresses files"
}
]
About this extraction
This page contains the full source code of the nuance-dev/achico GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 35 files (119.2 KB), approximately 27.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.