Added proper notification system

This commit is contained in:
Beingpax 2025-06-16 11:36:40 +05:45
parent bced13315d
commit 63ea51113f
4 changed files with 253 additions and 6 deletions

View File

@ -0,0 +1,125 @@
import SwiftUI
struct AppNotificationView: View {
let title: String
let message: String
let type: NotificationType
let duration: TimeInterval
let onClose: () -> Void
@State private var progress: Double = 1.0
@State private var timer: Timer?
enum NotificationType {
case error
case warning
case info
case success
var iconName: String {
switch self {
case .error: return "xmark.octagon.fill"
case .warning: return "exclamationmark.triangle.fill"
case .info: return "info.circle.fill"
case .success: return "checkmark.circle.fill"
}
}
var iconColor: Color {
switch self {
case .error: return .red
case .warning: return .yellow
case .info: return .blue
case .success: return .green
}
}
}
var body: some View {
ZStack {
// Main content
HStack(alignment: .center, spacing: 12) {
// App icon on the left side
if let appIcon = NSApp.applicationIconImage {
Image(nsImage: appIcon)
.resizable()
.aspectRatio(contentMode: .fit)
.frame(width: 50, height: 50)
.cornerRadius(10)
}
VStack(alignment: .leading, spacing: 3) {
Text(title)
.fontWeight(.bold)
.font(.system(size: 13))
.lineLimit(1)
Text(message)
.font(.system(size: 11))
.fontWeight(.semibold)
.opacity(0.9)
.lineLimit(2)
.multilineTextAlignment(.leading)
}
Spacer()
}
.padding(12)
.frame(height: 60) // Fixed compact height
// Close button overlaid on top-right
VStack {
HStack {
Spacer()
Button(action: onClose) {
Image(systemName: "xmark")
.font(.system(size: 11, weight: .medium))
.foregroundColor(.secondary)
}
.buttonStyle(PlainButtonStyle())
.frame(width: 16, height: 16)
}
Spacer()
}
.padding(8)
}
.frame(minWidth: 320, maxWidth: 420)
.background(
RoundedRectangle(cornerRadius: 12, style: .continuous)
.fill(Color(nsColor: .controlBackgroundColor).opacity(0.95))
)
.overlay(
// Progress bar at the bottom
VStack {
Spacer()
GeometryReader { geometry in
Rectangle()
.fill(Color.accentColor.opacity(0.6))
.frame(width: geometry.size.width * progress, height: 2)
.animation(.linear(duration: 0.1), value: progress)
}
.frame(height: 2)
}
.clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous))
)
.onAppear {
startProgressTimer()
}
.onDisappear {
timer?.invalidate()
}
}
private func startProgressTimer() {
let updateInterval: TimeInterval = 0.1
let totalSteps = duration / updateInterval
let stepDecrement = 1.0 / totalSteps
timer = Timer.scheduledTimer(withTimeInterval: updateInterval, repeats: true) { _ in
if progress > 0 {
progress -= stepDecrement
} else {
timer?.invalidate()
}
}
}
}

View File

@ -0,0 +1,107 @@
import SwiftUI
import AppKit
class NotificationManager {
static let shared = NotificationManager()
private var notificationWindow: NSPanel?
private var dismissTimer: Timer?
private init() {}
@MainActor
func showNotification(
title: String,
message: String,
type: AppNotificationView.NotificationType,
duration: TimeInterval = 8.0
) {
// If a notification is already showing, dismiss it before showing the new one.
if notificationWindow != nil {
dismissNotification()
}
let notificationView = AppNotificationView(
title: title,
message: message,
type: type,
duration: duration,
onClose: { [weak self] in
Task { @MainActor in
self?.dismissNotification()
}
}
)
let hostingController = NSHostingController(rootView: notificationView)
let size = hostingController.view.fittingSize
let panel = NSPanel(
contentRect: NSRect(origin: .zero, size: size),
styleMask: [.borderless, .nonactivatingPanel],
backing: .buffered,
defer: false
)
panel.contentView = hostingController.view
panel.isFloatingPanel = true
panel.level = .mainMenu
panel.backgroundColor = .clear
panel.hasShadow = false
panel.isMovableByWindowBackground = false
// Position at final location and start with fade animation
positionWindow(panel)
panel.alphaValue = 0
panel.makeKeyAndOrderFront(nil)
self.notificationWindow = panel
// Simple fade-in animation
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.3
context.timingFunction = CAMediaTimingFunction(name: .easeOut)
panel.animator().alphaValue = 1
})
dismissTimer?.invalidate()
dismissTimer = Timer.scheduledTimer(
withTimeInterval: duration,
repeats: false
) { [weak self] _ in
self?.dismissNotification()
}
}
@MainActor
private func positionWindow(_ window: NSWindow) {
guard let screen = NSScreen.main else { return }
let screenRect = screen.visibleFrame
let windowRect = window.frame
let x = screenRect.maxX - windowRect.width - 20 // 20px padding from the right
let y = screenRect.maxY - windowRect.height - 20 // 20px padding from the top
window.setFrameOrigin(NSPoint(x: x, y: y))
}
@MainActor
func dismissNotification() {
guard let window = notificationWindow else { return }
dismissTimer?.invalidate()
dismissTimer = nil
NSAnimationContext.runAnimationGroup({ context in
context.duration = 0.2
context.timingFunction = CAMediaTimingFunction(name: .easeIn)
window.animator().alphaValue = 0
}, completionHandler: {
window.close()
self.notificationWindow = nil
})
}
}

View File

@ -62,6 +62,22 @@ class Recorder: ObservableObject {
func startRecording(toOutputFile url: URL) async throws {
deviceManager.isRecordingActive = true
let currentDeviceID = deviceManager.getCurrentDevice()
let lastDeviceID = UserDefaults.standard.string(forKey: "lastUsedMicrophoneDeviceID")
if String(currentDeviceID) != lastDeviceID {
if let deviceName = deviceManager.availableDevices.first(where: { $0.id == currentDeviceID })?.name {
await MainActor.run {
NotificationManager.shared.showNotification(
title: "Audio Input Source",
message: "Using: \(deviceName)",
type: .info
)
}
}
}
UserDefaults.standard.set(String(currentDeviceID), forKey: "lastUsedMicrophoneDeviceID")
Task {
await mediaController.muteSystemAudio()
}

View File

@ -148,12 +148,11 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate {
} else {
guard currentTranscriptionModel != nil else {
await MainActor.run {
let alert = NSAlert()
alert.messageText = "No AI Model Selected"
alert.informativeText = "Please select a default AI model in AI Models tab before recording."
alert.alertStyle = .warning
alert.addButton(withTitle: "OK")
alert.runModal()
NotificationManager.shared.showNotification(
title: "No AI Model Selected",
message: "Please select a default AI model before recording.",
type: .error
)
}
return
}