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:
@@ -0,0 +1,113 @@
|
||||
import AppKit
|
||||
|
||||
// Draws the Konverter icon: a warm tangerine squircle with a radial conversion
|
||||
// wheel on it. Everything is vector-drawn so the script has no dependencies.
|
||||
|
||||
let canvas: CGFloat = 1024
|
||||
let size = NSSize(width: canvas, height: canvas)
|
||||
guard let rep = NSBitmapImageRep(
|
||||
bitmapDataPlanes: nil, pixelsWide: Int(canvas), pixelsHigh: Int(canvas),
|
||||
bitsPerSample: 8, samplesPerPixel: 4, hasAlpha: true, isPlanar: false,
|
||||
colorSpaceName: .deviceRGB, bytesPerRow: 0, bitsPerPixel: 0
|
||||
) else { exit(1) }
|
||||
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
NSGraphicsContext.current = NSGraphicsContext(bitmapImageRep: rep)
|
||||
NSGraphicsContext.current?.imageInterpolation = .high
|
||||
|
||||
let rect = NSRect(origin: .zero, size: size)
|
||||
|
||||
// macOS icons live inside a ~10% margin; the squircle fills the rest.
|
||||
let body = rect.insetBy(dx: 96, dy: 96)
|
||||
let squircle = NSBezierPath(roundedRect: body, xRadius: 200, yRadius: 200)
|
||||
|
||||
let gradient = NSGradient(colors: [
|
||||
NSColor(calibratedRed: 1.00, green: 0.66, blue: 0.20, alpha: 1),
|
||||
NSColor(calibratedRed: 0.94, green: 0.33, blue: 0.11, alpha: 1),
|
||||
])!
|
||||
gradient.draw(in: squircle, angle: -60)
|
||||
|
||||
// Soft top highlight.
|
||||
NSGraphicsContext.saveGraphicsState()
|
||||
squircle.addClip()
|
||||
NSGradient(colors: [
|
||||
NSColor.white.withAlphaComponent(0.28),
|
||||
NSColor.white.withAlphaComponent(0.00),
|
||||
])!.draw(in: NSRect(x: body.minX, y: body.midY, width: body.width, height: body.height / 2), angle: -90)
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
// Radial wheel: eight segments, one of them highlighted as the selection.
|
||||
let center = NSPoint(x: rect.midX, y: rect.midY + 24)
|
||||
let outer: CGFloat = 296
|
||||
let inner: CGFloat = 150
|
||||
let segmentCount = 8
|
||||
let gap: CGFloat = 3.2
|
||||
|
||||
func polar(_ radius: CGFloat, _ degrees: CGFloat) -> NSPoint {
|
||||
let radians = degrees * .pi / 180
|
||||
return NSPoint(x: center.x + radius * cos(radians), y: center.y + radius * sin(radians))
|
||||
}
|
||||
|
||||
func segment(_ index: Int, from start: CGFloat, to end: CGFloat) -> NSBezierPath {
|
||||
let path = NSBezierPath()
|
||||
path.move(to: polar(inner, start))
|
||||
path.line(to: polar(outer, start))
|
||||
path.appendArc(withCenter: center, radius: outer, startAngle: start, endAngle: end)
|
||||
path.line(to: polar(inner, end))
|
||||
path.appendArc(withCenter: center, radius: inner, startAngle: end, endAngle: start, clockwise: true)
|
||||
path.close()
|
||||
return path
|
||||
}
|
||||
|
||||
for index in 0..<segmentCount {
|
||||
let centre = 90 - CGFloat(index) * (360 / CGFloat(segmentCount))
|
||||
let start = centre - (360 / CGFloat(segmentCount)) / 2 + gap
|
||||
let end = centre + (360 / CGFloat(segmentCount)) / 2 - gap
|
||||
let path = segment(index, from: start, to: end)
|
||||
// Index 4 sits at -90°, i.e. straight down — where the arrow points.
|
||||
let selected = index == 4
|
||||
(selected ? NSColor.white : NSColor.white.withAlphaComponent(0.34)).setFill()
|
||||
path.fill()
|
||||
}
|
||||
|
||||
// Hub with a downward arrow: the drop that performs the conversion.
|
||||
let hubRadius: CGFloat = 118
|
||||
let hubRect = NSRect(
|
||||
x: center.x - hubRadius, y: center.y - hubRadius,
|
||||
width: hubRadius * 2, height: hubRadius * 2
|
||||
)
|
||||
NSColor(calibratedRed: 0.16, green: 0.10, blue: 0.10, alpha: 0.92).setFill()
|
||||
NSBezierPath(ovalIn: hubRect).fill()
|
||||
|
||||
NSColor.white.setFill()
|
||||
NSBezierPath(ovalIn: hubRect.insetBy(dx: 14, dy: 14)).fill()
|
||||
|
||||
// Arrow glyph, pointing at the selected segment.
|
||||
let arrow = NSBezierPath()
|
||||
let up = center.y + 58
|
||||
let down = center.y - 58
|
||||
let shaftWidth: CGFloat = 26
|
||||
arrow.move(to: NSPoint(x: center.x - shaftWidth / 2, y: up))
|
||||
arrow.line(to: NSPoint(x: center.x + shaftWidth / 2, y: up))
|
||||
arrow.line(to: NSPoint(x: center.x + shaftWidth / 2, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x + 62, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x, y: down - 32))
|
||||
arrow.line(to: NSPoint(x: center.x - 62, y: down + 30))
|
||||
arrow.line(to: NSPoint(x: center.x - shaftWidth / 2, y: down + 30))
|
||||
arrow.close()
|
||||
arrow.lineJoinStyle = .round
|
||||
arrow.lineWidth = 6
|
||||
NSColor(calibratedRed: 0.86, green: 0.28, blue: 0.09, alpha: 1).setFill()
|
||||
NSColor(calibratedRed: 0.86, green: 0.28, blue: 0.09, alpha: 1).setStroke()
|
||||
arrow.fill()
|
||||
arrow.stroke()
|
||||
|
||||
NSGraphicsContext.restoreGraphicsState()
|
||||
|
||||
guard CommandLine.arguments.count >= 2 else { exit(1) }
|
||||
guard let data = rep.representation(using: .png, properties: [:]) else { exit(1) }
|
||||
do {
|
||||
try data.write(to: URL(fileURLWithPath: CommandLine.arguments[1]))
|
||||
} catch {
|
||||
exit(1)
|
||||
}
|
||||
Executable
+29
@@ -0,0 +1,29 @@
|
||||
#!/bin/bash
|
||||
# Builds Icon.icns from scripts/gen-icon.swift and drops it into Konverter.app.
|
||||
# Run ./build-app.sh first (this expects the bundle to exist).
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(dirname "$0")/.."
|
||||
|
||||
APP="Konverter.app"
|
||||
if [ ! -d "$APP" ]; then
|
||||
echo "make-icon.sh: $APP not found — run ./build-app.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TMP=$(mktemp -d)
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
swift scripts/gen-icon.swift "$TMP/icon-1024.png"
|
||||
|
||||
mkdir -p "$TMP/Icon.iconset"
|
||||
for s in 16 32 128 256 512; do
|
||||
d=$((s * 2))
|
||||
sips -z "$s" "$s" "$TMP/icon-1024.png" --out "$TMP/Icon.iconset/icon_${s}x${s}.png" >/dev/null
|
||||
sips -z "$d" "$d" "$TMP/icon-1024.png" --out "$TMP/Icon.iconset/icon_${s}x${s}@2x.png" >/dev/null
|
||||
done
|
||||
|
||||
iconutil -c icns "$TMP/Icon.iconset" -o "$TMP/Icon.icns"
|
||||
mkdir -p "$APP/Contents/Resources"
|
||||
cp "$TMP/Icon.icns" "$APP/Contents/Resources/Icon.icns"
|
||||
echo "Generated $APP/Contents/Resources/Icon.icns"
|
||||
Reference in New Issue
Block a user