ux: add hover/ edit close button and keep overlay until escape or send
parent
ec046411f1
commit
7a82777fc5
|
|
@ -46,7 +46,13 @@ final class VoiceWakeOverlayController: ObservableObject {
|
|||
self.updateWindowFrame(animate: true)
|
||||
}
|
||||
|
||||
func presentFinal(transcript: String, forwardConfig: VoiceWakeForwardConfig, delay: TimeInterval, attributed: NSAttributedString? = nil) {
|
||||
func presentFinal(
|
||||
transcript: String,
|
||||
forwardConfig: VoiceWakeForwardConfig,
|
||||
delay: TimeInterval,
|
||||
sendChime: VoiceWakeChime = .none,
|
||||
attributed: NSAttributedString? = nil)
|
||||
{
|
||||
self.autoSendTask?.cancel()
|
||||
self.forwardConfig = forwardConfig
|
||||
self.model.text = transcript
|
||||
|
|
@ -56,7 +62,7 @@ final class VoiceWakeOverlayController: ObservableObject {
|
|||
self.model.isEditing = false
|
||||
self.model.attributed = attributed ?? self.makeAttributed(from: transcript)
|
||||
self.present()
|
||||
self.scheduleAutoSend(after: delay)
|
||||
self.scheduleAutoSend(after: delay, sendChime: sendChime)
|
||||
}
|
||||
|
||||
func userBeganEditing() {
|
||||
|
|
@ -248,11 +254,14 @@ final class VoiceWakeOverlayController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private func scheduleAutoSend(after delay: TimeInterval) {
|
||||
private func scheduleAutoSend(after delay: TimeInterval, sendChime: VoiceWakeChime) {
|
||||
guard let forwardConfig, forwardConfig.enabled else { return }
|
||||
self.autoSendTask = Task { [weak self] in
|
||||
let nanos = UInt64(delay * 1_000_000_000)
|
||||
try? await Task.sleep(nanoseconds: nanos)
|
||||
if sendChime != .none {
|
||||
VoiceWakeChimePlayer.play(sendChime)
|
||||
}
|
||||
self?.sendNow()
|
||||
}
|
||||
}
|
||||
|
|
@ -267,11 +276,33 @@ final class VoiceWakeOverlayController: ObservableObject {
|
|||
}
|
||||
}
|
||||
|
||||
private struct CloseHoverButton: View {
|
||||
var onClose: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.onClose) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(Color.white.opacity(0.85))
|
||||
.frame(width: 22, height: 22)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: Color.black.opacity(0.35), radius: 6, y: 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.focusable(false)
|
||||
.contentShape(Circle())
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
|
||||
private struct VoiceWakeOverlayView: View {
|
||||
@ObservedObject var controller: VoiceWakeOverlayController
|
||||
@FocusState private var textFocused: Bool
|
||||
@State private var isHovering: Bool = false
|
||||
|
||||
var body: some View {
|
||||
ZStack(alignment: .topLeading) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
if self.controller.model.isEditing {
|
||||
TranscriptTextView(
|
||||
|
|
@ -336,6 +367,16 @@ private struct VoiceWakeOverlayView: View {
|
|||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
|
||||
.background(.regularMaterial)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
|
||||
.onHover { self.isHovering = $0 }
|
||||
|
||||
if self.controller.model.isEditing || self.isHovering {
|
||||
CloseHoverButton(onClose: {
|
||||
self.controller.cancelEditingAndDismiss()
|
||||
})
|
||||
.offset(x: -10, y: -10)
|
||||
.transition(AnyTransition.scale.combined(with: .opacity))
|
||||
}
|
||||
}
|
||||
.onAppear { self.textFocused = false }
|
||||
.onChange(of: self.controller.model.text) { _, _ in
|
||||
self.textFocused = self.controller.model.isEditing
|
||||
|
|
@ -502,6 +543,26 @@ private final class ClickCatcher: NSView {
|
|||
self.onTap()
|
||||
}
|
||||
}
|
||||
|
||||
private struct CloseHoverButton: View {
|
||||
var onClose: () -> Void
|
||||
|
||||
var body: some View {
|
||||
Button(action: self.onClose) {
|
||||
Image(systemName: "xmark")
|
||||
.font(.system(size: 12, weight: .bold))
|
||||
.foregroundColor(Color.white.opacity(0.85))
|
||||
.frame(width: 22, height: 22)
|
||||
.background(Color.black.opacity(0.35))
|
||||
.clipShape(Circle())
|
||||
.shadow(color: Color.black.opacity(0.35), radius: 6, y: 2)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
.focusable(false)
|
||||
.contentShape(Circle())
|
||||
.padding(6)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private extension NSAttributedString {
|
||||
|
|
|
|||
Loading…
Reference in New Issue