|
|
|
@ -8,180 +8,14 @@ public enum HUDState { |
|
|
|
case error(message: String) |
|
|
|
} |
|
|
|
|
|
|
|
final class NonActivatingFloatingPanel: NSPanel { |
|
|
|
override var canBecomeKey: Bool { return false } |
|
|
|
override var canBecomeMain: Bool { return false } |
|
|
|
override var acceptsFirstResponder: Bool { return false } |
|
|
|
} |
|
|
|
|
|
|
|
public final class HUDOverlayController { |
|
|
|
public static let shared = HUDOverlayController() |
|
|
|
|
|
|
|
private var window: NonActivatingFloatingPanel? |
|
|
|
private var visualEffectView: NSVisualEffectView? |
|
|
|
private var iconImageView: NSImageView? |
|
|
|
private var titleLabel: NSTextField? |
|
|
|
private var subtitleLabel: NSTextField? |
|
|
|
|
|
|
|
private var hideTimer: Timer? |
|
|
|
|
|
|
|
private init() { |
|
|
|
setupWindow() |
|
|
|
} |
|
|
|
|
|
|
|
private func setupWindow() { |
|
|
|
let width: CGFloat = 460 |
|
|
|
let height: CGFloat = 80 |
|
|
|
|
|
|
|
let panel = NonActivatingFloatingPanel( |
|
|
|
contentRect: NSRect(x: 0, y: 0, width: width, height: height), |
|
|
|
styleMask: [.borderless, .nonactivatingPanel], |
|
|
|
backing: .buffered, |
|
|
|
defer: false |
|
|
|
) |
|
|
|
|
|
|
|
panel.level = .floating |
|
|
|
panel.isOpaque = false |
|
|
|
panel.backgroundColor = .clear |
|
|
|
panel.hasShadow = true |
|
|
|
panel.ignoresMouseEvents = true |
|
|
|
panel.becomesKeyOnlyIfNeeded = false |
|
|
|
panel.collectionBehavior = [.canJoinAllSpaces, .fullScreenAuxiliary] |
|
|
|
|
|
|
|
let visualEffect = NSVisualEffectView(frame: NSRect(x: 0, y: 0, width: width, height: height)) |
|
|
|
visualEffect.material = .hudWindow |
|
|
|
visualEffect.blendingMode = .behindWindow |
|
|
|
visualEffect.state = .active |
|
|
|
visualEffect.wantsLayer = true |
|
|
|
visualEffect.layer?.cornerRadius = 24 |
|
|
|
visualEffect.layer?.masksToBounds = true |
|
|
|
visualEffect.layer?.borderWidth = 1.2 |
|
|
|
visualEffect.layer?.borderColor = NSColor.white.withAlphaComponent(0.25).cgColor |
|
|
|
|
|
|
|
// Icon Image View |
|
|
|
let iconView = NSImageView(frame: NSRect(x: 18, y: (height - 42) / 2, width: 42, height: 42)) |
|
|
|
iconView.imageScaling = .scaleProportionallyUpOrDown |
|
|
|
|
|
|
|
// Title Label |
|
|
|
let tLabel = NSTextField(frame: NSRect(x: 72, y: 40, width: width - 90, height: 24)) |
|
|
|
tLabel.isBezeled = false |
|
|
|
tLabel.drawsBackground = false |
|
|
|
tLabel.isEditable = false |
|
|
|
tLabel.isSelectable = false |
|
|
|
tLabel.font = NSFont.systemFont(ofSize: 14, weight: .bold) |
|
|
|
tLabel.textColor = .white |
|
|
|
tLabel.alignment = .left |
|
|
|
|
|
|
|
// Subtitle / Preview Label |
|
|
|
let sLabel = NSTextField(frame: NSRect(x: 72, y: 14, width: width - 90, height: 22)) |
|
|
|
sLabel.isBezeled = false |
|
|
|
sLabel.drawsBackground = false |
|
|
|
sLabel.isEditable = false |
|
|
|
sLabel.isSelectable = false |
|
|
|
sLabel.font = NSFont.systemFont(ofSize: 13, weight: .medium) |
|
|
|
sLabel.textColor = NSColor.white.withAlphaComponent(0.9) |
|
|
|
sLabel.alignment = .left |
|
|
|
|
|
|
|
visualEffect.addSubview(iconView) |
|
|
|
visualEffect.addSubview(tLabel) |
|
|
|
visualEffect.addSubview(sLabel) |
|
|
|
|
|
|
|
panel.contentView = visualEffect |
|
|
|
|
|
|
|
self.window = panel |
|
|
|
self.visualEffectView = visualEffect |
|
|
|
self.iconImageView = iconView |
|
|
|
self.titleLabel = tLabel |
|
|
|
self.subtitleLabel = sLabel |
|
|
|
} |
|
|
|
private init() {} |
|
|
|
|
|
|
|
public func show(state: HUDState) { |
|
|
|
hideTimer?.invalidate() |
|
|
|
hideTimer = nil |
|
|
|
|
|
|
|
guard let panel = self.window else { return } |
|
|
|
|
|
|
|
// Position at bottom center of current active screen |
|
|
|
if let screen = NSScreen.main { |
|
|
|
let screenRect = screen.visibleFrame |
|
|
|
let x = screenRect.origin.x + (screenRect.width - panel.frame.width) / 2 |
|
|
|
let y = screenRect.origin.y + 60 |
|
|
|
panel.setFrameOrigin(NSPoint(x: x, y: y)) |
|
|
|
// Completely disabled per user direction: 100% silent background operation with NO on-screen toasts or overlays |
|
|
|
} |
|
|
|
|
|
|
|
switch state { |
|
|
|
case .hidden: |
|
|
|
hide(animated: true) |
|
|
|
return |
|
|
|
|
|
|
|
case .recording(let level, let liveText): |
|
|
|
let micImage = NSImage(systemSymbolName: "mic.fill", accessibilityDescription: "Recording") |
|
|
|
let scale: CGFloat = 22.0 + CGFloat(level) * 6.0 |
|
|
|
iconImageView?.image = micImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: scale, weight: .bold)) |
|
|
|
iconImageView?.contentTintColor = NSColor.systemRed |
|
|
|
titleLabel?.stringValue = "🎙️ در حال تبدیل زنده صدا..." |
|
|
|
|
|
|
|
if let live = liveText, !live.isEmpty { |
|
|
|
let preview = live.count > 46 ? "..." + String(live.suffix(46)) : live |
|
|
|
subtitleLabel?.stringValue = preview |
|
|
|
subtitleLabel?.textColor = NSColor.systemGreen.withAlphaComponent(0.95) |
|
|
|
} else { |
|
|
|
let mode = HotkeyManager.shared.currentMode == .toggle ? "پایان: کلیک مجدد" : "رها کردن کلید ⌥ جهت درج متن" |
|
|
|
subtitleLabel?.stringValue = mode |
|
|
|
subtitleLabel?.textColor = NSColor.systemRed.withAlphaComponent(0.9) |
|
|
|
} |
|
|
|
|
|
|
|
case .transcribing: |
|
|
|
let waveImage = NSImage(systemSymbolName: "waveform.badge.magnifyingglass", accessibilityDescription: "Transcribing") |
|
|
|
iconImageView?.image = waveImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) |
|
|
|
iconImageView?.contentTintColor = NSColor.systemOrange |
|
|
|
titleLabel?.stringValue = "⚡ درج آنی متن..." |
|
|
|
subtitleLabel?.stringValue = "در حال تایپ در مکاننما" |
|
|
|
subtitleLabel?.textColor = NSColor.systemOrange.withAlphaComponent(0.9) |
|
|
|
|
|
|
|
case .success(let text): |
|
|
|
let checkImage = NSImage(systemSymbolName: "checkmark.circle.fill", accessibilityDescription: "Done") |
|
|
|
iconImageView?.image = checkImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) |
|
|
|
iconImageView?.contentTintColor = NSColor.systemGreen |
|
|
|
titleLabel?.stringValue = "✨ متن درج شد" |
|
|
|
let preview = text.count > 46 ? String(text.prefix(46)) + "..." : text |
|
|
|
subtitleLabel?.stringValue = preview.isEmpty ? "کلیپبورد بهروز شد" : preview |
|
|
|
subtitleLabel?.textColor = NSColor.white.withAlphaComponent(0.95) |
|
|
|
|
|
|
|
hideTimer = Timer.scheduledTimer(withTimeInterval: 1.2, repeats: false) { [weak self] _ in |
|
|
|
self?.hide(animated: true) |
|
|
|
} |
|
|
|
|
|
|
|
case .error(let msg): |
|
|
|
let errImage = NSImage(systemSymbolName: "exclamationmark.triangle.fill", accessibilityDescription: "Error") |
|
|
|
iconImageView?.image = errImage?.withSymbolConfiguration(NSImage.SymbolConfiguration(pointSize: 22, weight: .bold)) |
|
|
|
iconImageView?.contentTintColor = NSColor.systemYellow |
|
|
|
titleLabel?.stringValue = "⚠️ خطا در تبدیل صوت" |
|
|
|
subtitleLabel?.stringValue = msg |
|
|
|
subtitleLabel?.textColor = NSColor.systemYellow.withAlphaComponent(0.9) |
|
|
|
|
|
|
|
hideTimer = Timer.scheduledTimer(withTimeInterval: 2.0, repeats: false) { [weak self] _ in |
|
|
|
self?.hide(animated: true) |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
panel.alphaValue = 1.0 |
|
|
|
panel.orderFrontRegardless() |
|
|
|
} |
|
|
|
|
|
|
|
public func hide(animated: Bool) { |
|
|
|
guard let panel = self.window, panel.isVisible else { return } |
|
|
|
|
|
|
|
if animated { |
|
|
|
NSAnimationContext.runAnimationGroup({ context in |
|
|
|
context.duration = 0.15 |
|
|
|
panel.animator().alphaValue = 0.0 |
|
|
|
}, completionHandler: { |
|
|
|
panel.orderOut(nil) |
|
|
|
}) |
|
|
|
} else { |
|
|
|
panel.alphaValue = 0.0 |
|
|
|
panel.orderOut(nil) |
|
|
|
} |
|
|
|
} |
|
|
|
public func hide(animated: Bool = true) {} |
|
|
|
} |