feat:mac os
This commit is contained in:
1 parent
136bf7cfa3
commit
3400e2b465
18 files changed
+1129
No files matched your search
@@ -0,0 +1,133 @@
|
||||
import AppKit
|
||||
import SwiftUI
|
||||
|
||||
@MainActor
|
||||
final class OverlayPanelController {
|
||||
private let panel: NSPanel
|
||||
|
||||
init(model: AppModel) {
|
||||
panel = NSPanel(
|
||||
contentRect: NSRect(x: 0, y: 0, width: 304, height: 82),
|
||||
styleMask: [.borderless, .nonactivatingPanel],
|
||||
backing: .buffered,
|
||||
defer: false
|
||||
)
|
||||
panel.level = .floating
|
||||
panel.isOpaque = false
|
||||
panel.backgroundColor = .clear
|
||||
panel.hasShadow = true
|
||||
panel.hidesOnDeactivate = false
|
||||
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary, .stationary]
|
||||
panel.contentView = NSHostingView(rootView: RecordingOverlay(model: model))
|
||||
}
|
||||
|
||||
func show() {
|
||||
positionPanel()
|
||||
panel.orderFrontRegardless()
|
||||
}
|
||||
|
||||
func hide() {
|
||||
panel.orderOut(nil)
|
||||
}
|
||||
|
||||
private func positionPanel() {
|
||||
let mouse = NSEvent.mouseLocation
|
||||
let screen = NSScreen.screens.first { NSMouseInRect(mouse, $0.frame, false) } ?? NSScreen.main
|
||||
guard let frame = screen?.visibleFrame else { return }
|
||||
let x = frame.midX - panel.frame.width / 2
|
||||
let y = frame.minY + 42
|
||||
panel.setFrameOrigin(NSPoint(x: x, y: y))
|
||||
}
|
||||
}
|
||||
|
||||
private struct RecordingOverlay: View {
|
||||
@ObservedObject var model: AppModel
|
||||
private let teal = Color(red: 0.039, green: 0.624, blue: 0.604)
|
||||
|
||||
var body: some View {
|
||||
HStack(spacing: 14) {
|
||||
signal
|
||||
.frame(width: 46, height: 34)
|
||||
VStack(alignment: .leading, spacing: 3) {
|
||||
Text(title)
|
||||
.font(.system(size: 13, weight: .semibold))
|
||||
Text(detail)
|
||||
.font(.system(size: 12, design: .monospaced))
|
||||
.foregroundStyle(.secondary)
|
||||
.lineLimit(1)
|
||||
}
|
||||
Spacer(minLength: 6)
|
||||
if model.isRecording {
|
||||
Button(action: model.toggleRecording) {
|
||||
Image(systemName: "stop.fill")
|
||||
.font(.system(size: 11, weight: .bold))
|
||||
.frame(width: 28, height: 28)
|
||||
}
|
||||
.buttonStyle(.borderedProminent)
|
||||
.tint(teal)
|
||||
.clipShape(Circle())
|
||||
.help("Stop and transcribe")
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 17)
|
||||
.frame(width: 304, height: 82)
|
||||
.background(.ultraThickMaterial, in: RoundedRectangle(cornerRadius: 19, style: .continuous))
|
||||
.overlay {
|
||||
RoundedRectangle(cornerRadius: 19, style: .continuous)
|
||||
.strokeBorder(.white.opacity(0.13))
|
||||
}
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var signal: some View {
|
||||
switch model.state {
|
||||
case .recording:
|
||||
HStack(alignment: .center, spacing: 3) {
|
||||
ForEach(0..<5, id: \.self) { index in
|
||||
Capsule()
|
||||
.fill(teal)
|
||||
.frame(width: 4, height: barHeight(index))
|
||||
}
|
||||
}
|
||||
case .preparing, .transcribing:
|
||||
ProgressView().controlSize(.small).tint(teal)
|
||||
case .success:
|
||||
Image(systemName: "checkmark.circle.fill").font(.title2).foregroundStyle(teal)
|
||||
case .failure:
|
||||
Image(systemName: "exclamationmark.triangle.fill").font(.title3).foregroundStyle(.red)
|
||||
case .idle:
|
||||
Image(systemName: "waveform").foregroundStyle(teal)
|
||||
}
|
||||
}
|
||||
|
||||
private var title: String {
|
||||
switch model.state {
|
||||
case .preparing: "Starting microphone"
|
||||
case .recording: "Recording"
|
||||
case .transcribing: "Transcribing"
|
||||
case .success(let message), .failure(let message): message
|
||||
case .idle: "Ready"
|
||||
}
|
||||
}
|
||||
|
||||
private var detail: String {
|
||||
switch model.state {
|
||||
case .recording: Self.duration(model.elapsed)
|
||||
case .preparing: "Waiting for permission"
|
||||
case .transcribing: "Sending audio"
|
||||
case .failure: "Open Transui settings to fix this"
|
||||
case .success: "Done"
|
||||
case .idle: model.settings.shortcut.displayName
|
||||
}
|
||||
}
|
||||
|
||||
private func barHeight(_ index: Int) -> CGFloat {
|
||||
let shape = [0.55, 0.82, 1.0, 0.72, 0.45][index]
|
||||
return 7 + 24 * max(0.08, model.level * shape)
|
||||
}
|
||||
|
||||
private static func duration(_ interval: TimeInterval) -> String {
|
||||
let seconds = Int(interval)
|
||||
return String(format: "%02d:%02d", seconds / 60, seconds % 60)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user