mirror of
https://github.com/samsonjs/vibetunnel.git
synced 2026-04-27 15:17:38 +00:00
- Move worktree toggle button inside responsive container - Button now properly hides when compact menu is shown - Prevents redundant display of same functionality on mobile
241 lines
11 KiB
Swift
241 lines
11 KiB
Swift
import AppKit
|
|
import os.log
|
|
import SwiftUI
|
|
|
|
private let logger = Logger(subsystem: BundleIdentifiers.loggerSubsystem, category: "NotificationSettings")
|
|
|
|
/// Settings view for managing notification preferences
|
|
struct NotificationSettingsView: View {
|
|
@AppStorage("showNotifications")
|
|
private var showNotifications = true
|
|
|
|
@Environment(ConfigManager.self) private var configManager
|
|
@Environment(NotificationService.self) private var notificationService
|
|
|
|
@State private var isTestingNotification = false
|
|
@State private var showingPermissionAlert = false
|
|
|
|
private func updateNotificationPreferences() {
|
|
// Load current preferences from ConfigManager and notify the service
|
|
let prefs = NotificationService.NotificationPreferences(fromConfig: configManager)
|
|
notificationService.updatePreferences(prefs)
|
|
// Also update the enabled state in ConfigManager
|
|
configManager.notificationsEnabled = showNotifications
|
|
}
|
|
|
|
var body: some View {
|
|
NavigationStack {
|
|
@Bindable var bindableConfig = configManager
|
|
|
|
Form {
|
|
// Master toggle section
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
Toggle("Show Session Notifications", isOn: $showNotifications)
|
|
.controlSize(.large)
|
|
.onChange(of: showNotifications) { _, newValue in
|
|
// Update ConfigManager's notificationsEnabled to match
|
|
configManager.notificationsEnabled = newValue
|
|
|
|
// Ensure NotificationService starts/stops based on the toggle
|
|
if newValue {
|
|
Task {
|
|
// Request permissions and show test notification
|
|
let granted = await notificationService
|
|
.requestPermissionAndShowTestNotification()
|
|
|
|
if granted {
|
|
await notificationService.start()
|
|
} else {
|
|
// If permission denied, turn toggle back off
|
|
await MainActor.run {
|
|
showNotifications = false
|
|
configManager.notificationsEnabled = false
|
|
showingPermissionAlert = true
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
notificationService.stop()
|
|
}
|
|
}
|
|
Text("Display native macOS notifications for session and command events")
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
}
|
|
.padding(.vertical, 8)
|
|
}
|
|
|
|
// Notification types section
|
|
if showNotifications {
|
|
Section {
|
|
NotificationToggleRow(
|
|
title: "Session starts",
|
|
description: "When a new session starts (useful for shared terminals)",
|
|
isOn: $bindableConfig.notificationSessionStart
|
|
)
|
|
.onChange(of: bindableConfig.notificationSessionStart) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
NotificationToggleRow(
|
|
title: "Session ends",
|
|
description: "When a session terminates or crashes (shows exit code)",
|
|
isOn: $bindableConfig.notificationSessionExit
|
|
)
|
|
.onChange(of: bindableConfig.notificationSessionExit) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
NotificationToggleRow(
|
|
title: "Commands fail",
|
|
description: "When commands fail with non-zero exit codes",
|
|
isOn: $bindableConfig.notificationCommandError
|
|
)
|
|
.onChange(of: bindableConfig.notificationCommandError) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
NotificationToggleRow(
|
|
title: "Commands complete (> 3 seconds)",
|
|
description: "When commands taking >3 seconds finish (builds, tests, etc.)",
|
|
isOn: $bindableConfig.notificationCommandCompletion
|
|
)
|
|
.onChange(of: bindableConfig.notificationCommandCompletion) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
NotificationToggleRow(
|
|
title: "Terminal bell (🔔)",
|
|
description: "Terminal bell (^G) from vim, IRC mentions, completion sounds",
|
|
isOn: $bindableConfig.notificationBell
|
|
)
|
|
.onChange(of: bindableConfig.notificationBell) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
NotificationToggleRow(
|
|
title: "Claude turn notifications",
|
|
description: "When Claude AI finishes responding and awaits input",
|
|
isOn: $bindableConfig.notificationClaudeTurn
|
|
)
|
|
.onChange(of: bindableConfig.notificationClaudeTurn) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
} header: {
|
|
Text("Notification Types")
|
|
.font(.headline)
|
|
}
|
|
|
|
// Behavior section
|
|
Section {
|
|
VStack(spacing: 12) {
|
|
Toggle("Play sound", isOn: $bindableConfig.notificationSoundEnabled)
|
|
.onChange(of: bindableConfig.notificationSoundEnabled) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
|
|
Toggle("Show in Notification Center", isOn: $bindableConfig.showInNotificationCenter)
|
|
.onChange(of: bindableConfig.showInNotificationCenter) { _, _ in
|
|
updateNotificationPreferences()
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Notification Behavior")
|
|
.font(.headline)
|
|
}
|
|
|
|
// Test section
|
|
Section {
|
|
VStack(alignment: .leading, spacing: 12) {
|
|
HStack {
|
|
Button("Test Notification") {
|
|
Task {
|
|
isTestingNotification = true
|
|
await notificationService.sendGenericNotification(
|
|
title: "VibeTunnel Test",
|
|
body: "This is a test notification to verify your settings are working correctly."
|
|
)
|
|
// Reset button state after a delay
|
|
try? await Task.sleep(nanoseconds: 1_000_000_000)
|
|
isTestingNotification = false
|
|
}
|
|
}
|
|
.buttonStyle(.bordered)
|
|
.disabled(!showNotifications || isTestingNotification)
|
|
|
|
if isTestingNotification {
|
|
ProgressView()
|
|
.scaleEffect(0.7)
|
|
.frame(width: 16, height: 16)
|
|
}
|
|
|
|
Spacer()
|
|
}
|
|
|
|
HStack {
|
|
Button("Open System Settings") {
|
|
notificationService.openNotificationSettings()
|
|
}
|
|
.buttonStyle(.link)
|
|
|
|
Spacer()
|
|
}
|
|
}
|
|
} header: {
|
|
Text("Actions")
|
|
.font(.headline)
|
|
}
|
|
}
|
|
}
|
|
.formStyle(.grouped)
|
|
.scrollContentBackground(.hidden)
|
|
.navigationTitle("Notification Settings")
|
|
.onAppear {
|
|
// Sync the AppStorage value with ConfigManager on first load
|
|
showNotifications = configManager.notificationsEnabled
|
|
}
|
|
}
|
|
.alert("Notification Permission Required", isPresented: $showingPermissionAlert) {
|
|
Button("Open System Settings") {
|
|
notificationService.openNotificationSettings()
|
|
}
|
|
Button("Cancel", role: .cancel) {}
|
|
} message: {
|
|
Text(
|
|
"VibeTunnel needs permission to show notifications. Please enable notifications for VibeTunnel in System Settings."
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Reusable component for notification toggle rows with descriptions
|
|
struct NotificationToggleRow: View {
|
|
let title: String
|
|
let description: String
|
|
@Binding var isOn: Bool
|
|
|
|
var body: some View {
|
|
HStack(alignment: .top, spacing: 12) {
|
|
VStack(alignment: .leading, spacing: 4) {
|
|
Text(title)
|
|
.font(.body)
|
|
Text(description)
|
|
.font(.caption)
|
|
.foregroundStyle(.secondary)
|
|
.fixedSize(horizontal: false, vertical: true)
|
|
}
|
|
Spacer()
|
|
Toggle("", isOn: $isOn)
|
|
.labelsHidden()
|
|
}
|
|
.padding(.vertical, 6)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
NotificationSettingsView()
|
|
.environment(ConfigManager.shared)
|
|
.environment(NotificationService.shared)
|
|
.frame(width: 560, height: 700)
|
|
}
|