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
+232
View File
@@ -0,0 +1,232 @@
import AppKit
import Foundation
import PDFKit
import QuickLookThumbnailing
import SwiftUI
/// "Float": a borderless preview that hovers over other windows, the same
/// gesture Tangerine offers from the Control wheel.
final class FloatPanel: NSObject {
static let shared = FloatPanel()
private var window: NSWindow?
private var urls: [URL] = []
private var index = 0
private override init() { super.init() }
@MainActor
func show(urls: [URL]) {
self.urls = urls
index = 0
let window = ensureWindow()
if let screen = NSScreen.screens.first(where: { $0.frame.contains(NSEvent.mouseLocation) }) ?? NSScreen.main {
let frame = screen.visibleFrame
let size = CGSize(width: 420, height: 320)
window.setContentSize(size)
window.setFrameOrigin(CGPoint(
x: min(max(frame.minX + 16, NSEvent.mouseLocation.x - size.width / 2), frame.maxX - size.width - 16),
y: min(max(frame.minY + 16, NSEvent.mouseLocation.y - size.height - 24), frame.maxY - size.height - 16)
))
}
window.alphaValue = 0
window.orderFrontRegardless()
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.18
window.animator().alphaValue = 1
}
render()
}
@MainActor
private func ensureWindow() -> NSWindow {
if let window { return window }
let window = OverlayWindow(
contentRect: NSRect(x: 0, y: 0, width: 420, height: 320),
styleMask: [.borderless],
backing: .buffered,
defer: false
)
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = true
window.level = .floating
window.isMovableByWindowBackground = true
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary]
window.contentView = NSHostingView(rootView: FloatView(controller: self))
self.window = window
return window
}
@MainActor
func render() {
guard let window, let content = window.contentView as? NSHostingView<FloatView> else { return }
content.rootView = FloatView(controller: self)
}
@MainActor
func close() {
guard let window else { return }
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.15
window.animator().alphaValue = 0
} completionHandler: {
window.orderOut(nil)
}
urls = []
}
@MainActor
func step(_ delta: Int) {
guard !urls.isEmpty else { return }
index = ((index + delta) % urls.count + urls.count) % urls.count
render()
}
var currentURL: URL? {
urls.indices.contains(index) ? urls[index] : nil
}
var counter: String? {
urls.count > 1 ? "\(index + 1) of \(urls.count)" : nil
}
@MainActor
func openInDefaultApp() {
guard let url = currentURL else { return }
NSWorkspace.shared.open(url)
}
@MainActor
func reveal() {
guard let url = currentURL else { return }
NSWorkspace.shared.activateFileViewerSelecting([url])
}
}
struct FloatView: View {
let controller: FloatPanel
@State private var image: NSImage?
@State private var failure: String?
@State private var isLoading = true
var body: some View {
VStack(spacing: 0) {
Group {
if let image {
Image(nsImage: image)
.resizable()
.aspectRatio(contentMode: .fit)
.padding(10)
} else if let failure {
VStack(spacing: 6) {
Image(systemName: "eye.slash")
.font(.system(size: 22))
Text(failure)
.font(.system(size: 11))
.multilineTextAlignment(.center)
}
.foregroundStyle(.secondary)
.padding()
} else {
ProgressView()
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
Divider().opacity(0.35)
HStack(spacing: 10) {
Image(systemName: "doc")
.font(.system(size: 11))
Text(controller.currentURL?.lastPathComponent ?? "")
.font(.system(size: 11, weight: .medium))
.lineLimit(1)
.truncationMode(.middle)
if let counter = controller.counter {
Text(counter)
.font(.system(size: 10))
.foregroundStyle(.secondary)
}
Spacer()
Button {
controller.step(-1)
} label: { Image(systemName: "chevron.left") }
.buttonStyle(.plain)
Button {
controller.step(1)
} label: { Image(systemName: "chevron.right") }
.buttonStyle(.plain)
Button("Open") { controller.openInDefaultApp() }
.buttonStyle(.plain)
.font(.system(size: 11, weight: .medium))
.foregroundStyle(Color.accentColor)
Button {
controller.close()
} label: { Image(systemName: "xmark") }
.buttonStyle(.plain)
}
.padding(.horizontal, 12)
.padding(.vertical, 8)
}
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 14, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 14, style: .continuous)
.strokeBorder(Color.primary.opacity(0.1))
)
.task(id: controller.currentURL) {
await load()
}
}
private func load() async {
guard let url = controller.currentURL else {
image = nil
failure = "Nothing to preview"
isLoading = false
return
}
isLoading = true
failure = nil
// Images and PDFs get a real render; everything else falls back to a
// Quick Look thumbnail.
if FileKind.category(of: url) == .image, let loaded = NSImage(contentsOf: url) {
image = loaded
isLoading = false
return
}
if url.pathExtension.lowercased() == "pdf", let page = PDFKitPreview.image(for: url) {
image = page
isLoading = false
return
}
let request = QLThumbnailGenerator.Request(
fileAt: url,
size: CGSize(width: 900, height: 700),
scale: NSScreen.main?.backingScaleFactor ?? 2,
representationTypes: .all
)
let representation = try? await QLThumbnailGenerator.shared.generateBestRepresentation(for: request)
if let cgImage = representation?.cgImage {
image = NSImage(cgImage: cgImage, size: NSSize(width: cgImage.width / 2, height: cgImage.height / 2))
} else {
image = nil
failure = "No preview available for .\(url.pathExtension)"
}
isLoading = false
}
}
enum PDFKitPreview {
static func image(for url: URL) -> NSImage? {
guard let document = PDFDocument(url: url), let page = document.page(at: 0) else { return nil }
let bounds = page.bounds(for: .mediaBox)
let scale: CGFloat = 1.6
let size = CGSize(width: bounds.width * scale, height: bounds.height * scale)
let thumbnail = page.thumbnail(of: size, for: .mediaBox)
return thumbnail
}
}