Files
TZGyn 2d031e58a4 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.
2026-10-20 00:36:40 +08:00

89 lines
3.4 KiB
Swift

import AppKit
import Foundation
import SwiftUI
/// Small, non-modal confirmations ("Path copied", "3 files converted").
/// Nothing here blocks the user's flow, which is the whole point of the app.
enum Notifier {
private static var toastWindow: NSWindow?
private static var dismissWorkItem: DispatchWorkItem?
@MainActor
static func flash(title: String, body: String? = nil, symbol: String = "checkmark.circle.fill") {
guard Settings.shared.showProgressWindow else { return }
show(title: title, body: body, symbol: symbol, tone: .success, duration: 2.0)
}
@MainActor
static func error(_ title: String, _ message: String) {
show(title: title, body: message, symbol: "exclamationmark.triangle.fill", tone: .failure, duration: 6.0)
}
private enum Tone { case success, failure }
@MainActor
private static func show(title: String, body: String?, symbol: String, tone: Tone, duration: TimeInterval) {
dismissWorkItem?.cancel()
toastWindow?.close()
let accent: Color = tone == .success ? .green : .orange
let view = HStack(spacing: 10) {
Image(systemName: symbol)
.font(.system(size: 16, weight: .semibold))
.foregroundStyle(accent)
VStack(alignment: .leading, spacing: 2) {
Text(title)
.font(.system(size: 12, weight: .semibold))
.lineLimit(1)
if let body, !body.isEmpty {
Text(body)
.font(.system(size: 11))
.foregroundStyle(.secondary)
.lineLimit(2)
.truncationMode(.middle)
}
}
}
.padding(.horizontal, 14)
.padding(.vertical, 10)
.frame(minWidth: 220, maxWidth: 420, alignment: .leading)
.background(.regularMaterial, in: RoundedRectangle(cornerRadius: 12, style: .continuous))
.overlay(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.strokeBorder(Color.primary.opacity(0.08))
)
let controller = NSHostingController(rootView: view)
let window = NSWindow(contentViewController: controller)
window.styleMask = [.borderless]
window.isOpaque = false
window.backgroundColor = .clear
window.hasShadow = true
window.level = .statusBar
window.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
window.ignoresMouseEvents = true
window.appearance = NSApp.effectiveAppearance
let size = controller.view.fittingSize
let screen = NSScreen.main ?? NSScreen.screens.first
if let frame = screen?.visibleFrame {
let origin = CGPoint(x: frame.midX - size.width / 2, y: frame.minY + 96)
window.setFrame(CGRect(origin: origin, size: size), display: true)
}
window.orderFrontRegardless()
toastWindow = window
let work = DispatchWorkItem { [weak window] in
NSAnimationContext.runAnimationGroup { context in
context.duration = 0.25
window?.animator().alphaValue = 0
} completionHandler: {
window?.close()
}
if toastWindow === window { toastWindow = nil }
}
dismissWorkItem = work
DispatchQueue.main.asyncAfter(deadline: .now() + duration, execute: work)
}
}