Improved notification system
This commit is contained in:
parent
e0ff033581
commit
8537a59b2a
@ -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 {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user