diff --git a/boringNotch.xcodeproj/project.pbxproj b/boringNotch.xcodeproj/project.pbxproj index dfee6850..ec587f0f 100644 --- a/boringNotch.xcodeproj/project.pbxproj +++ b/boringNotch.xcodeproj/project.pbxproj @@ -82,6 +82,7 @@ 9A987A0D2C73CA66005CA465 /* NotchShelfView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9A987A032C73CA66005CA465 /* NotchShelfView.swift */; }; 9A987A102C73CA8D005CA465 /* Collections in Frameworks */ = {isa = PBXBuildFile; productRef = 9A987A0F2C73CA8D005CA465 /* Collections */; }; 9AB0C6BD2C73C9CB00F7CD30 /* NotchHomeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 9AB0C6BB2C73C9CB00F7CD30 /* NotchHomeView.swift */; }; + A79A415A2E6996BA001D3BC9 /* BackgroundManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = A79A41592E6996BA001D3BC9 /* BackgroundManager.swift */; }; B10348D92C74E56000475897 /* ConditionalModifier.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10348D82C74E56000475897 /* ConditionalModifier.swift */; }; B10A848A2C7BCC940088BFFC /* AirDropView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10A84892C7BCC940088BFFC /* AirDropView.swift */; }; B10A848C2C7BCD150088BFFC /* AirDrop.swift in Sources */ = {isa = PBXBuildFile; fileRef = B10A848B2C7BCD150088BFFC /* AirDrop.swift */; }; @@ -210,6 +211,7 @@ 9A987A022C73CA66005CA465 /* DropItemView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = DropItemView.swift; sourceTree = ""; }; 9A987A032C73CA66005CA465 /* NotchShelfView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotchShelfView.swift; sourceTree = ""; }; 9AB0C6BB2C73C9CB00F7CD30 /* NotchHomeView.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = NotchHomeView.swift; sourceTree = ""; }; + A79A41592E6996BA001D3BC9 /* BackgroundManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundManager.swift; sourceTree = ""; }; B10348D82C74E56000475897 /* ConditionalModifier.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ConditionalModifier.swift; sourceTree = ""; }; B10A84892C7BCC940088BFFC /* AirDropView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirDropView.swift; sourceTree = ""; }; B10A848B2C7BCD150088BFFC /* AirDrop.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AirDrop.swift; sourceTree = ""; }; @@ -357,6 +359,7 @@ 149E0B962C737D00006418B1 /* WebcamManager.swift */, 147CB9562C8CCC980094C254 /* BoringExtensionManager.swift */, 14C08BB52C8DE42D000F8AA0 /* CalendarManager.swift */, + A79A41592E6996BA001D3BC9 /* BackgroundManager.swift */, ); path = managers; sourceTree = ""; @@ -726,6 +729,7 @@ B10A848C2C7BCD150088BFFC /* AirDrop.swift in Sources */, 1153BD9C2D98853B00979FB0 /* NowPlayingController.swift in Sources */, B141C2412CA5F53F00AC8CC8 /* SparkleView.swift in Sources */, + A79A415A2E6996BA001D3BC9 /* BackgroundManager.swift in Sources */, 116398962DF5D6C00052E6AF /* CalendarServiceProviding.swift in Sources */, 1163988D2DF5CAB40052E6AF /* EventModel.swift in Sources */, 14D570B92C5E98A20011E668 /* drop.swift in Sources */, diff --git a/boringNotch/Assets.xcassets/wallpaper.imageset/Contents.json b/boringNotch/Assets.xcassets/wallpaper.imageset/Contents.json new file mode 100644 index 00000000..1ebc2713 --- /dev/null +++ b/boringNotch/Assets.xcassets/wallpaper.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "SCR-20250905-lpmt.jpeg", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/boringNotch/Assets.xcassets/wallpaper.imageset/SCR-20250905-lpmt.jpeg b/boringNotch/Assets.xcassets/wallpaper.imageset/SCR-20250905-lpmt.jpeg new file mode 100644 index 00000000..37f5ea79 Binary files /dev/null and b/boringNotch/Assets.xcassets/wallpaper.imageset/SCR-20250905-lpmt.jpeg differ diff --git a/boringNotch/ContentView.swift b/boringNotch/ContentView.swift index a2e4676d..119160cd 100644 --- a/boringNotch/ContentView.swift +++ b/boringNotch/ContentView.swift @@ -20,6 +20,7 @@ struct ContentView: View { @ObservedObject var coordinator = BoringViewCoordinator.shared @ObservedObject var musicManager = MusicManager.shared @ObservedObject var batteryModel = BatteryStatusViewModel.shared + @ObservedObject var backgroundManager = BackgroundManager.shared @State private var isHovering: Bool = false @State private var hoverWorkItem: DispatchWorkItem? @@ -41,6 +42,19 @@ struct ContentView: View { private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 + var effectiveBackground: some View { + if Defaults[.backgroundIsBlack] { + return AnyView(Color.black) + } else { + return AnyView( + backgroundManager.background + .opacity(vm.notchState == .open ? 1 : 0) + .background(Color.black.opacity(vm.notchState == .open ? 0 : 1)) + .animation(.easeInOut, value: vm.notchState) + ) + } + } + var body: some View { ZStack(alignment: .top) { let mainLayout = NotchLayout() @@ -53,7 +67,7 @@ struct ContentView: View { : cornerRadiusInsets.closed.bottom ) .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0) - .background(.black) + .background(effectiveBackground) .mask { ((vm.notchState == .open) && Defaults[.cornerRadiusScaling]) ? NotchShape( @@ -132,7 +146,7 @@ struct ContentView: View { handleUpGesture(translation: translation, phase: phase) } } - .onAppear(perform: { + .onAppear { DispatchQueue.main.asyncAfter(deadline: .now() + 1) { withAnimation(vm.animation) { if coordinator.firstLaunch { @@ -140,7 +154,7 @@ struct ContentView: View { } } } - }) + } .onChange(of: vm.notchState) { _, newState in // Reset hover state when notch state changes if newState == .closed && isHovering { @@ -188,6 +202,7 @@ struct ContentView: View { ) .background(dragDetector) .environmentObject(vm) + .preferredColorScheme(.dark) } @ViewBuilder @@ -205,16 +220,14 @@ struct ContentView: View { if coordinator.expandingView.type == .battery && coordinator.expandingView.show && vm.notchState == .closed && Defaults[.showPowerStatusNotifications] { - HStack(spacing: 0) { - HStack { Text(batteryModel.statusText) .font(.subheadline) .foregroundStyle(.white) - } + HStack(spacing: 0) { Rectangle() - .fill(.black) - .frame(width: vm.closedNotchSize.width + 10) + .fill(Color.clear) + .frame(width: vm.closedNotchSize.width + 10) HStack { BoringBatteryView( @@ -242,7 +255,12 @@ struct ContentView: View { .blur(radius: abs(gestureProgress) > 0.3 ? min(abs(gestureProgress), 8) : 0) .animation(.spring(response: 1, dampingFraction: 1, blendDuration: 0.8), value: vm.notchState) } else { - Rectangle().fill(.clear).frame(width: vm.closedNotchSize.width - 20, height: vm.effectiveClosedNotchHeight) + Rectangle() + .fill(Color.clear) + .frame( + width: vm.closedNotchSize.width - 20, + height: vm.effectiveClosedNotchHeight + ) } if coordinator.sneakPeek.show { @@ -307,21 +325,21 @@ struct ContentView: View { .frame(width: vm.closedNotchSize.width - 20) MinimalFaceFeatures() } - }.frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) + } + .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) } @ViewBuilder func MusicLiveActivity() -> some View { HStack { HStack { - Color.clear + Color.clear .aspectRatio(1, contentMode: .fit) .background( Image(nsImage: musicManager.albumArt) .resizable() .aspectRatio(contentMode: .fill) ) - .clipped() .clipShape( RoundedRectangle( cornerRadius: MusicPlayerImageSizes.cornerRadiusInset.closed) @@ -337,7 +355,9 @@ struct ContentView: View { height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))) Rectangle() - .fill(.black) + .fill(.clear) + + .overlay( HStack(alignment: .top) { if coordinator.expandingView.show @@ -580,7 +600,7 @@ struct FullScreenDropDelegate: DropDelegate { } #Preview { - let vm = BoringViewModel() + let vm = BoringViewModel() vm.open() return ContentView() .environmentObject(vm) diff --git a/boringNotch/Localizable.xcstrings b/boringNotch/Localizable.xcstrings index c9124027..eac456e6 100644 --- a/boringNotch/Localizable.xcstrings +++ b/boringNotch/Localizable.xcstrings @@ -3110,6 +3110,9 @@ } } } + }, + "Background" : { + }, "Battery" : { "localizations" : { @@ -3510,6 +3513,9 @@ } } } + }, + "Black" : { + }, "Boost your productivity with Clipboard Manager" : { "localizations" : { @@ -5010,6 +5016,9 @@ } } } + }, + "Content" : { + }, "Continue" : { "localizations" : { @@ -5710,6 +5719,9 @@ } } } + }, + "Dark" : { + }, "Default" : { "localizations" : { @@ -18412,6 +18424,9 @@ } } } + }, + "Translucent" : { + }, "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : { "localizations" : { @@ -20015,4 +20030,4 @@ } }, "version" : "1.0" -} +} \ No newline at end of file diff --git a/boringNotch/boringNotchApp.swift b/boringNotch/boringNotchApp.swift index bee49b9d..21632df7 100644 --- a/boringNotch/boringNotchApp.swift +++ b/boringNotch/boringNotchApp.swift @@ -65,7 +65,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var windows: [NSScreen: NSWindow] = [:] var viewModels: [NSScreen: BoringViewModel] = [:] var window: NSWindow? - let vm: BoringViewModel = .init() + let vm = BoringViewModel() @ObservedObject var coordinator = BoringViewCoordinator.shared var whatsNewWindow: NSWindow? var timer: Timer? @@ -326,7 +326,8 @@ class AppDelegate: NSObject, NSApplicationDelegate { for screen in currentScreens { if windows[screen] == nil { - let viewModel = BoringViewModel(screen: screen.localizedName) + let viewModel = BoringViewModel() + viewModel.screen = screen.localizedName let window = createBoringNotchWindow(for: screen, with: viewModel) windows[screen] = window diff --git a/boringNotch/components/Calendar/BoringCalendar.swift b/boringNotch/components/Calendar/BoringCalendar.swift index 8c5f6441..cebd3418 100644 --- a/boringNotch/components/Calendar/BoringCalendar.swift +++ b/boringNotch/components/Calendar/BoringCalendar.swift @@ -199,17 +199,26 @@ struct CalendarView: View { ZStack(alignment: .top) { WheelPicker(selectedDate: $selectedDate, config: Config()) - HStack(alignment: .top) { - LinearGradient( - colors: [Color.black, .clear], startPoint: .leading, endPoint: .trailing - ) - .frame(width: 20) - Spacer() - LinearGradient( - colors: [.clear, Color.black], startPoint: .leading, endPoint: .trailing - ) - .frame(width: 20) - } + .mask( + HStack(spacing: 0) { + + LinearGradient(gradient: + Gradient( + colors: [Color.black.opacity(0), Color.black]), + startPoint: .leading, endPoint: .trailing + ) + .frame(width: 15) + + Rectangle().fill(Color.black) + + LinearGradient(gradient: + Gradient( + colors: [Color.black, Color.black.opacity(0)]), + startPoint: .leading, endPoint: .trailing + ) + .frame(width: 15) + } + ) } } @@ -217,6 +226,7 @@ struct CalendarView: View { events: calendarManager.events ) if filteredEvents.isEmpty { + Spacer(minLength: 0) EmptyEventsView() Spacer(minLength: 0) } else { @@ -283,6 +293,7 @@ struct EventListView: View { } var body: some View { + List { ForEach(filteredEvents) { event in Button(action: { @@ -303,7 +314,25 @@ struct EventListView: View { .scrollIndicators(.never) .scrollContentBackground(.hidden) .background(Color.clear) - Spacer(minLength: 0) + .mask { + VStack(spacing: 0) { + LinearGradient(gradient: + Gradient( + colors: [Color.black.opacity(0), Color.black]), + startPoint: .top, endPoint: .bottom + ) + .frame(height: 10) + + Rectangle().fill(Color.black) + + LinearGradient(gradient: + Gradient( + colors: [Color.black, Color.black.opacity(0)]), + startPoint: .top, endPoint: .bottom + ) + .frame(height: 10) + } + } } private func eventRow(_ event: EventModel) -> some View { @@ -433,5 +462,5 @@ struct ReminderToggle: View { CalendarView() .frame(width: 215, height: 130) .background(.black) - .environmentObject(BoringViewModel()) + .environmentObject(BoringViewModel()) } diff --git a/boringNotch/components/Live activities/BoringBattery.swift b/boringNotch/components/Live activities/BoringBattery.swift index 5c540b38..666538c6 100644 --- a/boringNotch/components/Live activities/BoringBattery.swift +++ b/boringNotch/components/Live activities/BoringBattery.swift @@ -28,6 +28,19 @@ struct BatteryView: View { } } + /// Determines the icon to display when charging. + var iconStatusView: Image { + if isCharging { + return Image(systemName: "bolt.fill") + } + else if isPluggedIn { + return Image(systemName: "powerplug.portrait.fill") + } + else { + return Image(systemName: "") + } + } + /// Determines the color of the battery based on its status. var batteryColor: Color { if isInLowPowerMode { @@ -41,41 +54,88 @@ struct BatteryView: View { } } - var body: some View { - ZStack(alignment: .leading) { +// var body: some View { +// ZStack(alignment: .leading) { +// +// Image(systemName: icon) +// .resizable() +// .fontWeight(.thin) +// .aspectRatio(contentMode: .fit) +// .foregroundColor(.white.opacity(0.5)) +// .frame( +// width: batteryWidth + 1 +// ) +// +// RoundedRectangle(cornerRadius: 2.5) +// .fill(batteryColor) +// .frame( +// width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))), +// height: (batteryWidth - 2.75) - 18 +// ) +// .padding(.leading, 2) +// +// if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) { +// ZStack { +// iconStatusView +// .resizable() +// .aspectRatio(contentMode: .fit) +// .foregroundColor(.white) +// .frame( +// width: 17, +// height: 17 +// ) +// .mask( +// iconStatusView +// .resizable() +// .frame( +// width: 17, +// height: 17 +// ) +// .padding(4) // expands the mask +// .compositingGroup() +// .luminanceToAlpha() +// ) +// .blendMode(.destinationOut) // removes underlying pixels +// +// +// } +// .frame(width: batteryWidth, height: batteryWidth) +// } +// } +// } + var body: some View { + ZStack(alignment: .center) { - Image(systemName: icon) - .resizable() - .fontWeight(.thin) - .aspectRatio(contentMode: .fit) - .foregroundColor(.white.opacity(0.5)) - .frame( - width: batteryWidth + 1 - ) + ZStack(alignment: .leading) { + Image(systemName: icon) + .resizable() + .fontWeight(.thin) + .aspectRatio(contentMode: .fit) + .foregroundColor(.white.opacity(0.5)) + .frame(width: batteryWidth + 1) - RoundedRectangle(cornerRadius: 2.5) - .fill(batteryColor) - .frame( - width: CGFloat(((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6))), - height: (batteryWidth - 2.75) - 18 - ) - .padding(.leading, 2) - if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) { - ZStack { - Image(iconStatus) - .resizable() - .aspectRatio(contentMode: .fit) - .foregroundColor(.white) - .frame( - width: 17, - height: 17 - ) - } - .frame(width: batteryWidth, height: batteryWidth) - } - } - } + RoundedRectangle(cornerRadius: 2.5) + .fill(batteryColor) + .frame( + width: CGFloat((CGFloat(CFloat(levelBattery)) / 100) * (batteryWidth - 6)), + height: (batteryWidth - 2.75) - 18 + ) + .padding(.leading, 2) + + } + if iconStatus != "" && (isForNotification || Defaults[.showPowerStatusIcons]) { + iconStatusView + .resizable() + .aspectRatio(contentMode: .fit) + .foregroundStyle(.white) + .frame(width: 17, height: 17) + + } + + } + } + } /// A view that displays detailed battery information and settings. diff --git a/boringNotch/components/Live activities/InlineHUD.swift b/boringNotch/components/Live activities/InlineHUD.swift index 19f92004..0a627c4c 100644 --- a/boringNotch/components/Live activities/InlineHUD.swift +++ b/boringNotch/components/Live activities/InlineHUD.swift @@ -143,5 +143,5 @@ struct InlineHUD: View { .padding(.horizontal, 8) .background(Color.black) .padding() - .environmentObject(BoringViewModel()) + .environmentObject(BoringViewModel()) } diff --git a/boringNotch/components/Notch/BoringExtrasMenu.swift b/boringNotch/components/Notch/BoringExtrasMenu.swift index 63d12ed8..81457bbb 100644 --- a/boringNotch/components/Notch/BoringExtrasMenu.swift +++ b/boringNotch/components/Notch/BoringExtrasMenu.swift @@ -105,5 +105,5 @@ struct BoringExtrasMenu : View { #Preview { - BoringExtrasMenu(vm: .init()) + BoringExtrasMenu(vm: BoringViewModel()) } diff --git a/boringNotch/components/Notch/BoringHeader.swift b/boringNotch/components/Notch/BoringHeader.swift index 74e54564..732aa4ad 100644 --- a/boringNotch/components/Notch/BoringHeader.swift +++ b/boringNotch/components/Notch/BoringHeader.swift @@ -45,7 +45,7 @@ struct BoringHeader: View { vm.toggleCameraPreview() }) { Capsule() - .fill(.black) + .fill(.clear) .frame(width: 30, height: 30) .overlay { Image(systemName: "web.camera") @@ -61,7 +61,7 @@ struct BoringHeader: View { SettingsWindowController.shared.showWindow() }) { Capsule() - .fill(.black) + .fill(.clear) .frame(width: 30, height: 30) .overlay { Image(systemName: "gear") diff --git a/boringNotch/components/Settings/SettingsView.swift b/boringNotch/components/Settings/SettingsView.swift index 2546d943..275a8328 100644 --- a/boringNotch/components/Settings/SettingsView.swift +++ b/boringNotch/components/Settings/SettingsView.swift @@ -1,9 +1,9 @@ -// -// SettingsView.swift -// boringNotch -// -// Created by Richard Kunkli on 07/08/2024. -// + // + // SettingsView.swift + // boringNotch + // + // Created by Richard Kunkli on 07/08/2024. + // import AVFoundation import Defaults @@ -16,352 +16,354 @@ import SwiftUI import SwiftUIIntrospect struct SettingsView: View { - @StateObject var extensionManager = BoringExtensionManager() - @State private var selectedTab = "General" + @StateObject var extensionManager = BoringExtensionManager() + @State private var selectedTab = "General" - let updaterController: SPUStandardUpdaterController? + let updaterController: SPUStandardUpdaterController? - init(updaterController: SPUStandardUpdaterController? = nil) { - self.updaterController = updaterController - } + init(updaterController: SPUStandardUpdaterController? = nil) { + self.updaterController = updaterController + } - var body: some View { - NavigationSplitView { - List(selection: $selectedTab) { - NavigationLink(value: "General") { - Label("General", systemImage: "gear") - } - NavigationLink(value: "Appearance") { - Label("Appearance", systemImage: "eye") - } - NavigationLink(value: "Media") { - Label("Media", systemImage: "play.laptopcomputer") - } - NavigationLink(value: "Calendar") { - Label("Calendar", systemImage: "calendar") - } - if extensionManager.installedExtensions - .contains(where: { $0.bundleIdentifier == hudExtension }) - { - NavigationLink(value: "HUD") { - Label("HUDs", systemImage: "dial.medium.fill") - } - } - NavigationLink(value: "Battery") { - Label("Battery", systemImage: "battery.100.bolt") - } - if extensionManager.installedExtensions - .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) - { - NavigationLink(value: "Downloads") { - Label("Downloads", systemImage: "square.and.arrow.down") - } - } - NavigationLink(value: "Shelf") { - Label("Shelf", systemImage: "books.vertical") - } - NavigationLink(value: "Shortcuts") { - Label("Shortcuts", systemImage: "keyboard") - } - NavigationLink(value: "Extensions") { - Label("Extensions", systemImage: "puzzlepiece.extension") - } - NavigationLink(value: "About") { - Label("About", systemImage: "info.circle") - } - } - .listStyle(SidebarListStyle()) - .toolbar(removing: .sidebarToggle) - .navigationSplitViewColumnWidth(200) - } detail: { - Group { - switch selectedTab { - case "General": - GeneralSettings() - case "Appearance": - Appearance() - case "Media": - Media() - case "Calendar": - CalendarSettings() - case "HUD": - HUD() - case "Battery": - Charge() - case "Shelf": - Shelf() - case "Shortcuts": - Shortcuts() - case "Extensions": - Extensions() - case "About": - if let controller = updaterController { - About(updaterController: controller) - } else { - // Fallback with a default controller - About( - updaterController: SPUStandardUpdaterController( - startingUpdater: false, updaterDelegate: nil, - userDriverDelegate: nil)) - } - default: - GeneralSettings() - } - } - .frame(maxWidth: .infinity, maxHeight: .infinity) - } - .navigationSplitViewStyle(.balanced) - .toolbar(removing: .sidebarToggle) - .toolbar { - Button("") {} // Empty label, does nothing - .controlSize(.extraLarge) - .opacity(0) // Invisible, but reserves space for a consistent look between tabs - .disabled(true) - } - .environmentObject(extensionManager) - .formStyle(.grouped) - .frame(width: 700) - .background(Color(NSColor.windowBackgroundColor)) - } + var body: some View { + NavigationSplitView { + List(selection: $selectedTab) { + NavigationLink(value: "General") { + Label("General", systemImage: "gear") + } + NavigationLink(value: "Appearance") { + Label("Appearance", systemImage: "eye") + } + NavigationLink(value: "Media") { + Label("Media", systemImage: "play.laptopcomputer") + } + NavigationLink(value: "Calendar") { + Label("Calendar", systemImage: "calendar") + } + if extensionManager.installedExtensions + .contains(where: { $0.bundleIdentifier == hudExtension }) + { + NavigationLink(value: "HUD") { + Label("HUDs", systemImage: "dial.medium.fill") + } + } + NavigationLink(value: "Battery") { + Label("Battery", systemImage: "battery.100.bolt") + } + if extensionManager.installedExtensions + .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) + { + NavigationLink(value: "Downloads") { + Label("Downloads", systemImage: "square.and.arrow.down") + } + } + NavigationLink(value: "Shelf") { + Label("Shelf", systemImage: "books.vertical") + } + NavigationLink(value: "Shortcuts") { + Label("Shortcuts", systemImage: "keyboard") + } + NavigationLink(value: "Extensions") { + Label("Extensions", systemImage: "puzzlepiece.extension") + } + NavigationLink(value: "About") { + Label("About", systemImage: "info.circle") + } + } + .listStyle(SidebarListStyle()) + .toolbar(removing: .sidebarToggle) + .navigationSplitViewColumnWidth(200) + } detail: { + Group { + switch selectedTab { + case "General": + GeneralSettings() + .environmentObject(BoringViewModel()) + case "Appearance": + Appearance() + .environmentObject(BoringViewModel()) + case "Media": + Media() + case "Calendar": + CalendarSettings() + case "HUD": + HUD() + case "Battery": + Charge() + case "Shelf": + Shelf() + case "Shortcuts": + Shortcuts() + case "Extensions": + Extensions() + case "About": + if let controller = updaterController { + About(updaterController: controller) + } else { + // Fallback with a default controller + About( + updaterController: SPUStandardUpdaterController( + startingUpdater: false, updaterDelegate: nil, + userDriverDelegate: nil)) + } + default: + GeneralSettings() + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .navigationSplitViewStyle(.balanced) + .toolbar(removing: .sidebarToggle) + .toolbar { + Button("") {} // Empty label, does nothing + .controlSize(.extraLarge) + .opacity(0) // Invisible, but reserves space for a consistent look between tabs + .disabled(true) + } + .environmentObject(extensionManager) + .formStyle(.grouped) + .frame(width: 700) + .background(Color(NSColor.windowBackgroundColor)) + } } struct GeneralSettings: View { - @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName } - @EnvironmentObject var vm: BoringViewModel - @ObservedObject var coordinator = BoringViewCoordinator.shared + @State private var screens: [String] = NSScreen.screens.compactMap { $0.localizedName } + @EnvironmentObject var vm: BoringViewModel + @ObservedObject var coordinator = BoringViewCoordinator.shared - @Default(.mirrorShape) var mirrorShape - @Default(.showEmojis) var showEmojis - @Default(.gestureSensitivity) var gestureSensitivity - @Default(.minimumHoverDuration) var minimumHoverDuration - @Default(.nonNotchHeight) var nonNotchHeight - @Default(.nonNotchHeightMode) var nonNotchHeightMode - @Default(.notchHeight) var notchHeight - @Default(.notchHeightMode) var notchHeightMode - @Default(.showOnAllDisplays) var showOnAllDisplays - @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay - @Default(.enableGestures) var enableGestures - @Default(.openNotchOnHover) var openNotchOnHover + @Default(.mirrorShape) var mirrorShape + @Default(.showEmojis) var showEmojis + @Default(.gestureSensitivity) var gestureSensitivity + @Default(.minimumHoverDuration) var minimumHoverDuration + @Default(.nonNotchHeight) var nonNotchHeight + @Default(.nonNotchHeightMode) var nonNotchHeightMode + @Default(.notchHeight) var notchHeight + @Default(.notchHeightMode) var notchHeightMode + @Default(.showOnAllDisplays) var showOnAllDisplays + @Default(.automaticallySwitchDisplay) var automaticallySwitchDisplay + @Default(.enableGestures) var enableGestures + @Default(.openNotchOnHover) var openNotchOnHover - var body: some View { - Form { - Section { - Defaults.Toggle(key: .menubarIcon) { - Text("Menubar icon") - } - LaunchAtLogin.Toggle("Launch at login") - Defaults.Toggle(key: .showOnAllDisplays) { - Text("Show on all displays") - } - .onChange(of: showOnAllDisplays) { - NotificationCenter.default.post( - name: Notification.Name.showOnAllDisplaysChanged, object: nil) - } - Picker("Show on a specific display", selection: $coordinator.preferredScreen) { - ForEach(screens, id: \.self) { screen in - Text(screen) - } - } - .onChange(of: NSScreen.screens) { - screens = NSScreen.screens.compactMap({ $0.localizedName }) - } - .disabled(showOnAllDisplays) - Defaults.Toggle(key: .automaticallySwitchDisplay) { - Text("Automatically switch displays") - } - .onChange(of: automaticallySwitchDisplay) { - NotificationCenter.default.post( - name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) - } - .disabled(showOnAllDisplays) - } header: { - Text("System features") - } + var body: some View { + Form { + Section { + Defaults.Toggle(key: .menubarIcon) { + Text("Menubar icon") + } + LaunchAtLogin.Toggle("Launch at login") + Defaults.Toggle(key: .showOnAllDisplays) { + Text("Show on all displays") + } + .onChange(of: showOnAllDisplays) { + NotificationCenter.default.post( + name: Notification.Name.showOnAllDisplaysChanged, object: nil) + } + Picker("Show on a specific display", selection: $coordinator.preferredScreen) { + ForEach(screens, id: \.self) { screen in + Text(screen) + } + } + .onChange(of: NSScreen.screens) { + screens = NSScreen.screens.compactMap({ $0.localizedName }) + } + .disabled(showOnAllDisplays) + Defaults.Toggle(key: .automaticallySwitchDisplay) { + Text("Automatically switch displays") + } + .onChange(of: automaticallySwitchDisplay) { + NotificationCenter.default.post( + name: Notification.Name.automaticallySwitchDisplayChanged, object: nil) + } + .disabled(showOnAllDisplays) + } header: { + Text("System features") + } - Section { - Picker( - selection: $notchHeightMode, - label: - Text("Notch display height") - ) { - Text("Match real notch size") - .tag(WindowHeightMode.matchRealNotchSize) - Text("Match menubar height") - .tag(WindowHeightMode.matchMenuBar) - Text("Custom height") - .tag(WindowHeightMode.custom) - } - .onChange(of: notchHeightMode) { - switch notchHeightMode { - case .matchRealNotchSize: - notchHeight = 38 - case .matchMenuBar: - notchHeight = 44 - case .custom: - notchHeight = 38 - } - NotificationCenter.default.post( - name: Notification.Name.notchHeightChanged, object: nil) - } - if notchHeightMode == .custom { - Slider(value: $notchHeight, in: 15...45, step: 1) { - Text("Custom notch size - \(notchHeight, specifier: "%.0f")") - } - .onChange(of: notchHeight) { - NotificationCenter.default.post( - name: Notification.Name.notchHeightChanged, object: nil) - } - } - Picker("Non-notch display height", selection: $nonNotchHeightMode) { - Text("Match menubar height") - .tag(WindowHeightMode.matchMenuBar) - Text("Match real notch size") - .tag(WindowHeightMode.matchRealNotchSize) - Text("Custom height") - .tag(WindowHeightMode.custom) - } - .onChange(of: nonNotchHeightMode) { - switch nonNotchHeightMode { - case .matchMenuBar: - nonNotchHeight = 24 - case .matchRealNotchSize: - nonNotchHeight = 32 - case .custom: - nonNotchHeight = 32 - } - NotificationCenter.default.post( - name: Notification.Name.notchHeightChanged, object: nil) - } - if nonNotchHeightMode == .custom { - Slider(value: $nonNotchHeight, in: 0...40, step: 1) { - Text("Custom notch size - \(nonNotchHeight, specifier: "%.0f")") - } - .onChange(of: nonNotchHeight) { - NotificationCenter.default.post( - name: Notification.Name.notchHeightChanged, object: nil) - } - } - } header: { - Text("Notch Height") - } + Section { + Picker( + selection: $notchHeightMode, + label: + Text("Notch display height") + ) { + Text("Match real notch size") + .tag(WindowHeightMode.matchRealNotchSize) + Text("Match menubar height") + .tag(WindowHeightMode.matchMenuBar) + Text("Custom height") + .tag(WindowHeightMode.custom) + } + .onChange(of: notchHeightMode) { + switch notchHeightMode { + case .matchRealNotchSize: + notchHeight = 38 + case .matchMenuBar: + notchHeight = 44 + case .custom: + notchHeight = 38 + } + NotificationCenter.default.post( + name: Notification.Name.notchHeightChanged, object: nil) + } + if notchHeightMode == .custom { + Slider(value: $notchHeight, in: 15...45, step: 1) { + Text("Custom notch size - \(notchHeight, specifier: "%.0f")") + } + .onChange(of: notchHeight) { + NotificationCenter.default.post( + name: Notification.Name.notchHeightChanged, object: nil) + } + } + Picker("Non-notch display height", selection: $nonNotchHeightMode) { + Text("Match menubar height") + .tag(WindowHeightMode.matchMenuBar) + Text("Match real notch size") + .tag(WindowHeightMode.matchRealNotchSize) + Text("Custom height") + .tag(WindowHeightMode.custom) + } + .onChange(of: nonNotchHeightMode) { + switch nonNotchHeightMode { + case .matchMenuBar: + nonNotchHeight = 24 + case .matchRealNotchSize: + nonNotchHeight = 32 + case .custom: + nonNotchHeight = 32 + } + NotificationCenter.default.post( + name: Notification.Name.notchHeightChanged, object: nil) + } + if nonNotchHeightMode == .custom { + Slider(value: $nonNotchHeight, in: 0...40, step: 1) { + Text("Custom notch size - \(nonNotchHeight, specifier: "%.0f")") + } + .onChange(of: nonNotchHeight) { + NotificationCenter.default.post( + name: Notification.Name.notchHeightChanged, object: nil) + } + } + } header: { + Text("Notch Height") + } - NotchBehaviour() + NotchBehaviour() - gestureControls() - } - .toolbar { - Button("Quit app") { - NSApp.terminate(self) - } - .controlSize(.extraLarge) - } - .navigationTitle("General") - .onChange(of: openNotchOnHover) { - if !openNotchOnHover { - enableGestures = true - } - } - } + gestureControls() + } + .toolbar { + Button("Quit app") { + NSApp.terminate(self) + } + .controlSize(.extraLarge) + } + .navigationTitle("General") + .onChange(of: openNotchOnHover) { + if !openNotchOnHover { + enableGestures = true + } + } + } - @ViewBuilder - func gestureControls() -> some View { - Section { - Defaults.Toggle(key: .enableGestures) { - Text("Enable gestures") - } - .disabled(!openNotchOnHover) - if enableGestures { - Toggle("Media change with horizontal gestures", isOn: .constant(false)) - .disabled(true) - Defaults.Toggle(key: .closeGestureEnabled) { - Text("Close gesture") - } - Slider(value: $gestureSensitivity, in: 100...300, step: 100) { - HStack { - Text("Gesture sensitivity") - Spacer() - Text( - Defaults[.gestureSensitivity] == 100 - ? "High" : Defaults[.gestureSensitivity] == 200 ? "Medium" : "Low" - ) - .foregroundStyle(.secondary) - } - } - } - } header: { - HStack { - Text("Gesture control") - customBadge(text: "Beta") - } - } footer: { - Text( - "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" - ) - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - } + @ViewBuilder + func gestureControls() -> some View { + Section { + Defaults.Toggle(key: .enableGestures) { + Text("Enable gestures") + } + .disabled(!openNotchOnHover) + if enableGestures { + Toggle("Media change with horizontal gestures", isOn: .constant(false)) + .disabled(true) + Defaults.Toggle(key: .closeGestureEnabled) { + Text("Close gesture") + } + Slider(value: $gestureSensitivity, in: 100...300, step: 100) { + HStack { + Text("Gesture sensitivity") + Spacer() + Text( + Defaults[.gestureSensitivity] == 100 + ? "High" : Defaults[.gestureSensitivity] == 200 ? "Medium" : "Low" + ) + .foregroundStyle(.secondary) + } + } + } + } header: { + HStack { + Text("Gesture control") + customBadge(text: "Beta") + } + } footer: { + Text( + "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" + ) + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + } - @ViewBuilder - func NotchBehaviour() -> some View { - Section { - Defaults.Toggle(key: .extendHoverArea) { - Text("Extend hover area") - } - Defaults.Toggle(key: .enableHaptics) { - Text("Enable haptics") - } - Defaults.Toggle(key: .openNotchOnHover) { - Text("Open notch on hover") - } - Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault) - if openNotchOnHover { - Slider(value: $minimumHoverDuration, in: 0...1, step: 0.1) { - HStack { - Text("Minimum hover duration") - Spacer() - Text("\(minimumHoverDuration, specifier: "%.1f")s") - .foregroundStyle(.secondary) - } - } - .onChange(of: minimumHoverDuration) { - NotificationCenter.default.post( - name: Notification.Name.notchHeightChanged, object: nil) - } - } - } header: { - Text("Notch behavior") - } - } + @ViewBuilder + func NotchBehaviour() -> some View { + Section { + Defaults.Toggle(key: .extendHoverArea) { + Text("Extend hover area") + } + Defaults.Toggle(key: .enableHaptics) { + Text("Enable haptics") + } + Defaults.Toggle(key: .openNotchOnHover) { + Text("Open notch on hover") + } + Toggle("Remember last tab", isOn: $coordinator.openLastTabByDefault) + if openNotchOnHover { + Slider(value: $minimumHoverDuration, in: 0...1, step: 0.1) { + HStack { + Text("Minimum hover duration") + Spacer() + Text("\(minimumHoverDuration, specifier: "%.1f")s") + .foregroundStyle(.secondary) + } + } + .onChange(of: minimumHoverDuration) { + NotificationCenter.default.post( + name: Notification.Name.notchHeightChanged, object: nil) + } + } + } header: { + Text("Notch behavior") + } + } } struct Charge: View { - var body: some View { - Form { - Section { - Defaults.Toggle(key: .showBatteryIndicator) { - Text("Show battery indicator") - } - Defaults.Toggle(key: .showPowerStatusNotifications) { - Text("Show power status notifications") - } - } header: { - Text("General") - } - Section { - Defaults.Toggle(key: .showBatteryPercentage) { - Text("Show battery percentage") - } - Defaults.Toggle(key: .showPowerStatusIcons) { - Text("Show power status icons") - } - } header: { - Text("Battery Information") - } - } - .navigationTitle("Battery") - } + var body: some View { + Form { + Section { + Defaults.Toggle(key: .showBatteryIndicator) { + Text("Show battery indicator") + } + Defaults.Toggle(key: .showPowerStatusNotifications) { + Text("Show power status notifications") + } + } header: { + Text("General") + } + Section { + Defaults.Toggle(key: .showBatteryPercentage) { + Text("Show battery percentage") + } + Defaults.Toggle(key: .showPowerStatusIcons) { + Text("Show power status icons") + } + } header: { + Text("Battery Information") + } + } + .navigationTitle("Battery") + } } //struct Downloads: View { @@ -445,880 +447,931 @@ struct Charge: View { //} struct HUD: View { - @EnvironmentObject var vm: BoringViewModel - @Default(.inlineHUD) var inlineHUD - @Default(.enableGradient) var enableGradient - @ObservedObject var coordinator = BoringViewCoordinator.shared - var body: some View { - Form { - Section { - Toggle("Enable HUD replacement", isOn: $coordinator.hudReplacement) - } header: { - Text("General") - } - Section { - Picker("HUD style", selection: $inlineHUD) { - Text("Default") - .tag(false) - Text("Inline") - .tag(true) - } - .onChange(of: Defaults[.inlineHUD]) { - if Defaults[.inlineHUD] { - withAnimation { - Defaults[.systemEventIndicatorShadow] = false - Defaults[.enableGradient] = false - } - } - } - Picker("Progressbar style", selection: $enableGradient) { - Text("Hierarchical") - .tag(false) - Text("Gradient") - .tag(true) - } - Defaults.Toggle(key: .systemEventIndicatorShadow) { - Text("Enable glowing effect") - } - Defaults.Toggle(key: .systemEventIndicatorUseAccent) { - Text("Use accent color") - } - } header: { - HStack { - Text("Appearance") - } - } - } - .navigationTitle("HUDs") - } + @EnvironmentObject var vm: BoringViewModel + @Default(.inlineHUD) var inlineHUD + @Default(.enableGradient) var enableGradient + @ObservedObject var coordinator = BoringViewCoordinator.shared + var body: some View { + Form { + Section { + Toggle("Enable HUD replacement", isOn: $coordinator.hudReplacement) + } header: { + Text("General") + } + Section { + Picker("HUD style", selection: $inlineHUD) { + Text("Default") + .tag(false) + Text("Inline") + .tag(true) + } + .onChange(of: Defaults[.inlineHUD]) { + if Defaults[.inlineHUD] { + withAnimation { + Defaults[.systemEventIndicatorShadow] = false + Defaults[.enableGradient] = false + } + } + } + Picker("Progressbar style", selection: $enableGradient) { + Text("Hierarchical") + .tag(false) + Text("Gradient") + .tag(true) + } + Defaults.Toggle(key: .systemEventIndicatorShadow) { + Text("Enable glowing effect") + } + Defaults.Toggle(key: .systemEventIndicatorUseAccent) { + Text("Use accent color") + } + } header: { + HStack { + Text("Appearance") + } + } + } + .navigationTitle("HUDs") + } } struct Media: View { - @Default(.waitInterval) var waitInterval - @Default(.mediaController) var mediaController - @ObservedObject var coordinator = BoringViewCoordinator.shared - @Default(.hideNotchOption) var hideNotchOption - @Default(.enableSneakPeek) private var enableSneakPeek - @Default(.sneakPeekStyles) var sneakPeekStyles + @Default(.waitInterval) var waitInterval + @Default(.mediaController) var mediaController + @ObservedObject var coordinator = BoringViewCoordinator.shared + @Default(.hideNotchOption) var hideNotchOption + @Default(.enableSneakPeek) private var enableSneakPeek + @Default(.sneakPeekStyles) var sneakPeekStyles - var body: some View { - Form { - Section { - Picker("Music Source", selection: $mediaController) { - ForEach(availableMediaControllers) { controller in - Text(controller.rawValue).tag(controller) - } - } - .onChange(of: mediaController) { _, _ in - NotificationCenter.default.post( - name: Notification.Name.mediaControllerChanged, - object: nil - ) - } - } header: { - Text("Media Source") - } footer: { - if MusicManager.shared.isNowPlayingDeprecated { - HStack { - Text("YouTube Music requires this third-party app to be installed: ") - .foregroundStyle(.secondary) - .font(.caption) - Link( - "https://github.com/th-ch/youtube-music", - destination: URL(string: "https://github.com/th-ch/youtube-music")! - ) - .font(.caption) - .foregroundColor(.blue) // Ensures it's visibly a link - } - } else { - Text( - "'Now Playing' was the only option on previous versions and works with all media apps." - ) - .foregroundStyle(.secondary) - .font(.caption) - } - } - Section { - Defaults.Toggle(key: .showShuffleAndRepeat) { - HStack { - Text("Show shuffle and repeat buttons") - customBadge(text: "Beta") - } - } - } header: { - Text("Media controls") - } - Section { - Toggle( - "Enable music live activity", - isOn: $coordinator.musicLiveActivityEnabled.animation() - ) - Toggle("Enable sneak peek", isOn: $enableSneakPeek) - Picker("Sneak Peek Style", selection: $sneakPeekStyles) { - ForEach(SneakPeekStyle.allCases) { style in - Text(style.rawValue).tag(style) - } - }.disabled(!enableSneakPeek) - HStack { - Stepper(value: $waitInterval, in: 0...10, step: 1) { - HStack { - Text("Media inactivity timeout") - Spacer() - Text("\(Defaults[.waitInterval], specifier: "%.0f") seconds") - .foregroundStyle(.secondary) - } - } - } - } header: { - Text("Media playback live activity") - } + var body: some View { + Form { + Section { + Picker("Music Source", selection: $mediaController) { + ForEach(availableMediaControllers) { controller in + Text(controller.rawValue).tag(controller) + } + } + .onChange(of: mediaController) { _, _ in + NotificationCenter.default.post( + name: Notification.Name.mediaControllerChanged, + object: nil + ) + } + } header: { + Text("Media Source") + } footer: { + if MusicManager.shared.isNowPlayingDeprecated { + HStack { + Text("YouTube Music requires this third-party app to be installed: ") + .foregroundStyle(.secondary) + .font(.caption) + Link( + "https://github.com/th-ch/youtube-music", + destination: URL(string: "https://github.com/th-ch/youtube-music")! + ) + .font(.caption) + .foregroundColor(.blue) // Ensures it's visibly a link + } + } else { + Text( + "'Now Playing' was the only option on previous versions and works with all media apps." + ) + .foregroundStyle(.secondary) + .font(.caption) + } + } + Section { + Defaults.Toggle(key: .showShuffleAndRepeat) { + HStack { + Text("Show shuffle and repeat buttons") + customBadge(text: "Beta") + } + } + } header: { + Text("Media controls") + } + Section { + Toggle( + "Enable music live activity", + isOn: $coordinator.musicLiveActivityEnabled.animation() + ) + Toggle("Enable sneak peek", isOn: $enableSneakPeek) + Picker("Sneak Peek Style", selection: $sneakPeekStyles) { + ForEach(SneakPeekStyle.allCases) { style in + Text(style.rawValue).tag(style) + } + }.disabled(!enableSneakPeek) + HStack { + Stepper(value: $waitInterval, in: 0...10, step: 1) { + HStack { + Text("Media inactivity timeout") + Spacer() + Text("\(Defaults[.waitInterval], specifier: "%.0f") seconds") + .foregroundStyle(.secondary) + } + } + } + } header: { + Text("Media playback live activity") + } - Picker( - selection: $hideNotchOption, - label: - HStack { - Text("Hide BoringNotch Options") - customBadge(text: "Beta") - } - ) { - Text("Always hide in fullscreen").tag(HideNotchOption.always) - Text("Hide only when NowPlaying app is in fullscreen").tag( - HideNotchOption.nowPlayingOnly) - Text("Never hide").tag(HideNotchOption.never) - } - .onChange(of: hideNotchOption) { - Defaults[.enableFullscreenMediaDetection] = hideNotchOption != .never - } - } - .navigationTitle("Media") - } + Picker( + selection: $hideNotchOption, + label: + HStack { + Text("Hide BoringNotch Options") + customBadge(text: "Beta") + } + ) { + Text("Always hide in fullscreen").tag(HideNotchOption.always) + Text("Hide only when NowPlaying app is in fullscreen").tag( + HideNotchOption.nowPlayingOnly) + Text("Never hide").tag(HideNotchOption.never) + } + .onChange(of: hideNotchOption) { + Defaults[.enableFullscreenMediaDetection] = hideNotchOption != .never + } + } + .navigationTitle("Media") + } - // Only show controller options that are available on this macOS version - private var availableMediaControllers: [MediaControllerType] { - if MusicManager.shared.isNowPlayingDeprecated { - return MediaControllerType.allCases.filter { $0 != .nowPlaying } - } else { - return MediaControllerType.allCases - } - } + // Only show controller options that are available on this macOS version + private var availableMediaControllers: [MediaControllerType] { + if MusicManager.shared.isNowPlayingDeprecated { + return MediaControllerType.allCases.filter { $0 != .nowPlaying } + } else { + return MediaControllerType.allCases + } + } } struct CalendarSettings: View { - @ObservedObject private var calendarManager = CalendarManager.shared - @Default(.showCalendar) var showCalendar: Bool - @Default(.hideCompletedReminders) var hideCompletedReminders + @ObservedObject private var calendarManager = CalendarManager.shared + @Default(.showCalendar) var showCalendar: Bool + @Default(.hideCompletedReminders) var hideCompletedReminders - var body: some View { - Form { - Defaults.Toggle(key: .showCalendar) { - Text("Show calendar") - } - Defaults.Toggle(key: .hideCompletedReminders) { - Text("Hide completed reminders") - } - Section(header: Text("Calendars")) { - if calendarManager.calendarAuthorizationStatus != .fullAccess { - Text("Calendar access is denied. Please enable it in System Settings.") - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding() - Button("Open Calendar Settings") { - if let settingsURL = URL( - string: - "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars" - ) { - NSWorkspace.shared.open(settingsURL) - } - } - } else { - List { - ForEach(calendarManager.eventCalendars, id: \.id) { calendar in - Toggle( - isOn: Binding( - get: { calendarManager.getCalendarSelected(calendar) }, - set: { isSelected in - Task { - await calendarManager.setCalendarSelected( - calendar, isSelected: isSelected) - } - } - ) - ) { - Text(calendar.title) - } - .disabled(!showCalendar) - } - } - } - } - Section(header: Text("Reminders")) { - if calendarManager.reminderAuthorizationStatus != .fullAccess { - Text("Reminder access is denied. Please enable it in System Settings.") - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding() - Button("Open Reminder Settings") { - if let settingsURL = URL( - string: - "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders" - ) { - NSWorkspace.shared.open(settingsURL) - } - } - } else { - List { - ForEach(calendarManager.reminderLists, id: \.id) { calendar in - Toggle( - isOn: Binding( - get: { calendarManager.getCalendarSelected(calendar) }, - set: { isSelected in - Task { - await calendarManager.setCalendarSelected( - calendar, isSelected: isSelected) - } - } - ) - ) { - Text(calendar.title) - } - .disabled(!showCalendar) - } - } - } - } - } - .onAppear { - Task { - await calendarManager.checkCalendarAuthorization() - await calendarManager.checkReminderAuthorization() - } - } - } + var body: some View { + Form { + Defaults.Toggle(key: .showCalendar) { + Text("Show calendar") + } + Defaults.Toggle(key: .hideCompletedReminders) { + Text("Hide completed reminders") + } + Section(header: Text("Calendars")) { + if calendarManager.calendarAuthorizationStatus != .fullAccess { + Text("Calendar access is denied. Please enable it in System Settings.") + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + Button("Open Calendar Settings") { + if let settingsURL = URL( + string: + "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars" + ) { + NSWorkspace.shared.open(settingsURL) + } + } + } else { + List { + ForEach(calendarManager.eventCalendars, id: \.id) { calendar in + Toggle( + isOn: Binding( + get: { calendarManager.getCalendarSelected(calendar) }, + set: { isSelected in + Task { + await calendarManager.setCalendarSelected( + calendar, isSelected: isSelected) + } + } + ) + ) { + Text(calendar.title) + } + .disabled(!showCalendar) + } + } + } + } + Section(header: Text("Reminders")) { + if calendarManager.reminderAuthorizationStatus != .fullAccess { + Text("Reminder access is denied. Please enable it in System Settings.") + .foregroundColor(.red) + .multilineTextAlignment(.center) + .padding() + Button("Open Reminder Settings") { + if let settingsURL = URL( + string: + "x-apple.systempreferences:com.apple.preference.security?Privacy_Reminders" + ) { + NSWorkspace.shared.open(settingsURL) + } + } + } else { + List { + ForEach(calendarManager.reminderLists, id: \.id) { calendar in + Toggle( + isOn: Binding( + get: { calendarManager.getCalendarSelected(calendar) }, + set: { isSelected in + Task { + await calendarManager.setCalendarSelected( + calendar, isSelected: isSelected) + } + } + ) + ) { + Text(calendar.title) + } + .disabled(!showCalendar) + } + } + } + } + } + .onAppear { + Task { + await calendarManager.checkCalendarAuthorization() + await calendarManager.checkReminderAuthorization() + } + } + } } struct About: View { - @State private var showBuildNumber: Bool = false - let updaterController: SPUStandardUpdaterController - @Environment(\.openWindow) var openWindow - var body: some View { - VStack { - Form { - Section { - HStack { - Text("Release name") - Spacer() - Text(Defaults[.releaseName]) - .foregroundStyle(.secondary) - } - HStack { - Text("Version") - Spacer() - if showBuildNumber { - Text("(\(Bundle.main.buildVersionNumber ?? ""))") - .foregroundStyle(.secondary) - } - Text(Bundle.main.releaseVersionNumber ?? "unkown") - .foregroundStyle(.secondary) - } - .onTapGesture { - withAnimation { - showBuildNumber.toggle() - } - } - } header: { - Text("Version info") - } + @State private var showBuildNumber: Bool = false + let updaterController: SPUStandardUpdaterController + @Environment(\.openWindow) var openWindow + var body: some View { + VStack { + Form { + Section { + HStack { + Text("Release name") + Spacer() + Text(Defaults[.releaseName]) + .foregroundStyle(.secondary) + } + HStack { + Text("Version") + Spacer() + if showBuildNumber { + Text("(\(Bundle.main.buildVersionNumber ?? ""))") + .foregroundStyle(.secondary) + } + Text(Bundle.main.releaseVersionNumber ?? "unkown") + .foregroundStyle(.secondary) + } + .onTapGesture { + withAnimation { + showBuildNumber.toggle() + } + } + } header: { + Text("Version info") + } - UpdaterSettingsView(updater: updaterController.updater) + UpdaterSettingsView(updater: updaterController.updater) - HStack(spacing: 30) { - Spacer(minLength: 0) - Button { - NSWorkspace.shared.open(sponsorPage) - } label: { - VStack(spacing: 5) { - Image(systemName: "cup.and.saucer.fill") - .imageScale(.large) - Text("Support Us") - .foregroundStyle(.white) - } - .contentShape(Rectangle()) - } - Spacer(minLength: 0) - Button { - NSWorkspace.shared.open(productPage) - } label: { - VStack(spacing: 5) { - Image("Github") - .resizable() - .aspectRatio(contentMode: .fit) - .frame(width: 18) - Text("GitHub") - .foregroundStyle(.white) - } - .contentShape(Rectangle()) - } - Spacer(minLength: 0) - } - .buttonStyle(PlainButtonStyle()) - } - VStack(spacing: 0) { - Divider() - Text("Made with 🫶🏻 by not so boring not.people") - .foregroundStyle(.secondary) - .padding(.top, 5) - .padding(.bottom, 7) - .multilineTextAlignment(.center) - .padding(.horizontal, 10) - } - .frame(maxWidth: .infinity, alignment: .center) - } - .toolbar { - // Button("Welcome window") { - // openWindow(id: "onboarding") - // } - // .controlSize(.extraLarge) - CheckForUpdatesView(updater: updaterController.updater) - } - .navigationTitle("About") - } + HStack(spacing: 30) { + Spacer(minLength: 0) + Button { + NSWorkspace.shared.open(sponsorPage) + } label: { + VStack(spacing: 5) { + Image(systemName: "cup.and.saucer.fill") + .imageScale(.large) + Text("Support Us") + .foregroundStyle(.white) + } + .contentShape(Rectangle()) + } + Spacer(minLength: 0) + Button { + NSWorkspace.shared.open(productPage) + } label: { + VStack(spacing: 5) { + Image("Github") + .resizable() + .aspectRatio(contentMode: .fit) + .frame(width: 18) + Text("GitHub") + .foregroundStyle(.white) + } + .contentShape(Rectangle()) + } + Spacer(minLength: 0) + } + .buttonStyle(PlainButtonStyle()) + } + VStack(spacing: 0) { + Divider() + Text("Made with 🫶🏻 by not so boring not.people") + .foregroundStyle(.secondary) + .padding(.top, 5) + .padding(.bottom, 7) + .multilineTextAlignment(.center) + .padding(.horizontal, 10) + } + .frame(maxWidth: .infinity, alignment: .center) + } + .toolbar { + // Button("Welcome window") { + // openWindow(id: "onboarding") + // } + // .controlSize(.extraLarge) + CheckForUpdatesView(updater: updaterController.updater) + } + .navigationTitle("About") + } } struct Shelf: View { - var body: some View { - Form { - Section { - Defaults.Toggle(key: .boringShelf) { - Text("Enable shelf") - } - Defaults.Toggle(key: .openShelfByDefault) { - Text("Open shelf by default if items are present") - } - } header: { - HStack { - Text("General") - } - } - } - .navigationTitle("Shelf") - } + var body: some View { + Form { + Section { + Defaults.Toggle(key: .boringShelf) { + Text("Enable shelf") + } + Defaults.Toggle(key: .openShelfByDefault) { + Text("Open shelf by default if items are present") + } + } header: { + HStack { + Text("General") + } + } + } + .navigationTitle("Shelf") + } } struct Extensions: View { - @EnvironmentObject var extensionManager: BoringExtensionManager - @State private var effectTrigger: Bool = false - var body: some View { - Form { - //warningBadge("We don't support extensions yet") // Uhhhh You do? <><><> Oori.S - Section { - List { - ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in - let item = extensionManager.installedExtensions[index] - HStack { - AppIcon(for: item.bundleIdentifier) - .resizable() - .frame(width: 24, height: 24) - Text(item.name) - ListItemPopover { - Text("Description") - } - Spacer(minLength: 0) - HStack(spacing: 6) { - Circle() - .frame(width: 6, height: 6) - .foregroundColor( - isExtensionRunning(item.bundleIdentifier) - ? .green : item.status == .disabled ? .gray : .red - ) - .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) - { view in - view - .shadow(color: .green, radius: 3) - } - Text( - isExtensionRunning(item.bundleIdentifier) - ? "Running" - : item.status == .disabled ? "Disabled" : "Stopped" - ) - .contentTransition(.numericText()) - .foregroundStyle(.secondary) - .font(.footnote) - } - .frame(width: 60, alignment: .leading) + @EnvironmentObject var extensionManager: BoringExtensionManager + @State private var effectTrigger: Bool = false + var body: some View { + Form { + //warningBadge("We don't support extensions yet") // Uhhhh You do? <><><> Oori.S + Section { + List { + ForEach(extensionManager.installedExtensions.indices, id: \.self) { index in + let item = extensionManager.installedExtensions[index] + HStack { + AppIcon(for: item.bundleIdentifier) + .resizable() + .frame(width: 24, height: 24) + Text(item.name) + ListItemPopover { + Text("Description") + } + Spacer(minLength: 0) + HStack(spacing: 6) { + Circle() + .frame(width: 6, height: 6) + .foregroundColor( + isExtensionRunning(item.bundleIdentifier) + ? .green : item.status == .disabled ? .gray : .red + ) + .conditionalModifier(isExtensionRunning(item.bundleIdentifier)) + { view in + view + .shadow(color: .green, radius: 3) + } + Text( + isExtensionRunning(item.bundleIdentifier) + ? "Running" + : item.status == .disabled ? "Disabled" : "Stopped" + ) + .contentTransition(.numericText()) + .foregroundStyle(.secondary) + .font(.footnote) + } + .frame(width: 60, alignment: .leading) - Menu( - content: { - Button("Restart") { - let ws = NSWorkspace.shared + Menu( + content: { + Button("Restart") { + let ws = NSWorkspace.shared - if let ext = ws.runningApplications.first(where: { - $0.bundleIdentifier == item.bundleIdentifier - }) { - ext.terminate() - } + if let ext = ws.runningApplications.first(where: { + $0.bundleIdentifier == item.bundleIdentifier + }) { + ext.terminate() + } - if let appURL = ws.urlForApplication( - withBundleIdentifier: item.bundleIdentifier) - { - ws.openApplication( - at: appURL, configuration: .init(), - completionHandler: nil) - } - } - .keyboardShortcut("R", modifiers: .command) - Button("Disable") { - if let ext = NSWorkspace.shared.runningApplications.first( - where: { $0.bundleIdentifier == item.bundleIdentifier }) - { - ext.terminate() - } - extensionManager.installedExtensions[index].status = - .disabled - } - .keyboardShortcut("D", modifiers: .command) - Divider() - Button("Uninstall", role: .destructive) { - // - } - }, - label: { - Image(systemName: "ellipsis.circle") - .foregroundStyle(.secondary) - } - ) - .controlSize(.regular) - } - .buttonStyle(PlainButtonStyle()) - .padding(.vertical, 5) - } - } - .frame(minHeight: 120) - .actionBar { - Button { - } label: { - HStack(spacing: 3) { - Image(systemName: "plus") - Text("Add manually") - } - .foregroundStyle(.secondary) - } - .disabled(true) - Spacer() - Button { - withAnimation(.linear(duration: 1)) { - effectTrigger.toggle() - } completion: { - effectTrigger.toggle() - } - extensionManager.checkIfExtensionsAreInstalled() - } label: { - HStack(spacing: 3) { - Image(systemName: "arrow.triangle.2.circlepath") - .rotationEffect(effectTrigger ? .degrees(360) : .zero) - } - .foregroundStyle(.secondary) - } - } - .controlSize(.small) - .buttonStyle(PlainButtonStyle()) - .overlay { - if extensionManager.installedExtensions.isEmpty { - Text("No extension installed") - .foregroundStyle(Color(.secondaryLabelColor)) - .padding(.bottom, 22) - } - } - } header: { - HStack(spacing: 0) { - Text("Installed extensions") - if !extensionManager.installedExtensions.isEmpty { - Text(" – \(extensionManager.installedExtensions.count)") - .foregroundStyle(.secondary) - } - } - } - } - .navigationTitle("Extensions") - // TipsView() - // .padding(.horizontal, 19) - } + if let appURL = ws.urlForApplication( + withBundleIdentifier: item.bundleIdentifier) + { + ws.openApplication( + at: appURL, configuration: .init(), + completionHandler: nil) + } + } + .keyboardShortcut("R", modifiers: .command) + Button("Disable") { + if let ext = NSWorkspace.shared.runningApplications.first( + where: { $0.bundleIdentifier == item.bundleIdentifier }) + { + ext.terminate() + } + extensionManager.installedExtensions[index].status = + .disabled + } + .keyboardShortcut("D", modifiers: .command) + Divider() + Button("Uninstall", role: .destructive) { + // + } + }, + label: { + Image(systemName: "ellipsis.circle") + .foregroundStyle(.secondary) + } + ) + .controlSize(.regular) + } + .buttonStyle(PlainButtonStyle()) + .padding(.vertical, 5) + } + } + .frame(minHeight: 120) + .actionBar { + Button { + } label: { + HStack(spacing: 3) { + Image(systemName: "plus") + Text("Add manually") + } + .foregroundStyle(.secondary) + } + .disabled(true) + Spacer() + Button { + withAnimation(.linear(duration: 1)) { + effectTrigger.toggle() + } completion: { + effectTrigger.toggle() + } + extensionManager.checkIfExtensionsAreInstalled() + } label: { + HStack(spacing: 3) { + Image(systemName: "arrow.triangle.2.circlepath") + .rotationEffect(effectTrigger ? .degrees(360) : .zero) + } + .foregroundStyle(.secondary) + } + } + .controlSize(.small) + .buttonStyle(PlainButtonStyle()) + .overlay { + if extensionManager.installedExtensions.isEmpty { + Text("No extension installed") + .foregroundStyle(Color(.secondaryLabelColor)) + .padding(.bottom, 22) + } + } + } header: { + HStack(spacing: 0) { + Text("Installed extensions") + if !extensionManager.installedExtensions.isEmpty { + Text(" – \(extensionManager.installedExtensions.count)") + .foregroundStyle(.secondary) + } + } + } + } + .navigationTitle("Extensions") + // TipsView() + // .padding(.horizontal, 19) + } } struct Appearance: View { - @ObservedObject var coordinator = BoringViewCoordinator.shared - @Default(.mirrorShape) var mirrorShape - @Default(.sliderColor) var sliderColor - @Default(.useMusicVisualizer) var useMusicVisualizer - @Default(.customVisualizers) var customVisualizers - @Default(.selectedVisualizer) var selectedVisualizer - let icons: [String] = ["logo2"] - @State private var selectedIcon: String = "logo2" - @State private var selectedListVisualizer: CustomVisualizer? = nil + @ObservedObject var coordinator = BoringViewCoordinator.shared + @EnvironmentObject var vm: BoringViewModel + @ObservedObject var backgroundManager = BackgroundManager.shared + @Default(.mirrorShape) var mirrorShape + @Default(.sliderColor) var sliderColor + @Default(.useMusicVisualizer) var useMusicVisualizer + @Default(.customVisualizers) var customVisualizers + @Default(.selectedVisualizer) var selectedVisualizer + @Default(.backgroundIsBlack) var backgroundIsBlack + @Default(.backgroundBlackGradient) var backgroundBlackGradient + let icons: [String] = ["logo2"] + @State private var selectedIcon: String = "logo2" + @State private var selectedListVisualizer: CustomVisualizer? = nil + + @State private var isPresented: Bool = false + @State private var name: String = "" + @State private var url: String = "" + @State private var speed: CGFloat = 1.0 + + var body: some View { + + Form { + Section { + Toggle("Always show tabs", isOn: $coordinator.alwaysShowTabs) + Defaults.Toggle(key: .settingsIconInNotch) { + Text("Settings icon in notch") + } + Defaults.Toggle(key: .enableShadow) { + Text("Enable window shadow") + } + Defaults.Toggle(key: .cornerRadiusScaling) { + Text("Corner radius scaling") + } + Defaults.Toggle(key: .useModernCloseAnimation) { + Text("Use simpler close animation") + } + } header: { + Text("General") + } + + Section { + + Picker("Background", selection: $backgroundIsBlack) { + Text("Black").tag(true) + Text("Translucent").tag(false) + } + .pickerStyle(.segmented) + + Slider( + value: $backgroundBlackGradient, + in: 0...0.65, + ) { + ZStack(alignment: .top) { + Image("wallpaper") + .resizable() + .aspectRatio(contentMode: .fill) + .frame(width: 180, height: 100) + .clipShape(RoundedRectangle(cornerRadius: 10)) - @State private var isPresented: Bool = false - @State private var name: String = "" - @State private var url: String = "" - @State private var speed: CGFloat = 1.0 - var body: some View { - Form { - Section { - Toggle("Always show tabs", isOn: $coordinator.alwaysShowTabs) - Defaults.Toggle(key: .settingsIconInNotch) { - Text("Settings icon in notch") - } - Defaults.Toggle(key: .enableShadow) { - Text("Enable window shadow") - } - Defaults.Toggle(key: .cornerRadiusScaling) { - Text("Corner radius scaling") - } - Defaults.Toggle(key: .useModernCloseAnimation) { - Text("Use simpler close animation") - } - } header: { - Text("General") - } + Rectangle() + .fill(Color.clear) + .overlay(alignment: .center) { + Text("Content") + .foregroundStyle(.white) + } + .background(backgroundManager.background) + .frame(width: 150, height: 40) + .mask { + NotchShape( + topCornerRadius: cornerRadiusInsets.opened.top, + bottomCornerRadius: cornerRadiusInsets.closed.bottom + ) + .drawingGroup() + } + } + } minimumValueLabel: { + Text("Translucent") + } maximumValueLabel: { + Text("Dark") + } + .disabled(Defaults[.backgroundIsBlack]) + } header: { + Text("Background") + } - Section { - Defaults.Toggle(key: .coloredSpectrogram) { - Text("Enable colored spectrograms") - } - Defaults - .Toggle("Player tinting", key: .playerColorTinting) - Defaults.Toggle(key: .lightingEffect) { - Text("Enable blur effect behind album art") - } - Picker("Slider color", selection: $sliderColor) { - ForEach(SliderColorEnum.allCases, id: \.self) { option in - Text(option.rawValue) - } - } - } header: { - Text("Media") - } + Section { + Defaults.Toggle(key: .coloredSpectrogram) { + Text("Enable colored spectrograms") + } + Defaults + .Toggle("Player tinting", key: .playerColorTinting) + Defaults.Toggle(key: .lightingEffect) { + Text("Enable blur effect behind album art") + } + Picker("Slider color", selection: $sliderColor) { + ForEach(SliderColorEnum.allCases, id: \.self) { option in + Text(option.rawValue) + } + } + } header: { + Text("Media") + } - Section { - Toggle( - "Use music visualizer spectrogram", - isOn: $useMusicVisualizer.animation() - ) - .disabled(true) - if !useMusicVisualizer { - if customVisualizers.count > 0 { - Picker( - "Selected animation", - selection: $selectedVisualizer - ) { - ForEach( - customVisualizers, - id: \.self - ) { visualizer in - Text(visualizer.name) - .tag(visualizer) - } - } - } else { - HStack { - Text("Selected animation") - Spacer() - Text("No custom animation available") - .foregroundStyle(.secondary) - } - } - } - } header: { - HStack { - Text("Custom music live activity animation") - customBadge(text: "Coming soon") - } - } + Section { + Toggle( + "Use music visualizer spectrogram", + isOn: $useMusicVisualizer.animation() + ) + .disabled(true) + if !useMusicVisualizer { + if customVisualizers.count > 0 { + Picker( + "Selected animation", + selection: $selectedVisualizer + ) { + ForEach( + customVisualizers, + id: \.self + ) { visualizer in + Text(visualizer.name) + .tag(visualizer) + } + } + } else { + HStack { + Text("Selected animation") + Spacer() + Text("No custom animation available") + .foregroundStyle(.secondary) + } + } + } + } header: { + HStack { + Text("Custom music live activity animation") + customBadge(text: "Coming soon") + } + } - Section { - List { - ForEach(customVisualizers, id: \.self) { visualizer in - HStack { - LottieView( - state: LUStateData( - type: .loadedFrom(visualizer.url), speed: visualizer.speed, - loopMode: .loop) - ) - .frame(width: 30, height: 30, alignment: .center) - Text(visualizer.name) - Spacer(minLength: 0) - if selectedVisualizer == visualizer { - Text("selected") - .font(.caption) - .fontWeight(.medium) - .foregroundStyle(.secondary) - .padding(.trailing, 8) - } - } - .buttonStyle(PlainButtonStyle()) - .padding(.vertical, 2) - .background( - selectedListVisualizer != nil - ? selectedListVisualizer == visualizer - ? Color.accentColor : Color.clear : Color.clear, - in: RoundedRectangle(cornerRadius: 5) - ) - .contentShape(Rectangle()) - .onTapGesture { - if selectedListVisualizer == visualizer { - selectedListVisualizer = nil - return - } - selectedListVisualizer = visualizer - } - } - } - .safeAreaPadding( - EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) - ) - .frame(minHeight: 120) - .actionBar { - HStack(spacing: 5) { - Button { - name = "" - url = "" - speed = 1.0 - isPresented.toggle() - } label: { - Image(systemName: "plus") - .foregroundStyle(.secondary) - .contentShape(Rectangle()) - } - Divider() - Button { - if selectedListVisualizer != nil { - let visualizer = selectedListVisualizer! - selectedListVisualizer = nil - customVisualizers.remove( - at: customVisualizers.firstIndex(of: visualizer)!) - if visualizer == selectedVisualizer && customVisualizers.count > 0 { - selectedVisualizer = customVisualizers[0] - } - } - } label: { - Image(systemName: "minus") - .foregroundStyle(.secondary) - .contentShape(Rectangle()) - } - } - } - .controlSize(.small) - .buttonStyle(PlainButtonStyle()) - .overlay { - if customVisualizers.isEmpty { - Text("No custom visualizer") - .foregroundStyle(Color(.secondaryLabelColor)) - .padding(.bottom, 22) - } - } - .sheet(isPresented: $isPresented) { - VStack(alignment: .leading) { - Text("Add new visualizer") - .font(.largeTitle.bold()) - .padding(.vertical) - TextField("Name", text: $name) - TextField("Lottie JSON URL", text: $url) - HStack { - Text("Speed") - Spacer(minLength: 80) - Text("\(speed, specifier: "%.1f")s") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - Slider(value: $speed, in: 0...2, step: 0.1) - } - .padding(.vertical) - HStack { - Button { - isPresented.toggle() - } label: { - Text("Cancel") - .frame(maxWidth: .infinity, alignment: .center) - } + Section { + List { + ForEach(customVisualizers, id: \.self) { visualizer in + HStack { + LottieView( + state: LUStateData( + type: .loadedFrom(visualizer.url), speed: visualizer.speed, + loopMode: .loop) + ) + .frame(width: 30, height: 30, alignment: .center) + Text(visualizer.name) + Spacer(minLength: 0) + if selectedVisualizer == visualizer { + Text("selected") + .font(.caption) + .fontWeight(.medium) + .foregroundStyle(.secondary) + .padding(.trailing, 8) + } + } + .buttonStyle(PlainButtonStyle()) + .padding(.vertical, 2) + .background( + selectedListVisualizer != nil + ? selectedListVisualizer == visualizer + ? Color.accentColor : Color.clear : Color.clear, + in: RoundedRectangle(cornerRadius: 5) + ) + .contentShape(Rectangle()) + .onTapGesture { + if selectedListVisualizer == visualizer { + selectedListVisualizer = nil + return + } + selectedListVisualizer = visualizer + } + } + } + .safeAreaPadding( + EdgeInsets(top: 5, leading: 0, bottom: 5, trailing: 0) + ) + .frame(minHeight: 120) + .actionBar { + HStack(spacing: 5) { + Button { + name = "" + url = "" + speed = 1.0 + isPresented.toggle() + } label: { + Image(systemName: "plus") + .foregroundStyle(.secondary) + .contentShape(Rectangle()) + } + Divider() + Button { + if selectedListVisualizer != nil { + let visualizer = selectedListVisualizer! + selectedListVisualizer = nil + customVisualizers.remove( + at: customVisualizers.firstIndex(of: visualizer)!) + if visualizer == selectedVisualizer && customVisualizers.count > 0 { + selectedVisualizer = customVisualizers[0] + } + } + } label: { + Image(systemName: "minus") + .foregroundStyle(.secondary) + .contentShape(Rectangle()) + } + } + } + .controlSize(.small) + .buttonStyle(PlainButtonStyle()) + .overlay { + if customVisualizers.isEmpty { + Text("No custom visualizer") + .foregroundStyle(Color(.secondaryLabelColor)) + .padding(.bottom, 22) + } + } + .sheet(isPresented: $isPresented) { + VStack(alignment: .leading) { + Text("Add new visualizer") + .font(.largeTitle.bold()) + .padding(.vertical) + TextField("Name", text: $name) + TextField("Lottie JSON URL", text: $url) + HStack { + Text("Speed") + Spacer(minLength: 80) + Text("\(speed, specifier: "%.1f")s") + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + Slider(value: $speed, in: 0...2, step: 0.1) + } + .padding(.vertical) + HStack { + Button { + isPresented.toggle() + } label: { + Text("Cancel") + .frame(maxWidth: .infinity, alignment: .center) + } - Button { - let visualizer: CustomVisualizer = .init( - UUID: UUID(), - name: name, - url: URL(string: url)!, - speed: speed - ) + Button { + let visualizer: CustomVisualizer = .init( + UUID: UUID(), + name: name, + url: URL(string: url)!, + speed: speed + ) - if !customVisualizers.contains(visualizer) { - customVisualizers.append(visualizer) - } + if !customVisualizers.contains(visualizer) { + customVisualizers.append(visualizer) + } - isPresented.toggle() - } label: { - Text("Add") - .frame(maxWidth: .infinity, alignment: .center) - } - .buttonStyle(BorderedProminentButtonStyle()) - } - } - .textFieldStyle(RoundedBorderTextFieldStyle()) - .controlSize(.extraLarge) - .padding() - } - } header: { - HStack(spacing: 0) { - Text("Custom vizualizers (Lottie)") - if !Defaults[.customVisualizers].isEmpty { - Text(" – \(Defaults[.customVisualizers].count)") - .foregroundStyle(.secondary) - } - } - } + isPresented.toggle() + } label: { + Text("Add") + .frame(maxWidth: .infinity, alignment: .center) + } + .buttonStyle(BorderedProminentButtonStyle()) + } + } + .textFieldStyle(RoundedBorderTextFieldStyle()) + .controlSize(.extraLarge) + .padding() + } + } header: { + HStack(spacing: 0) { + Text("Custom vizualizers (Lottie)") + if !Defaults[.customVisualizers].isEmpty { + Text(" – \(Defaults[.customVisualizers].count)") + .foregroundStyle(.secondary) + } + } + } - Section { - Defaults.Toggle(key: .showMirror) { - Text("Enable boring mirror") - } - .disabled(!checkVideoInput()) - Picker("Mirror shape", selection: $mirrorShape) { - Text("Circle") - .tag(MirrorShapeEnum.circle) - Text("Square") - .tag(MirrorShapeEnum.rectangle) - } - Defaults.Toggle(key: .showNotHumanFace) { - Text("Show cool face animation while inactivity") - } - } header: { - HStack { - Text("Additional features") - } - } + Section { + Defaults.Toggle(key: .showMirror) { + Text("Enable boring mirror") + } + .disabled(!checkVideoInput()) + Picker("Mirror shape", selection: $mirrorShape) { + Text("Circle") + .tag(MirrorShapeEnum.circle) + Text("Square") + .tag(MirrorShapeEnum.rectangle) + } + Defaults.Toggle(key: .showNotHumanFace) { + Text("Show cool face animation while inactivity") + } + } header: { + HStack { + Text("Additional features") + } + } - Section { - HStack { - ForEach(icons, id: \.self) { icon in - Spacer() - VStack { - Image(icon) - .resizable() - .frame(width: 80, height: 80) - .background( - RoundedRectangle(cornerRadius: 20, style: .circular) - .strokeBorder( - icon == selectedIcon ? Color.accentColor : .clear, - lineWidth: 2.5 - ) - ) + Section { + HStack { + ForEach(icons, id: \.self) { icon in + Spacer() + VStack { + Image(icon) + .resizable() + .frame(width: 80, height: 80) + .background( + RoundedRectangle(cornerRadius: 20, style: .circular) + .strokeBorder( + icon == selectedIcon ? Color.accentColor : .clear, + lineWidth: 2.5 + ) + ) - Text("Default") - .fontWeight(.medium) - .font(.caption) - .foregroundStyle(icon == selectedIcon ? .white : .secondary) - .padding(.horizontal, 10) - .padding(.vertical, 3) - .background( - Capsule() - .fill(icon == selectedIcon ? Color.accentColor : .clear) - ) - } - .onTapGesture { - withAnimation { - selectedIcon = icon - } - NSApp.applicationIconImage = NSImage(named: icon) - } - Spacer() - } - } - .disabled(true) - } header: { - HStack { - Text("App icon") - customBadge(text: "Coming soon") - } - } - } - .navigationTitle("Appearance") - } + Text("Default") + .fontWeight(.medium) + .font(.caption) + .foregroundStyle(icon == selectedIcon ? .white : .secondary) + .padding(.horizontal, 10) + .padding(.vertical, 3) + .background( + Capsule() + .fill(icon == selectedIcon ? Color.accentColor : .clear) + ) + } + .onTapGesture { + withAnimation { + selectedIcon = icon + } + NSApp.applicationIconImage = NSImage(named: icon) + } + Spacer() + } + } + .disabled(true) + } header: { + HStack { + Text("App icon") + customBadge(text: "Coming soon") + } + } + } + .navigationTitle("Appearance") + } - func checkVideoInput() -> Bool { - if AVCaptureDevice.default(for: .video) != nil { - return true - } + func checkVideoInput() -> Bool { + if AVCaptureDevice.default(for: .video) != nil { + return true + } - return false - } + return false + } } struct Shortcuts: View { - var body: some View { - Form { - Section { - KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) - } header: { - Text("Media") - } footer: { - Text( - "Sneak Peek shows the media title and artist under the notch for a few seconds." - ) - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - Section { - KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) - } - } - .navigationTitle("Shortcuts") - } + var body: some View { + Form { + Section { + KeyboardShortcuts.Recorder("Toggle Sneak Peek:", name: .toggleSneakPeek) + } header: { + Text("Media") + } footer: { + Text( + "Sneak Peek shows the media title and artist under the notch for a few seconds." + ) + .multilineTextAlignment(.trailing) + .foregroundStyle(.secondary) + .font(.caption) + } + Section { + KeyboardShortcuts.Recorder("Toggle Notch Open:", name: .toggleNotchOpen) + } + } + .navigationTitle("Shortcuts") + } } func proFeatureBadge() -> some View { - Text("Upgrade to Pro") - .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98)) - .font(.footnote.bold()) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background( - RoundedRectangle(cornerRadius: 4).stroke( - Color(red: 0.545, green: 0.196, blue: 0.98), lineWidth: 1)) + Text("Upgrade to Pro") + .foregroundStyle(Color(red: 0.545, green: 0.196, blue: 0.98)) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background( + RoundedRectangle(cornerRadius: 4).stroke( + Color(red: 0.545, green: 0.196, blue: 0.98), lineWidth: 1)) } func comingSoonTag() -> some View { - Text("Coming soon") - .foregroundStyle(.secondary) - .font(.footnote.bold()) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color(nsColor: .secondarySystemFill)) - .clipShape(.capsule) + Text("Coming soon") + .foregroundStyle(.secondary) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background(Color(nsColor: .secondarySystemFill)) + .clipShape(.capsule) } func customBadge(text: String) -> some View { - Text(text) - .foregroundStyle(.secondary) - .font(.footnote.bold()) - .padding(.vertical, 3) - .padding(.horizontal, 6) - .background(Color(nsColor: .secondarySystemFill)) - .clipShape(.capsule) + Text(text) + .foregroundStyle(.secondary) + .font(.footnote.bold()) + .padding(.vertical, 3) + .padding(.horizontal, 6) + .background(Color(nsColor: .secondarySystemFill)) + .clipShape(.capsule) } func warningBadge(_ text: String, _ description: String) -> some View { - Section { - HStack(spacing: 12) { - Image(systemName: "exclamationmark.triangle.fill") - .font(.system(size: 22)) - .foregroundStyle(.yellow) - VStack(alignment: .leading) { - Text(text) - .font(.headline) - Text(description) - .foregroundStyle(.secondary) - } - Spacer() - } - } + Section { + HStack(spacing: 12) { + Image(systemName: "exclamationmark.triangle.fill") + .font(.system(size: 22)) + .foregroundStyle(.yellow) + VStack(alignment: .leading) { + Text(text) + .font(.headline) + Text(description) + .foregroundStyle(.secondary) + } + Spacer() + } + } } #Preview { - HUD() + HUD() } diff --git a/boringNotch/enums/generic.swift b/boringNotch/enums/generic.swift index e70b4595..37a6ace4 100644 --- a/boringNotch/enums/generic.swift +++ b/boringNotch/enums/generic.swift @@ -67,3 +67,8 @@ enum SliderColorEnum: String, CaseIterable, Defaults.Serializable { case albumArt = "Match album art" case accent = "Accent color" } + +enum Background: String, CaseIterable, Defaults.Serializable { + case black = "Black" + case ultraThinMaterial = "Thin Material" +} diff --git a/boringNotch/managers/BackgroundManager.swift b/boringNotch/managers/BackgroundManager.swift new file mode 100644 index 00000000..10844fe0 --- /dev/null +++ b/boringNotch/managers/BackgroundManager.swift @@ -0,0 +1,29 @@ +// +// BackgroundManager.swift +// boringNotch +// +// Created by Adon Omeri on 4/9/2025. +// + +import Defaults +import SwiftUI + +@MainActor +class BackgroundManager: ObservableObject { + static let shared = BackgroundManager() + + private init() {} + + var background: some View { + if Defaults[.backgroundIsBlack] { + return AnyView( + Color.black + ) + } else { + return AnyView( + Color.black.opacity(Defaults[.backgroundBlackGradient]) + .background(.ultraThinMaterial) + ) + } + } +} diff --git a/boringNotch/models/BoringViewModel.swift b/boringNotch/models/BoringViewModel.swift index fed06bf7..9d5bb52d 100644 --- a/boringNotch/models/BoringViewModel.swift +++ b/boringNotch/models/BoringViewModel.swift @@ -11,195 +11,200 @@ import SwiftUI import TheBoringWorkerNotifier class BoringViewModel: NSObject, ObservableObject { - @ObservedObject var coordinator = BoringViewCoordinator.shared - @ObservedObject var detector = FullscreenMediaDetector.shared - - let animationLibrary: BoringAnimations = .init() - let animation: Animation? - - @Published var contentType: ContentType = .normal - @Published private(set) var notchState: NotchState = .closed - - @Published var dragDetectorTargeting: Bool = false - @Published var dropZoneTargeting: Bool = false - @Published var dropEvent: Bool = false - @Published var anyDropZoneTargeting: Bool = false - var cancellables: Set = [] - - @Published var hideOnClosed: Bool = true - @Published var isHoveringCalendar: Bool = false - @Published var isBatteryPopoverActive: Bool = false - - @Published var screen: String? - - @Published var notchSize: CGSize = getClosedNotchSize() - @Published var closedNotchSize: CGSize = getClosedNotchSize() - - let webcamManager = WebcamManager.shared - @Published var isCameraExpanded: Bool = false - @Published var isRequestingAuthorization: Bool = false - - deinit { - destroy() - } - - func destroy() { - cancellables.forEach { $0.cancel() } - cancellables.removeAll() - } - - init(screen: String? = nil) { - animation = animationLibrary.animation - - super.init() - - self.screen = screen - notchSize = getClosedNotchSize(screen: screen) - closedNotchSize = notchSize - - Publishers.CombineLatest($dropZoneTargeting, $dragDetectorTargeting) - .map { value1, value2 in - value1 || value2 - } - .assign(to: \.anyDropZoneTargeting, on: self) - .store(in: &cancellables) - - setupDetectorObserver() - } - - private func setupDetectorObserver() { - // Publisher for the user’s fullscreen detection setting - let enabledPublisher = Defaults - .publisher(.enableFullscreenMediaDetection) - .map(\.newValue) - .removeDuplicates() - - // Publisher for the current screen name (non-nil, distinct) - let screenPublisher = $screen - .compactMap { $0 } - .removeDuplicates() - - // Publisher for fullscreen status dictionary - let fullscreenStatusPublisher = detector.$fullscreenStatus - .removeDuplicates() - - // Combine all three: screen name, fullscreen status, and enabled setting - Publishers.CombineLatest3(screenPublisher, fullscreenStatusPublisher, enabledPublisher) - .map { screenName, fullscreenStatus, enabled in - let isFullscreen = fullscreenStatus[screenName] ?? false - return enabled && isFullscreen - } - .removeDuplicates() - .receive(on: RunLoop.main) - .sink { [weak self] shouldHide in - withAnimation(.smooth) { - self?.hideOnClosed = shouldHide - } - } - .store(in: &cancellables) - } - - // Computed property for effective notch height - var effectiveClosedNotchHeight: CGFloat { - let currentScreen = NSScreen.screens.first { $0.localizedName == screen } - let noNotchAndFullscreen = hideOnClosed && (currentScreen?.safeAreaInsets.top ?? 0 <= 0 || currentScreen == nil) - return noNotchAndFullscreen ? 0 : closedNotchSize.height - } - - func toggleCameraPreview() { - if isRequestingAuthorization { - return - } - - switch webcamManager.authorizationStatus { - case .authorized: - if webcamManager.isSessionRunning { - webcamManager.stopSession() - isCameraExpanded = false - } else if webcamManager.cameraAvailable { - webcamManager.startSession() - isCameraExpanded = true - } - - case .denied, .restricted: - DispatchQueue.main.async { - NSApp.setActivationPolicy(.regular) - NSApp.activate(ignoringOtherApps: true) - - let alert = NSAlert() - alert.messageText = "Camera Access Required" - alert.informativeText = "Please allow camera access in System Settings." - alert.addButton(withTitle: "Open Settings") - alert.addButton(withTitle: "Cancel") - - if alert.runModal() == .alertFirstButtonReturn { - if let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") { - NSWorkspace.shared.open(url) - } - } - - NSApp.setActivationPolicy(.accessory) - NSApp.deactivate() - } - - case .notDetermined: - isRequestingAuthorization = true - webcamManager.checkAndRequestVideoAuthorization() - DispatchQueue.main.asyncAfter(deadline: .now() + 2) { - self.isRequestingAuthorization = false - } - - default: - break - } - } - - func isMouseHovering(position: NSPoint = NSEvent.mouseLocation) -> Bool { - let screenFrame = getScreenFrame(screen) - if let frame = screenFrame { - - let baseY = frame.maxY - notchSize.height - let baseX = frame.midX - notchSize.width / 2 - - return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width - } - - return false - } - - func open() { - withAnimation(.bouncy) { - self.notchSize = openNotchSize - self.notchState = .open - } - - // Force music information update when notch is opened - MusicManager.shared.forceUpdate() - } - - func close() { - withAnimation(.smooth) { [weak self] in - guard let self = self else { return } - self.notchSize = getClosedNotchSize(screen: self.screen) - self.closedNotchSize = self.notchSize - self.notchState = .closed - } - - // Set the current view to shelf if it contains files and the user enables openShelfByDefault - // Otherwise, if the user has not enabled openLastShelfByDefault, set the view to home - if !TrayDrop.shared.isEmpty && Defaults[.openShelfByDefault] { - coordinator.currentView = .shelf - } else if !coordinator.openLastTabByDefault { - coordinator.currentView = .home - } - } - - func closeHello() { - DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in - self?.coordinator.firstLaunch = false - withAnimation(self?.animationLibrary.animation) { - self?.close() - } - } - } + + var coordinator = BoringViewCoordinator.shared + var detector = FullscreenMediaDetector.shared + + let animationLibrary: BoringAnimations = .init() + let animation: Animation? + + @Published var contentType: ContentType = .normal + @Published private(set) var notchState: NotchState = .closed + + @Published var dragDetectorTargeting: Bool = false + @Published var dropZoneTargeting: Bool = false + @Published var dropEvent: Bool = false + @Published var anyDropZoneTargeting: Bool = false + var cancellables: Set = [] + + @Published var hideOnClosed: Bool = true + @Published var isHoveringCalendar: Bool = false + @Published var isBatteryPopoverActive: Bool = false + + @Published var screen: String? + + @Published var notchSize: CGSize = getClosedNotchSize() + @Published var closedNotchSize: CGSize = getClosedNotchSize() + + let webcamManager = WebcamManager.shared + @Published var isCameraExpanded: Bool = false + @Published var isRequestingAuthorization: Bool = false + + @Published var keepUpdating: Bool = false + private var updateCancellable: AnyCancellable? + + func startContinuousUpdate() { + updateCancellable = Timer.publish(every: 0.01, on: .main, in: .common) + .autoconnect() + .sink { [weak self] _ in + guard let self = self, self.keepUpdating else { return } + // Place the code to update your variable here, e.g.: + self.screen = NSScreen.screens.first?.localizedName + } + } + + func stopContinuousUpdate() { + updateCancellable?.cancel() + updateCancellable = nil + } + + override init() { + animation = animationLibrary.animation + super.init() + + notchSize = getClosedNotchSize(screen: screen) + closedNotchSize = notchSize + + setupCombine() + setupDetectorObserver() + } + + deinit { + destroy() + } + + func destroy() { + cancellables.forEach { $0.cancel() } + cancellables.removeAll() + } + + private func setupCombine() { + Publishers.CombineLatest($dropZoneTargeting, $dragDetectorTargeting) + .map { $0 || $1 } + .assign(to: \.anyDropZoneTargeting, on: self) + .store(in: &cancellables) + } + + private func setupDetectorObserver() { + let enabledPublisher = Defaults + .publisher(.enableFullscreenMediaDetection) + .map(\.newValue) + .removeDuplicates() + + let screenPublisher = $screen + .compactMap { $0 } + .removeDuplicates() + + let fullscreenStatusPublisher = detector.$fullscreenStatus + .removeDuplicates() + + Publishers.CombineLatest3(screenPublisher, fullscreenStatusPublisher, enabledPublisher) + .map { screenName, fullscreenStatus, enabled in + let isFullscreen = fullscreenStatus[screenName] ?? false + return enabled && isFullscreen + } + .removeDuplicates() + .receive(on: RunLoop.main) + .sink { [weak self] shouldHide in + withAnimation(.smooth) { + self?.hideOnClosed = shouldHide + } + } + .store(in: &cancellables) + } + + var effectiveClosedNotchHeight: CGFloat { + let currentScreen = NSScreen.screens.first { $0.localizedName == screen } + let noNotchAndFullscreen = hideOnClosed && (currentScreen?.safeAreaInsets.top ?? 0 <= 0 || currentScreen == nil) + return noNotchAndFullscreen ? 0 : closedNotchSize.height + } + + func toggleCameraPreview() { + if isRequestingAuthorization { return } + + switch webcamManager.authorizationStatus { + case .authorized: + if webcamManager.isSessionRunning { + webcamManager.stopSession() + isCameraExpanded = false + } else if webcamManager.cameraAvailable { + webcamManager.startSession() + isCameraExpanded = true + } + + case .denied, .restricted: + DispatchQueue.main.async { + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + + let alert = NSAlert() + alert.messageText = "Camera Access Required" + alert.informativeText = "Please allow camera access in System Settings." + alert.addButton(withTitle: "Open Settings") + alert.addButton(withTitle: "Cancel") + + if alert.runModal() == .alertFirstButtonReturn, + let url = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Camera") { + NSWorkspace.shared.open(url) + } + + NSApp.setActivationPolicy(.accessory) + NSApp.deactivate() + } + + case .notDetermined: + isRequestingAuthorization = true + webcamManager.checkAndRequestVideoAuthorization() + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + self.isRequestingAuthorization = false + } + + default: + break + } + } + + func isMouseHovering(position: NSPoint = NSEvent.mouseLocation) -> Bool { + guard let frame = getScreenFrame(screen) else { return false } + + let baseY = frame.maxY - notchSize.height + let baseX = frame.midX - notchSize.width / 2 + + return position.y >= baseY && position.x >= baseX && position.x <= baseX + notchSize.width + } + + func open() { + withAnimation(.bouncy) { + self.notchSize = openNotchSize + self.notchState = .open + } + + MusicManager.shared.forceUpdate() + } + + func close() { + withAnimation(.smooth) { + self.notchSize = getClosedNotchSize(screen: self.screen) + self.closedNotchSize = self.notchSize + self.notchState = .closed + } + + if !TrayDrop.shared.isEmpty && Defaults[.openShelfByDefault] { + coordinator.currentView = .shelf + } else if !coordinator.openLastTabByDefault { + coordinator.currentView = .home + } + } + + func closeHello() { + DispatchQueue.main.asyncAfter(deadline: .now() + 3.5) { [weak self] in + guard let self = self else { return } + self.coordinator.firstLaunch = false + withAnimation(self.animationLibrary.animation) { + self.close() + } + } + } } + + diff --git a/boringNotch/models/Constants.swift b/boringNotch/models/Constants.swift index 9e060349..b9ddddcf 100644 --- a/boringNotch/models/Constants.swift +++ b/boringNotch/models/Constants.swift @@ -84,6 +84,8 @@ extension Defaults.Keys { //static let openLastTabByDefault = Key("openLastTabByDefault", default: false) // MARK: Appearance + static let backgroundIsBlack = Key("backgroundIsBlack", default: true) + static let backgroundBlackGradient = Key("backgroundBlackGradient", default: 1) static let showEmojis = Key("showEmojis", default: false) //static let alwaysShowTabs = Key("alwaysShowTabs", default: true) static let showMirror = Key("showMirror", default: false)