import SwiftUI import UniformTypeIdentifiers /// 1:1 ImageOptim layout: a two-column table (File | Savings) filling the /// window, a thin grey header, and a bottom status bar with file count, an /// overall savings percentage, and a settings gear. No inline controls. struct ContentView: View { @EnvironmentObject var model: AppModel var body: some View { VStack(spacing: 0) { if !model.encoderReady { Text("\(model.format.encoder.cmd) not found — run: brew install \(model.format.encoder.brew)") .foregroundColor(.white).frame(maxWidth: .infinity) .padding(6).background(Color.red) } header Divider() table Divider() statusBar } .background(Color(NSColor.controlBackgroundColor)) } private var header: some View { HStack(spacing: 0) { Text("File").padding(.leading, 28) Spacer() Text("Savings").padding(.trailing, 16) } .font(.system(size: 11, weight: .semibold)) .foregroundColor(.secondary) .frame(height: 22) .background(Color(NSColor.windowBackgroundColor)) } private var table: some View { ZStack { if model.items.isEmpty { VStack(spacing: 8) { Image(systemName: "photo.on.rectangle.angled") .font(.system(size: 40)).foregroundColor(.secondary) Text("Drop images here").font(.title3).foregroundColor(.secondary) Text("JPG · PNG · GIF → JPEG XL").font(.caption).foregroundColor(.secondary) } } else { List(model.items) { ImageRow(item: $0) } .listStyle(.plain) } } .frame(maxWidth: .infinity, maxHeight: .infinity) .onDrop(of: [UTType.fileURL], isTargeted: nil) { providers in for p in providers { _ = p.loadObject(ofClass: URL.self) { url, _ in if let url { DispatchQueue.main.async { model.add(urls: [url]) } } } } return true } } private var statusBar: some View { HStack(spacing: 12) { Button(action: openPanel) { Image(systemName: "plus") } .buttonStyle(.borderless).help("Add files or a folder") SettingsGear().environmentObject(model) Text("\(model.items.count) files").foregroundColor(.secondary) if model.busy { ProgressView().scaleEffect(0.5) } Spacer() Text("saved").foregroundColor(.secondary) Text(String(format: "%.1f%%", model.overallSavedPercent)) .foregroundColor(.green).bold().monospacedDigit() } .font(.system(size: 12)) .padding(.horizontal, 12).frame(height: 34) .background(Color(NSColor.windowBackgroundColor)) } /// "+" → pick files and/or folders; folders recurse via AppModel.add. private func openPanel() { let panel = NSOpenPanel() panel.canChooseFiles = true panel.canChooseDirectories = true panel.allowsMultipleSelection = true if panel.runModal() == .OK { model.add(urls: panel.urls) } } } /// Gear button → popover with the quality/effort/lossless settings ImageOptim /// keeps in Preferences. struct SettingsGear: View { @EnvironmentObject var model: AppModel @State private var open = false var body: some View { Button { open.toggle() } label: { Image(systemName: "gearshape") } .buttonStyle(.borderless) .popover(isPresented: $open) { VStack(alignment: .leading, spacing: 12) { Picker("Format", selection: $model.format) { ForEach(Compressor.Format.allCases) { Text($0.label).tag($0) } }.pickerStyle(.segmented) Toggle("Lossless", isOn: $model.lossless) if !model.lossless { HStack { Text("Quality"); Slider(value: $model.distance, in: 0.5...3.0) Text(String(format: "d%.1f", model.distance)).monospacedDigit() } } Stepper("Effort \(model.effort)", value: $model.effort, in: 1...9) Toggle("Keep original", isOn: $model.keepOriginal) }.padding(16).frame(width: 240) } } } struct ImageRow: View { @ObservedObject var item: ImageItem var body: some View { HStack(spacing: 8) { if let t = item.thumb { Image(nsImage: t).resizable().frame(width: 22, height: 22).cornerRadius(3) } else { Image(systemName: "photo").frame(width: 22, height: 22).foregroundColor(.secondary) } icon.frame(width: 14) Text(item.url.lastPathComponent).lineLimit(1) Spacer() Group { switch item.status { case .done: Text(String(format: "%.1f%%", item.savedFraction * 100)) .foregroundColor(.green).bold() case .failed: Text("error").foregroundColor(.red).help(item.message) case .skipped: Text("—").foregroundColor(.secondary) case .working: ProgressView().scaleEffect(0.5) case .pending: Text("…").foregroundColor(.secondary) } }.monospacedDigit() } .font(.system(size: 13)) .padding(.vertical, 2) .contentShape(Rectangle()) .onTapGesture(count: 2) { if let out = item.outputURL { NSWorkspace.shared.activateFileViewerSelecting([out]) } } .onTapGesture(count: 1) { QuickLook.shared.preview(item.url) } .contextMenu { if let out = item.outputURL { Button("Reveal in Finder") { NSWorkspace.shared.activateFileViewerSelecting([out]) } if out.pathExtension == "jxl" { Button("Decode back to PNG (compare)") { DispatchQueue.global().async { if let png = try? Compressor.decode(out) { DispatchQueue.main.async { NSWorkspace.shared.activateFileViewerSelecting([png]) } } } } } } } } @ViewBuilder var icon: some View { switch item.status { case .done: Image(systemName: "checkmark.circle.fill").foregroundColor(.green) case .failed: Image(systemName: "xmark.circle.fill").foregroundColor(.red) default: Image(systemName: "photo") } } }