From 8537a59b2ac77f92a667020db446605ce3c98869 Mon Sep 17 00:00:00 2001 From: Beingpax Date: Sun, 22 Jun 2025 12:12:16 +0545 Subject: [PATCH] Improved notification system --- .../Notifications/AppNotificationView.swift | 89 +++++++++++-------- .../Notifications/NotificationManager.swift | 20 +++-- VoiceInk/Recorder.swift | 6 +- VoiceInk/Whisper/WhisperState.swift | 7 +- 4 files changed, 66 insertions(+), 56 deletions(-) diff --git a/VoiceInk/Notifications/AppNotificationView.swift b/VoiceInk/Notifications/AppNotificationView.swift index e2daad8..476c72a 100644 --- a/VoiceInk/Notifications/AppNotificationView.swift +++ b/VoiceInk/Notifications/AppNotificationView.swift @@ -2,7 +2,6 @@ import SwiftUI struct AppNotificationView: View { let title: String - let message: String let type: NotificationType let duration: TimeInterval let onClose: () -> Void @@ -39,57 +38,70 @@ struct AppNotificationView: View { var body: some View { ZStack { HStack(alignment: .center, spacing: 12) { - if let appIcon = NSApp.applicationIconImage { - Image(nsImage: appIcon) - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 50, height: 50) - .cornerRadius(10) - } + // Type icon + Image(systemName: type.iconName) + .font(.system(size: 16, weight: .medium)) + .foregroundColor(type.iconColor) + .frame(width: 20, height: 20) - 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) - } + // Single message text + Text(title) + .font(.system(size: 12)) + .fontWeight(.medium) + .foregroundColor(.white) + .lineLimit(2) + .multilineTextAlignment(.leading) Spacer() - } - .padding(12) - .frame(height: 60) - VStack { - HStack { - Spacer() - Button(action: onClose) { - Image(systemName: "xmark") - .font(.system(size: 11, weight: .medium)) - .foregroundColor(.secondary) - } - .buttonStyle(PlainButtonStyle()) - .frame(width: 16, height: 16) + + Button(action: onClose) { + Image(systemName: "xmark") + .font(.system(size: 10, weight: .medium)) + .foregroundColor(.white.opacity(0.6)) } - Spacer() + .buttonStyle(PlainButtonStyle()) + .frame(width: 16, height: 16) } - .padding(8) + .padding(.horizontal, 16) + .padding(.vertical, 12) } - .frame(minWidth: 320, maxWidth: 420) + .frame(minWidth: 280, maxWidth: 380, minHeight: 44) .background( RoundedRectangle(cornerRadius: 12, style: .continuous) - .fill(Color(nsColor: .controlBackgroundColor).opacity(0.95)) + .fill(.clear) + .background( + ZStack { + // Base dark background + Color.black.opacity(0.9) + + // Subtle gradient overlay + LinearGradient( + colors: [ + Color.black.opacity(0.95), + Color(red: 0.15, green: 0.15, blue: 0.15).opacity(0.9) + ], + startPoint: .top, + endPoint: .bottom + ) + + // Very subtle visual effect for depth + VisualEffectView(material: .hudWindow, blendingMode: .withinWindow) + .opacity(0.05) + } + .clipShape(RoundedRectangle(cornerRadius: 12, style: .continuous)) + ) + ) + .overlay( + // Subtle inner border + RoundedRectangle(cornerRadius: 12, style: .continuous) + .strokeBorder(Color.white.opacity(0.1), lineWidth: 0.5) ) .overlay( VStack { Spacer() GeometryReader { geometry in Rectangle() - .fill(Color.accentColor.opacity(0.6)) + .fill(type.iconColor.opacity(0.8)) .frame(width: geometry.size.width * max(0, progress), height: 2) .animation(.linear(duration: 0.1), value: progress) } @@ -126,3 +138,4 @@ struct AppNotificationView: View { } } } + diff --git a/VoiceInk/Notifications/NotificationManager.swift b/VoiceInk/Notifications/NotificationManager.swift index e1fa493..ea115ad 100644 --- a/VoiceInk/Notifications/NotificationManager.swift +++ b/VoiceInk/Notifications/NotificationManager.swift @@ -12,9 +12,8 @@ class NotificationManager { @MainActor func showNotification( title: String, - message: String, type: AppNotificationView.NotificationType, - duration: TimeInterval = 8.0, + duration: TimeInterval = 5.0, onTap: (() -> Void)? = nil ) { dismissTimer?.invalidate() @@ -31,8 +30,7 @@ class NotificationManager { } let notificationView = AppNotificationView( - title: title, - message: message, + title: title, type: type, duration: duration, onClose: { [weak self] in @@ -84,12 +82,18 @@ class NotificationManager { private func positionWindow(_ window: NSWindow) { let activeScreen = NSApp.keyWindow?.screen ?? NSScreen.main ?? NSScreen.screens[0] let screenRect = activeScreen.visibleFrame - let windowRect = window.frame + let notificationRect = window.frame - let x = screenRect.maxX - windowRect.width - 20 - let y = screenRect.maxY - windowRect.height - 20 + // Position notification centered horizontally on screen + let notificationX = screenRect.midX - (notificationRect.width / 2) - window.setFrameOrigin(NSPoint(x: x, y: y)) + // Position notification near bottom of screen with appropriate spacing + let bottomPadding: CGFloat = 24 + let componentHeight: CGFloat = 34 + let notificationSpacing: CGFloat = 16 + let notificationY = screenRect.minY + bottomPadding + componentHeight + notificationSpacing + + window.setFrameOrigin(NSPoint(x: notificationX, y: notificationY)) } @MainActor diff --git a/VoiceInk/Recorder.swift b/VoiceInk/Recorder.swift index efecca6..6daa5e8 100644 --- a/VoiceInk/Recorder.swift +++ b/VoiceInk/Recorder.swift @@ -71,8 +71,7 @@ class Recorder: ObservableObject { if let deviceName = deviceManager.availableDevices.first(where: { $0.id == currentDeviceID })?.name { await MainActor.run { NotificationManager.shared.showNotification( - title: "Audio Input Source", - message: "Using: \(deviceName)", + title: "Using: \(deviceName)", type: .info ) } @@ -126,7 +125,7 @@ class Recorder: ObservableObject { } audioLevelCheckTask = Task { - try? await Task.sleep(nanoseconds: 5_000_000_000) + try? await Task.sleep(nanoseconds: 2_000_000_000) if Task.isCancelled { return } @@ -134,7 +133,6 @@ class Recorder: ObservableObject { await MainActor.run { NotificationManager.shared.showNotification( title: "No Audio Detected", - message: "Is your microphone muted? Please check your audio input settings.", type: .warning ) } diff --git a/VoiceInk/Whisper/WhisperState.swift b/VoiceInk/Whisper/WhisperState.swift index b56bc19..d93db9d 100644 --- a/VoiceInk/Whisper/WhisperState.swift +++ b/VoiceInk/Whisper/WhisperState.swift @@ -154,7 +154,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { await MainActor.run { NotificationManager.shared.showNotification( title: "No AI Model Selected", - message: "Please select a default AI model before recording.", type: .error ) } @@ -409,8 +408,7 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { await MainActor.run { if permanentURL != nil { NotificationManager.shared.showNotification( - title: "Transcription Failed", - message: "🔄 Tap to retry transcription", + title: "Transcription Failed. Tap to retry.", type: .error, onTap: { [weak self] in Task { @@ -421,7 +419,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { } else { NotificationManager.shared.showNotification( title: "Recording Failed", - message: "Could not save audio file. Please try recording again.", type: .error ) } @@ -463,7 +460,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { await MainActor.run { NotificationManager.shared.showNotification( title: "Transcription Successful", - message: "✅ Retry completed successfully", type: .success ) @@ -479,7 +475,6 @@ class WhisperState: NSObject, ObservableObject, AVAudioRecorderDelegate { await MainActor.run { NotificationManager.shared.showNotification( title: "Retry Failed", - message: "Transcription failed again. Check your model and settings.", type: .error ) }