chore: adiciona .gitignore e commit.command
This commit is contained in:
@@ -0,0 +1,172 @@
|
||||
//
|
||||
// HexCapsuleView.swift
|
||||
// Hex
|
||||
//
|
||||
// Created by Kit Langton on 1/25/25.
|
||||
|
||||
import AppKit
|
||||
import Inject
|
||||
import Pow
|
||||
import SwiftUI
|
||||
|
||||
struct TranscriptionIndicatorView: View {
|
||||
@ObserveInjection var inject
|
||||
|
||||
enum Status {
|
||||
case hidden
|
||||
case optionKeyPressed
|
||||
case recording
|
||||
case transcribing
|
||||
case prewarming
|
||||
}
|
||||
|
||||
var status: Status
|
||||
var meter: Meter
|
||||
|
||||
let transcribeBaseColor: Color = .blue
|
||||
private var backgroundColor: Color {
|
||||
switch status {
|
||||
case .hidden: return Color.clear
|
||||
case .optionKeyPressed: return Color.black
|
||||
case .recording:
|
||||
return mixedColor(mixedNSColor(.red, with: .black, by: 0.5), with: .red, by: meter.averagePower * 3)
|
||||
case .transcribing: return mixedColor(.blue, with: .black, by: 0.5)
|
||||
case .prewarming: return mixedColor(.blue, with: .black, by: 0.5)
|
||||
}
|
||||
}
|
||||
|
||||
private var strokeColor: Color {
|
||||
switch status {
|
||||
case .hidden: return Color.clear
|
||||
case .optionKeyPressed: return Color.black
|
||||
case .recording: return mixedColor(.red, with: .white, by: 0.1).opacity(0.6)
|
||||
case .transcribing: return mixedColor(.blue, with: .white, by: 0.1).opacity(0.6)
|
||||
case .prewarming: return mixedColor(.blue, with: .white, by: 0.1).opacity(0.6)
|
||||
}
|
||||
}
|
||||
|
||||
private func mixedColor(_ color: NSColor, with otherColor: NSColor, by fraction: Double) -> Color {
|
||||
Color(nsColor: mixedNSColor(color, with: otherColor, by: fraction))
|
||||
}
|
||||
|
||||
private func mixedNSColor(_ color: NSColor, with otherColor: NSColor, by fraction: Double) -> NSColor {
|
||||
let clampedFraction = min(max(fraction, 0), 1)
|
||||
return color.blended(withFraction: clampedFraction, of: otherColor) ?? color
|
||||
}
|
||||
|
||||
private var innerShadowColor: Color {
|
||||
switch status {
|
||||
case .hidden: return Color.clear
|
||||
case .optionKeyPressed: return Color.clear
|
||||
case .recording: return Color.red
|
||||
case .transcribing: return transcribeBaseColor
|
||||
case .prewarming: return transcribeBaseColor
|
||||
}
|
||||
}
|
||||
|
||||
private let cornerRadius: CGFloat = 8
|
||||
private let baseWidth: CGFloat = 16
|
||||
private let expandedWidth: CGFloat = 56
|
||||
|
||||
var isHidden: Bool {
|
||||
status == .hidden
|
||||
}
|
||||
|
||||
@State var transcribeEffect = 0
|
||||
|
||||
var body: some View {
|
||||
let averagePower = min(1, meter.averagePower * 3)
|
||||
let peakPower = min(1, meter.peakPower * 3)
|
||||
ZStack {
|
||||
Capsule()
|
||||
.fill(backgroundColor.shadow(.inner(color: innerShadowColor, radius: 4)))
|
||||
.overlay {
|
||||
Capsule()
|
||||
.stroke(strokeColor, lineWidth: 1)
|
||||
.blendMode(.screen)
|
||||
}
|
||||
.overlay(alignment: .center) {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.red.opacity(status == .recording ? (averagePower < 0.1 ? averagePower / 0.1 : 1) : 0))
|
||||
.blur(radius: 2)
|
||||
.blendMode(.screen)
|
||||
.padding(6)
|
||||
}
|
||||
.overlay(alignment: .center) {
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.white.opacity(status == .recording ? (averagePower < 0.1 ? averagePower / 0.1 : 0.5) : 0))
|
||||
.blur(radius: 1)
|
||||
.blendMode(.screen)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.padding(7)
|
||||
}
|
||||
.overlay(alignment: .center) {
|
||||
GeometryReader { proxy in
|
||||
RoundedRectangle(cornerRadius: cornerRadius)
|
||||
.fill(Color.red.opacity(status == .recording ? (peakPower < 0.1 ? (peakPower / 0.1) * 0.5 : 0.5) : 0))
|
||||
.frame(width: max(proxy.size.width * (peakPower + 0.6), 0), height: proxy.size.height, alignment: .center)
|
||||
.frame(maxWidth: .infinity, alignment: .center)
|
||||
.blur(radius: 4)
|
||||
.blendMode(.screen)
|
||||
}.padding(6)
|
||||
}
|
||||
.cornerRadius(cornerRadius)
|
||||
.shadow(
|
||||
color: status == .recording ? .red.opacity(averagePower) : .red.opacity(0),
|
||||
radius: 4
|
||||
)
|
||||
.shadow(
|
||||
color: status == .recording ? .red.opacity(averagePower * 0.5) : .red.opacity(0),
|
||||
radius: 8
|
||||
)
|
||||
.animation(.interactiveSpring(), value: meter)
|
||||
.frame(
|
||||
width: status == .recording ? expandedWidth : baseWidth,
|
||||
height: baseWidth
|
||||
)
|
||||
.opacity(status == .hidden ? 0 : 1)
|
||||
.scaleEffect(status == .hidden ? 0.0 : 1)
|
||||
.blur(radius: status == .hidden ? 4 : 0)
|
||||
.animation(.bouncy(duration: 0.3), value: status)
|
||||
.changeEffect(.glow(color: .red.opacity(0.5), radius: 8), value: status)
|
||||
.changeEffect(.shine(angle: .degrees(0), duration: 0.6), value: transcribeEffect)
|
||||
.compositingGroup()
|
||||
.task(id: status == .transcribing) {
|
||||
while status == .transcribing, !Task.isCancelled {
|
||||
transcribeEffect += 1
|
||||
try? await Task.sleep(for: .seconds(0.25))
|
||||
}
|
||||
}
|
||||
|
||||
// Show tooltip when prewarming
|
||||
if status == .prewarming {
|
||||
VStack(spacing: 4) {
|
||||
Text("Model prewarming...")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.padding(.horizontal, 8)
|
||||
.padding(.vertical, 4)
|
||||
.background(
|
||||
RoundedRectangle(cornerRadius: 4)
|
||||
.fill(Color.black.opacity(0.8))
|
||||
)
|
||||
}
|
||||
.offset(y: -24)
|
||||
.transition(.opacity)
|
||||
.zIndex(2)
|
||||
}
|
||||
}
|
||||
.enableInjection()
|
||||
}
|
||||
}
|
||||
|
||||
#Preview("HEX") {
|
||||
VStack(spacing: 8) {
|
||||
TranscriptionIndicatorView(status: .hidden, meter: .init(averagePower: 0, peakPower: 0))
|
||||
TranscriptionIndicatorView(status: .optionKeyPressed, meter: .init(averagePower: 0, peakPower: 0))
|
||||
TranscriptionIndicatorView(status: .recording, meter: .init(averagePower: 0.5, peakPower: 0.5))
|
||||
TranscriptionIndicatorView(status: .transcribing, meter: .init(averagePower: 0, peakPower: 0))
|
||||
TranscriptionIndicatorView(status: .prewarming, meter: .init(averagePower: 0, peakPower: 0))
|
||||
}
|
||||
.padding(40)
|
||||
}
|
||||
Reference in New Issue
Block a user