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,66 @@
|
||||
import Foundation
|
||||
|
||||
/// Where a produced file goes, and what it is called.
|
||||
enum OutputNaming {
|
||||
/// ``name.pdf`` for ``photo.jpg`` — appends ``-2``, ``-3`` … when the
|
||||
/// target already exists (unless the user asked to overwrite).
|
||||
static func destination(for source: URL, format: Format, settings: Settings = .shared) -> URL {
|
||||
let base = source.deletingPathExtension().lastPathComponent
|
||||
let directory: URL
|
||||
switch settings.outputLocation {
|
||||
case .besideOriginal:
|
||||
directory = source.deletingLastPathComponent()
|
||||
case .chosenFolder:
|
||||
let folder = settings.resolvedOutputFolder
|
||||
directory = FileManager.default.fileExists(atPath: folder.path)
|
||||
? folder
|
||||
: source.deletingLastPathComponent()
|
||||
}
|
||||
let candidate = directory.appendingPathComponent(base).appendingPathExtension(format.ext)
|
||||
return settings.conflictPolicy == .overwrite ? candidate : availableURL(for: candidate)
|
||||
}
|
||||
|
||||
/// A sibling of any output a tool produces (e.g. ``photo-compressed.jpg``).
|
||||
static func sibling(for source: URL, suffix: String, extension ext: String? = nil) -> URL {
|
||||
let base = source.deletingPathExtension().lastPathComponent
|
||||
let directory = source.deletingLastPathComponent()
|
||||
let resolvedExtension = ext ?? source.pathExtension
|
||||
return availableURL(
|
||||
for: directory.appendingPathComponent(base + suffix).appendingPathExtension(resolvedExtension)
|
||||
)
|
||||
}
|
||||
|
||||
/// A working directory for multi-file outputs (split pages, extracted
|
||||
/// images, unarchived trees).
|
||||
static func artifactDirectory(for source: URL, suffix: String) -> URL {
|
||||
let base = source.deletingPathExtension().lastPathComponent
|
||||
let directory = source.deletingLastPathComponent()
|
||||
let url = directory.appendingPathComponent(base + suffix, isDirectory: true)
|
||||
var candidate = url
|
||||
var index = 2
|
||||
while FileManager.default.fileExists(atPath: candidate.path) {
|
||||
candidate = directory.appendingPathComponent("\(base + suffix) \(index)", isDirectory: true)
|
||||
index += 1
|
||||
}
|
||||
return candidate
|
||||
}
|
||||
|
||||
/// ``foo 2.txt``, ``foo 3.txt`` … until the name is free.
|
||||
static func availableURL(for url: URL) -> URL {
|
||||
guard FileManager.default.fileExists(atPath: url.path) else { return url }
|
||||
let directory = url.deletingLastPathComponent()
|
||||
let ext = url.pathExtension
|
||||
let base = url.deletingPathExtension().lastPathComponent
|
||||
var index = 2
|
||||
while true {
|
||||
let name = ext.isEmpty ? "\(base) \(index)" : "\(base) \(index).\(ext)"
|
||||
let candidate = directory.appendingPathComponent(name)
|
||||
if !FileManager.default.fileExists(atPath: candidate.path) { return candidate }
|
||||
index += 1
|
||||
}
|
||||
}
|
||||
|
||||
static func uniqueChild(of directory: URL, suggestedName: String) -> URL {
|
||||
availableURL(for: directory.appendingPathComponent(suggestedName))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user