feat: add outcome-based dismiss animations
parent
d733d246f0
commit
81db44f584
|
|
@ -65,12 +65,12 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||||
func sendNow() {
|
func sendNow() {
|
||||||
self.autoSendTask?.cancel()
|
self.autoSendTask?.cancel()
|
||||||
guard let forwardConfig, forwardConfig.enabled else {
|
guard let forwardConfig, forwardConfig.enabled else {
|
||||||
self.dismiss()
|
self.dismiss(reason: .explicit)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
let text = self.model.text.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||||
guard !text.isEmpty else {
|
guard !text.isEmpty else {
|
||||||
self.dismiss()
|
self.dismiss(reason: .empty)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,17 +80,21 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||||
await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig)
|
await VoiceWakeForwarder.forward(transcript: payload, config: forwardConfig)
|
||||||
}
|
}
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.28) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.28) {
|
||||||
self.dismiss()
|
self.dismiss(reason: .explicit, outcome: .sent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func dismiss(reason: DismissReason = .explicit) {
|
func dismiss(reason: DismissReason = .explicit, outcome: SendOutcome = .empty) {
|
||||||
self.autoSendTask?.cancel()
|
self.autoSendTask?.cancel()
|
||||||
self.model.isSending = false
|
self.model.isSending = false
|
||||||
guard let window else { return }
|
guard let window else { return }
|
||||||
|
let target = self.dismissTargetFrame(for: window.frame, reason: reason, outcome: outcome)
|
||||||
NSAnimationContext.runAnimationGroup { context in
|
NSAnimationContext.runAnimationGroup { context in
|
||||||
context.duration = 0.18
|
context.duration = 0.18
|
||||||
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
|
||||||
|
if let target {
|
||||||
|
window.animator().setFrame(target, display: true)
|
||||||
|
}
|
||||||
window.animator().alphaValue = 0
|
window.animator().alphaValue = 0
|
||||||
} completionHandler: {
|
} completionHandler: {
|
||||||
Task { @MainActor in
|
Task { @MainActor in
|
||||||
|
|
@ -101,6 +105,7 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||||
}
|
}
|
||||||
|
|
||||||
enum DismissReason { case explicit, empty }
|
enum DismissReason { case explicit, empty }
|
||||||
|
enum SendOutcome { case sent, empty }
|
||||||
|
|
||||||
// MARK: - Private
|
// MARK: - Private
|
||||||
|
|
||||||
|
|
@ -183,6 +188,21 @@ final class VoiceWakeOverlayController: ObservableObject {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private func dismissTargetFrame(for frame: NSRect, reason: DismissReason, outcome: SendOutcome) -> NSRect? {
|
||||||
|
switch (reason, outcome) {
|
||||||
|
case (.empty, _):
|
||||||
|
let scale: CGFloat = 0.95
|
||||||
|
let newSize = NSSize(width: frame.size.width * scale, height: frame.size.height * scale)
|
||||||
|
let dx = (frame.size.width - newSize.width) / 2
|
||||||
|
let dy = (frame.size.height - newSize.height) / 2
|
||||||
|
return NSRect(x: frame.origin.x + dx, y: frame.origin.y + dy, width: newSize.width, height: newSize.height)
|
||||||
|
case (.explicit, .sent):
|
||||||
|
return frame.offsetBy(dx: 8, dy: 6)
|
||||||
|
default:
|
||||||
|
return frame
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private func scheduleAutoSend(after delay: TimeInterval) {
|
private func scheduleAutoSend(after delay: TimeInterval) {
|
||||||
guard let forwardConfig, forwardConfig.enabled else { return }
|
guard let forwardConfig, forwardConfig.enabled else { return }
|
||||||
self.autoSendTask = Task { [weak self] in
|
self.autoSendTask = Task { [weak self] in
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue