Add Konverter: a local-only menu bar file converter

Drag files in Finder, hold a modifier, and drop on a wheel of outputs.
Conversions run entirely on-device through ImageIO, PDFKit, AVFoundation,
the bundled CLI tools, and optional ffmpeg/LibreOffice.

Includes a --self-test suite (41 checks) and a --convert CLI mode so the
conversion matrix can be verified without the UI.
This commit is contained in:
2026-10-20 00:36:40 +08:00
commit 2d031e58a4
47 changed files with 9149 additions and 0 deletions
+190
View File
@@ -0,0 +1,190 @@
import AppKit
import SwiftUI
/// The form a tool needs before it can run (quality, trim points, password …).
/// Values are remembered per tool, so the second time is still one drop.
final class OptionsWindowController: NSObject {
static let shared = OptionsWindowController()
private var window: NSWindow?
private var tool: ToolID?
private var urls: [URL] = []
private var origin: CGPoint?
private var params = ToolParams()
private override init() { super.init() }
func present(tool: ToolID, urls: [URL], origin: CGPoint?) {
self.tool = tool
self.urls = urls
self.origin = origin
params = Settings.shared.params(for: tool)
params.fillMissing(from: tool)
let view = OptionsView(
tool: tool,
params: params,
fileCount: urls.count,
onApply: { [weak self] values in self?.apply(values) },
onCancel: { [weak self] in self?.close() }
)
close()
let window = NSWindow(
contentRect: NSRect(x: 0, y: 0, width: 340, height: 120),
styleMask: [.titled, .closable, .fullSizeContentView],
backing: .buffered,
defer: false
)
window.title = tool.title
window.titlebarAppearsTransparent = true
window.isMovableByWindowBackground = true
window.level = .floating
window.contentView = NSHostingView(rootView: view)
window.center()
if let origin, let screen = NSScreen.screens.first(where: { $0.frame.contains(origin) }) ?? NSScreen.main {
let frame = screen.visibleFrame
window.setFrameOrigin(CGPoint(
x: min(max(frame.minX + 20, origin.x - 170), frame.maxX - 360),
y: min(max(frame.minY + 20, origin.y - 180), frame.maxY - 220)
))
}
NSApp.activate(ignoringOtherApps: true)
window.makeKeyAndOrderFront(nil)
self.window = window
}
private func apply(_ values: ToolParams) {
guard let tool else { return }
Settings.shared.store(params: values, for: tool)
let captured = urls
let point = origin
close()
JobStore.shared.submit(urls: captured, target: .tool(tool), params: values, origin: point)
}
private func close() {
window?.orderOut(nil)
window = nil
}
}
struct OptionsView: View {
let tool: ToolID
let fileCount: Int
let onApply: (ToolParams) -> Void
let onCancel: () -> Void
@State private var values: [String: String] = [:]
init(
tool: ToolID,
params: ToolParams,
fileCount: Int,
onApply: @escaping (ToolParams) -> Void,
onCancel: @escaping () -> Void
) {
self.tool = tool
self.fileCount = fileCount
self.onApply = onApply
self.onCancel = onCancel
var initial: [String: String] = [:]
for field in tool.fields {
initial[field.id] = params[field.id] ?? field.defaultValue
}
_values = State(initialValue: initial)
}
var body: some View {
VStack(alignment: .leading, spacing: 12) {
HStack(spacing: 8) {
Image(systemName: tool.symbol)
.font(.system(size: 15, weight: .semibold))
.foregroundStyle(Color.accentColor)
VStack(alignment: .leading, spacing: 1) {
Text(tool.title)
.font(.system(size: 13, weight: .semibold))
Text(fileCount == 1 ? "1 file" : "\(fileCount) files")
.font(.system(size: 10.5))
.foregroundStyle(.secondary)
}
}
ForEach(tool.fields) { field in
fieldView(field)
}
HStack {
Spacer()
Button("Cancel", action: onCancel)
.keyboardShortcut(.cancelAction)
Button("Apply") {
var params = ToolParams()
for (key, value) in values { params[key] = value }
onApply(params)
}
.keyboardShortcut(.defaultAction)
}
}
.padding(16)
.frame(width: 340)
}
@ViewBuilder
private func fieldView(_ field: ToolField) -> some View {
VStack(alignment: .leading, spacing: 3) {
HStack {
Text(field.label)
.font(.system(size: 11, weight: .medium))
Spacer()
if let unit = field.unit {
Text(unit)
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
}
control(for: field)
if let help = field.help {
Text(help)
.font(.system(size: 9.5))
.foregroundStyle(.secondary)
}
}
}
@ViewBuilder
private func control(for field: ToolField) -> some View {
let binding = Binding<String>(
get: { values[field.id] ?? field.defaultValue },
set: { values[field.id] = $0 }
)
switch field.kind {
case .text:
TextField("", text: binding)
.textFieldStyle(.roundedBorder)
case .secret:
SecureField("", text: binding)
.textFieldStyle(.roundedBorder)
case .toggle:
Toggle("", isOn: Binding(
get: { (values[field.id] ?? field.defaultValue) == "true" },
set: { values[field.id] = $0 ? "true" : "false" }
))
.labelsHidden()
.toggleStyle(.switch)
case .choice(let options):
Picker("", selection: binding) {
ForEach(options, id: \.self) { option in
Text(option).tag(option)
}
}
.labelsHidden()
.pickerStyle(.menu)
case .integer, .decimal, .timecode:
TextField("", text: binding)
.textFieldStyle(.roundedBorder)
.font(.system(size: 12, design: .monospaced))
}
}
}