139 lines
6.0 KiB
Swift
139 lines
6.0 KiB
Swift
import SwiftUI
|
|
import SstUI
|
|
|
|
@main
|
|
struct GalleryApp: App {
|
|
init() {
|
|
// `SstGallery --snapshot out.png` renders both finishes to a PNG and exits (for checking without a screen grab).
|
|
let args = CommandLine.arguments
|
|
guard let flag = args.firstIndex(of: "--snapshot"), args.count > flag + 1 else { return }
|
|
let path = args[flag + 1]
|
|
Task { @MainActor in
|
|
let view = HStack(spacing: 0) {
|
|
Finish(scheme: .light, title: "Aluminium", frozen: true)
|
|
Finish(scheme: .dark, title: "Graphite", frozen: true)
|
|
}
|
|
.frame(width: Layout.windowDefaultWidth, height: Layout.windowDefaultHeight)
|
|
let renderer = ImageRenderer(content: view)
|
|
renderer.scale = 2
|
|
if let tiff = renderer.nsImage?.tiffRepresentation, let png = NSBitmapImageRep(data: tiff)?.representation(using: .png, properties: [:]) {
|
|
try? png.write(to: URL(fileURLWithPath: path))
|
|
}
|
|
exit(0)
|
|
}
|
|
}
|
|
|
|
var body: some Scene {
|
|
WindowGroup("Sst componenten") {
|
|
HStack(spacing: 0) {
|
|
Finish(scheme: .light, title: "Aluminium")
|
|
Finish(scheme: .dark, title: "Graphite")
|
|
}
|
|
.frame(minWidth: Layout.windowMinWidth, minHeight: Layout.windowMinHeight)
|
|
}
|
|
.defaultSize(width: Layout.windowDefaultWidth, height: Layout.windowDefaultHeight)
|
|
}
|
|
}
|
|
|
|
/// Fake speech level so the meter, LEDs and counter can be judged in motion.
|
|
private func simulatedDbfs(at t: Double) -> Double {
|
|
if t.truncatingRemainder(dividingBy: 9) > 6.2 { return -90 }
|
|
let syllable = floor(t * 4.2)
|
|
let r = abs(sin(syllable * 12.9898) * 43758.5453).truncatingRemainder(dividingBy: 1)
|
|
return r > 0.12 ? -38 + r * 22 : -90
|
|
}
|
|
|
|
struct Finish: View {
|
|
let scheme: ColorScheme
|
|
let title: String
|
|
var frozen = false
|
|
@State private var recording = true
|
|
@State private var started = Date()
|
|
@State private var query = ""
|
|
|
|
var body: some View {
|
|
Group {
|
|
if frozen {
|
|
content(elapsed: 12.3, dbfs: -26)
|
|
} else {
|
|
TimelineView(.animation(minimumInterval: 1.0 / 30.0)) { timeline in
|
|
let elapsed = timeline.date.timeIntervalSince(started)
|
|
let dbfs = recording ? simulatedDbfs(at: elapsed) : -120
|
|
content(elapsed: recording ? elapsed : 0, dbfs: dbfs)
|
|
}
|
|
}
|
|
}
|
|
.environment(\.colorScheme, scheme)
|
|
}
|
|
|
|
private func content(elapsed: Double, dbfs: Double) -> some View {
|
|
ZStack {
|
|
BrushedBackground()
|
|
VStack(alignment: .leading, spacing: Layout.panelGap) {
|
|
SilkLabel("Afwerking · \(title)")
|
|
VStack(alignment: .leading, spacing: Layout.panelGap) {
|
|
HStack(alignment: .top, spacing: Layout.panelGap) {
|
|
VUMeterView(levelDbfs: dbfs, isActive: recording, initialFraction: frozen ? VUScale.fraction(forDbfs: dbfs) : 0)
|
|
VStack(alignment: .leading, spacing: Layout.panelGap) {
|
|
SilkLabel("Tijd", style: Typography.labelSmall)
|
|
SegmentCounterView(Self.clock(elapsed))
|
|
SilkLabel("Niveau", style: Typography.labelSmall)
|
|
LEDBarView(fraction: VUScale.fraction(forDbfs: dbfs))
|
|
}
|
|
}
|
|
SeamDivider()
|
|
HStack(spacing: Layout.panelGap) {
|
|
Button { recording.toggle(); started = Date() } label: {
|
|
HStack { LampView(isOn: recording); Text("Rec") }
|
|
}
|
|
.buttonStyle(KeyButtonStyle(kind: .record, latched: recording))
|
|
Button("Stop") { recording = false }.buttonStyle(KeyButtonStyle())
|
|
Button("Kopieer") {}.buttonStyle(KeyButtonStyle())
|
|
Button("Uit") {}.buttonStyle(KeyButtonStyle()).disabled(true)
|
|
Spacer()
|
|
LampView(isOn: recording, large: true)
|
|
LampView(isOn: true, tint: .amber)
|
|
}
|
|
}
|
|
.padding(Layout.panelPadding)
|
|
.background(PanelBackground())
|
|
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
row("14:02:11", "Ik wil de meeting verplaatsen naar tomorrow morning.", selected: false)
|
|
row("14:03:40", "Hij stuurt het door naar Claude Code vandaag.", selected: true)
|
|
}
|
|
.background(PaperBackground())
|
|
|
|
HStack {
|
|
SilkLabel("Zoek")
|
|
Group {
|
|
if frozen {
|
|
Text("claude").textStyle(Typography.body).frame(maxWidth: .infinity, alignment: .leading)
|
|
} else {
|
|
TextField("", text: $query).textFieldStyle(.plain).textStyle(Typography.body)
|
|
}
|
|
}
|
|
.padding(Space.s3).background(WellBackground())
|
|
}
|
|
}
|
|
.padding(Layout.panelPadding)
|
|
}
|
|
}
|
|
|
|
private func row(_ time: String, _ text: String, selected: Bool) -> some View {
|
|
HStack(alignment: .firstTextBaseline, spacing: Space.s4) {
|
|
Text(time).textStyle(Typography.counterSmall).foregroundStyle(Palette.inkSecondary)
|
|
Text(text).textStyle(Typography.reading).foregroundStyle(Palette.inkPrimary)
|
|
Spacer()
|
|
}
|
|
.padding(.horizontal, Space.s4).padding(.vertical, Space.s3)
|
|
.frame(minHeight: Size.rowHeight)
|
|
.background(selected ? Palette.surfaceSelection : Color.clear)
|
|
}
|
|
|
|
private static func clock(_ seconds: Double) -> String {
|
|
let total = Int(seconds * 10)
|
|
return String(format: "%02d:%02d.%d", (total / 600) % 100, (total / 10) % 60, total % 10)
|
|
}
|
|
}
|