From fed1a43f88569c8136184ec4612100aa367641b4 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Sat, 4 Oct 2025 22:24:37 +0530 Subject: [PATCH] Working - need to fix ui issues --- .gitignore | 3 +- DynamicIsland/ContentView.swift | 31 ++- DynamicIsland/DynamicIslandApp.swift | 35 ++- DynamicIsland/Localizable.xcstrings | 14 + .../components/MinimalisticMusicView.swift | 83 ++++++ .../Notch/DynamicIslandHeader.swift | 26 +- .../Notch/MinimalisticMusicPlayerView.swift | 254 ++++++++++++++++++ .../components/Notch/NotchHomeView.swift | 40 +-- .../components/Settings/SettingsView.swift | 75 ++++-- DynamicIsland/models/Constants.swift | 3 + .../models/DynamicIslandViewModel.swift | 9 +- DynamicIsland/sizing/matters.swift | 2 + 12 files changed, 510 insertions(+), 65 deletions(-) create mode 100644 DynamicIsland/components/MinimalisticMusicView.swift create mode 100644 DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift diff --git a/.gitignore b/.gitignore index 306064c..25140be 100644 --- a/.gitignore +++ b/.gitignore @@ -73,4 +73,5 @@ gemini-apireference.md ScreenshotApp-main stats -boring.notch \ No newline at end of file +boring.notch +referenceimages \ No newline at end of file diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 66be883..5f0dbe3 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -33,6 +33,9 @@ struct ContentView: View { // Dynamic sizing based on view type and graph count with smooth transitions var dynamicNotchSize: CGSize { + // Use minimalistic or normal size based on settings + let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + // Always return the target size for the current view to ensure smooth transitions let targetSize: CGSize @@ -48,14 +51,14 @@ struct ContentView: View { if enabledGraphsCount > 3 { // Expand height for 4+ graphs targetSize = CGSize( - width: openNotchSize.width, - height: openNotchSize.height + 65 // Add extra height for second row + width: baseSize.width, + height: baseSize.height + 65 // Add extra height for second row ) } else { - targetSize = openNotchSize + targetSize = baseSize } } else { - targetSize = openNotchSize + targetSize = baseSize } return targetSize @@ -85,25 +88,35 @@ struct ContentView: View { @Default(.showNotHumanFace) var showNotHumanFace @Default(.useModernCloseAnimation) var useModernCloseAnimation + @Default(.enableMinimalisticUI) var enableMinimalisticUI private let extendedHoverPadding: CGFloat = 30 private let zeroHeightHoverPadding: CGFloat = 10 + + // Use minimalistic corner radius ONLY when opened, keep normal when closed + private var activeCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) { + if enableMinimalisticUI { + // Keep normal closed corner radius, use minimalistic when opened + return (opened: minimalisticCornerRadiusInsets.opened, closed: cornerRadiusInsets.closed) + } + return cornerRadiusInsets + } var body: some View { ZStack(alignment: .top) { let mainLayout = NotchLayout() .frame(alignment: .top) .padding(.horizontal, vm.notchState == .open - ? Defaults[.cornerRadiusScaling] ? (cornerRadiusInsets.opened.top - 5) : (cornerRadiusInsets.opened.bottom - 5) - : cornerRadiusInsets.closed.bottom + ? Defaults[.cornerRadiusScaling] ? (activeCornerRadiusInsets.opened.top - 5) : (activeCornerRadiusInsets.opened.bottom - 5) + : activeCornerRadiusInsets.closed.bottom ) .padding([.horizontal, .bottom], vm.notchState == .open ? 12 : 0) .background(.black) .mask { ((vm.notchState == .open) && Defaults[.cornerRadiusScaling]) - ? NotchShape(topCornerRadius: cornerRadiusInsets.opened.top, bottomCornerRadius: cornerRadiusInsets.opened.bottom) + ? NotchShape(topCornerRadius: activeCornerRadiusInsets.opened.top, bottomCornerRadius: activeCornerRadiusInsets.opened.bottom) .drawingGroup() - : NotchShape(topCornerRadius: cornerRadiusInsets.closed.top, bottomCornerRadius: cornerRadiusInsets.closed.bottom) + : NotchShape(topCornerRadius: activeCornerRadiusInsets.closed.top, bottomCornerRadius: activeCornerRadiusInsets.closed.bottom) .drawingGroup() } .padding(.bottom, vm.notchState == .open && Defaults[.extendHoverArea] ? 0 : (vm.effectiveClosedNotchHeight == 0) @@ -589,7 +602,7 @@ struct ContentView: View { @ViewBuilder var dragDetector: some View { - if Defaults[.dynamicShelf] { + if Defaults[.dynamicShelf] && !Defaults[.enableMinimalisticUI] { Color.clear .frame(maxWidth: .infinity, maxHeight: .infinity) .contentShape(Rectangle()) diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 9660c89..03134c9 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -349,9 +349,27 @@ class AppDelegate: NSObject, NSApplicationDelegate { } private func calculateRequiredNotchSize() -> CGSize { + // Check if inline sneak peek is showing and notch is closed + let isInlineSneakPeekActive = vm.notchState == .closed && + coordinator.expandingView.show && + (coordinator.expandingView.type == .music || coordinator.expandingView.type == .timer) && + Defaults[.enableSneakPeek] && + Defaults[.sneakPeekStyles] == .inline + + // If inline sneak peek is active, use a wider width to accommodate the expanded content + if isInlineSneakPeekActive { + // Calculate required width for inline sneak peek: + // Album art (~32) + Middle section (380) + Visualizer (~32) + padding = ~450 + let inlineSneakPeekWidth: CGFloat = 450 + return CGSize(width: inlineSneakPeekWidth, height: vm.effectiveClosedNotchHeight) + } + + // Use minimalistic or normal size based on settings + let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + // Only apply dynamic sizing when on stats tab and stats are enabled guard coordinator.currentView == .stats && Defaults[.enableStatsFeature] else { - return openNotchSize + return baseSize } let enabledGraphsCount = [ @@ -363,16 +381,16 @@ class AppDelegate: NSObject, NSApplicationDelegate { ].filter { $0 }.count // Calculate height based on layout: 1-3 graphs = single row, 4+ graphs = two rows - var requiredHeight = openNotchSize.height + var requiredHeight = baseSize.height if enabledGraphsCount >= 4 { // Two rows needed - add height for second row plus spacing let extraHeight: CGFloat = 120 + 12 // Graph height + spacing - requiredHeight = openNotchSize.height + extraHeight + requiredHeight = baseSize.height + extraHeight } // Width stays constant - no horizontal expansion - return CGSize(width: openNotchSize.width, height: requiredHeight) + return CGSize(width: baseSize.width, height: requiredHeight) } func applicationDidFinishLaunching(_ notification: Notification) { @@ -423,6 +441,15 @@ class AppDelegate: NSObject, NSApplicationDelegate { self?.debouncedUpdateWindowSize() }.store(in: &cancellables) + // Observe minimalistic UI setting changes - trigger window resize + Defaults.publisher(.enableMinimalisticUI, options: []).sink { [weak self] change in + // Force sneak peek to standard mode when minimalistic UI is enabled + if change.newValue == true && Defaults[.sneakPeekStyles] != .standard { + Defaults[.sneakPeekStyles] = .standard + } + self?.debouncedUpdateWindowSize() + }.store(in: &cancellables) + // Observe screen recording settings changes Defaults.publisher(.enableScreenRecordingDetection, options: []).sink { _ in if Defaults[.enableScreenRecordingDetection] { diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 89f9ac4..0e6a6b9 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -6,6 +6,10 @@ }, " – %lld" : { + }, + "-%@" : { + "comment" : "A label displaying the duration of the current song. The argument is the duration of the song in seconds.", + "isCommentAutoGenerated" : true }, "'Now Playing' was the only option on previous versions and works with all media apps." : { @@ -867,6 +871,9 @@ }, "min" : { + }, + "Minimalistic mode focuses on media controls and system HUDs, hiding all extra features for a clean, focused experience. Automatically enables simpler animations." : { + }, "Minimum hover duration" : { @@ -1362,6 +1369,10 @@ }, "Sneak Peek Style" : { + }, + "Sneak peek style is locked to Standard in minimalistic mode" : { + "comment" : "A message displayed when the sneak peek style is locked to standard in minimalistic UI mode.", + "isCommentAutoGenerated" : true }, "Software updates" : { "localizations" : { @@ -1510,6 +1521,9 @@ }, "Two-finger swipe up on notch to close, two-finger swipe down on notch to open when **Open notch on hover** option is disabled" : { + }, + "UI Mode" : { + }, "Uninstall" : { diff --git a/DynamicIsland/components/MinimalisticMusicView.swift b/DynamicIsland/components/MinimalisticMusicView.swift new file mode 100644 index 0000000..e297b96 --- /dev/null +++ b/DynamicIsland/components/MinimalisticMusicView.swift @@ -0,0 +1,83 @@ +// +// MinimalisticMusicView.swift +// DynamicIsland +// +// Created for minimalistic UI mode +// A clean, focused music player for closed notch state +// + +import SwiftUI +import Defaults + +struct MinimalisticMusicView: View { + @EnvironmentObject var vm: DynamicIslandViewModel + @ObservedObject var musicManager = MusicManager.shared + @State private var isHovering: Bool = false + + var body: some View { + HStack(spacing: 0) { + // Left: Album Art + albumArtView + + // Middle: Song Title (scrolling if needed) + Rectangle() + .fill(.black) + .overlay( + HStack(alignment: .center) { + if !musicManager.songTitle.isEmpty { + MarqueeText( + .constant(musicManager.songTitle), + textColor: Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor) : Color.gray, + minDuration: 0.4, + frameWidth: 100 + ) + } + } + ) + .frame(width: vm.closedNotchSize.width) + + // Right: Music Visualizer + visualizerView + } + .frame(height: vm.effectiveClosedNotchHeight + (isHovering ? 8 : 0), alignment: .center) + .onHover { hovering in + isHovering = hovering + } + } + + // MARK: - Album Art + + private var albumArtView: some View { + HStack { + Color.clear + .aspectRatio(1, contentMode: .fit) + .background( + Image(nsImage: musicManager.albumArt) + .resizable() + .aspectRatio(contentMode: .fill) + ) + .clipped() + .clipShape(RoundedRectangle(cornerRadius: 18)) // Dramatically increased corner radius for minimalistic mode + .frame(width: max(0, vm.effectiveClosedNotchHeight - 12), height: max(0, vm.effectiveClosedNotchHeight - 12)) + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12))) + } + + // MARK: - Visualizer + + private var visualizerView: some View { + HStack { + Rectangle() + .fill(Defaults[.coloredSpectrogram] ? Color(nsColor: musicManager.avgColor).gradient : Color.gray.gradient) + .frame(width: 50, alignment: .center) + .mask { + AudioSpectrumView(isPlaying: $musicManager.isPlaying) + .frame(width: 16, height: 12) + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), + height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center) + } + .frame(width: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), + height: max(0, vm.effectiveClosedNotchHeight - (isHovering ? 0 : 12)), alignment: .center) + } +} diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index c0c0af2..2a9e3b4 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -20,20 +20,22 @@ struct DynamicIslandHeader: View { var body: some View { HStack(spacing: 0) { - HStack { - if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] { - TabSelectionView() - } else if vm.notchState == .open { - EmptyView() + if !Defaults[.enableMinimalisticUI] { + HStack { + if (!tvm.isEmpty || coordinator.alwaysShowTabs) && Defaults[.dynamicShelf] { + TabSelectionView() + } else if vm.notchState == .open { + EmptyView() + } } + .frame(maxWidth: .infinity, alignment: .leading) + .opacity(vm.notchState == .closed ? 0 : 1) + .blur(radius: vm.notchState == .closed ? 20 : 0) + .animation(.smooth.delay(0.1), value: vm.notchState) + .zIndex(2) } - .frame(maxWidth: .infinity, alignment: .leading) - .opacity(vm.notchState == .closed ? 0 : 1) - .blur(radius: vm.notchState == .closed ? 20 : 0) - .animation(.smooth.delay(0.1), value: vm.notchState) - .zIndex(2) - if vm.notchState == .open { + if vm.notchState == .open && !Defaults[.enableMinimalisticUI] { Rectangle() .fill(NSScreen.screens .first(where: { $0.localizedName == coordinator.selectedScreen })?.safeAreaInsets.top ?? 0 > 0 ? .black : .clear) @@ -44,7 +46,7 @@ struct DynamicIslandHeader: View { } HStack(spacing: 4) { - if vm.notchState == .open { + if vm.notchState == .open && !Defaults[.enableMinimalisticUI] { if Defaults[.showMirror] { Button(action: { vm.toggleCameraPreview() diff --git a/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift new file mode 100644 index 0000000..fe05d52 --- /dev/null +++ b/DynamicIsland/components/Notch/MinimalisticMusicPlayerView.swift @@ -0,0 +1,254 @@ +// +// MinimalisticMusicPlayerView.swift +// DynamicIsland +// +// Created for minimalistic UI mode - Open state music player +// + +import SwiftUI +import Defaults + +struct MinimalisticMusicPlayerView: View { + @EnvironmentObject var vm: DynamicIslandViewModel + let albumArtNamespace: Namespace.ID + + var body: some View { + VStack(spacing: 6) { + // Music banner: Album art + Song info + Visualizer + musicBanner + + // Full-width progress bar with time labels + progressBar + + // Larger playback controls + playbackControls + } + .padding(.horizontal, 16) + .padding(.top, 4) + .padding(.bottom, 4) + .frame(maxWidth: .infinity) + } + + // MARK: - Music Banner (Compact) + + @Default(.useMusicVisualizer) var useMusicVisualizer + + private var musicBanner: some View { + HStack(spacing: 10) { + // Album Art - Smaller + MinimalisticAlbumArtView(vm: vm, albumArtNamespace: albumArtNamespace) + .frame(width: 50, height: 50) + + // Song Title and Artist + songInfo + + Spacer() + + // Visualizer from live activity (only if enabled) + if useMusicVisualizer { + visualizer + } + } + .frame(height: 50) + } + + private var songInfo: some View { + VStack(alignment: .leading, spacing: 2) { + Text(MusicManager.shared.songTitle) + .font(.system(size: 14, weight: .semibold)) + .foregroundColor(.white) + .lineLimit(1) + + Text(MusicManager.shared.artistName) + .font(.system(size: 12, weight: .regular)) + .foregroundColor(Defaults[.playerColorTinting] ? Color(nsColor: MusicManager.shared.avgColor).ensureMinimumBrightness(factor: 0.6) : .gray) + .lineLimit(1) + } + } + + private var visualizer: some View { + Rectangle() + .fill(Defaults[.coloredSpectrogram] ? Color(nsColor: MusicManager.shared.avgColor).gradient : Color.gray.gradient) + .mask { + AudioSpectrumView(isPlaying: .constant(MusicManager.shared.isPlaying)) + .frame(width: 20, height: 16) + } + .frame(width: 20, height: 16) + .matchedGeometryEffect(id: "spectrum", in: albumArtNamespace) + } + + // MARK: - Progress Bar (Full Width) + + @ObservedObject var musicManager = MusicManager.shared + @State private var sliderValue: Double = 0 + @State private var dragging: Bool = false + @State private var lastDragged: Date = .distantPast + + private var progressBar: some View { + HStack(spacing: 8) { + // Elapsed time - left + Text(formatTime(dragging ? sliderValue : musicManager.elapsedTime)) + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundColor(.white.opacity(0.7)) + .frame(width: 42, alignment: .leading) + + // Progress bar + TimelineView(.animation(minimumInterval: 0.05)) { timeline in + GeometryReader { geometry in + ZStack(alignment: .leading) { + // Background track + RoundedRectangle(cornerRadius: 3) + .fill(Color.white.opacity(0.2)) + .frame(height: 6) + + // Filled portion + RoundedRectangle(cornerRadius: 3) + .fill(sliderColor) + .frame(width: max(0, geometry.size.width * (currentSliderValue(timeline.date) / max(musicManager.songDuration, 1))), height: 6) + } + .gesture( + DragGesture(minimumDistance: 0) + .onChanged { value in + dragging = true + let newValue = min(max(0, Double(value.location.x / geometry.size.width) * musicManager.songDuration), musicManager.songDuration) + sliderValue = newValue + lastDragged = Date() + } + .onEnded { _ in + musicManager.seek(to: sliderValue) + dragging = false + } + ) + } + } + .frame(height: 6) + + // Time remaining - right + Text("-\(formatTime(musicManager.songDuration - (dragging ? sliderValue : musicManager.elapsedTime)))") + .font(.system(size: 11, weight: .medium, design: .monospaced)) + .foregroundColor(.white.opacity(0.7)) + .frame(width: 48, alignment: .trailing) + } + .onAppear { + sliderValue = musicManager.elapsedTime + } + } + + private func currentSliderValue(_ date: Date) -> Double { + if dragging { + return sliderValue + } + + // Update slider value based on playback + if musicManager.isPlaying { + let timeSinceLastUpdate = date.timeIntervalSince(musicManager.lastUpdated) + let estimatedElapsed = musicManager.elapsedTime + (timeSinceLastUpdate * musicManager.playbackRate) + return min(estimatedElapsed, musicManager.songDuration) + } + + return musicManager.elapsedTime + } + + private var sliderColor: Color { + switch Defaults[.sliderColor] { + case .white: + return .white + case .albumArt: + return Color(nsColor: musicManager.avgColor) + case .accent: + return .accentColor + } + } + + private func formatTime(_ time: TimeInterval) -> String { + let minutes = Int(time) / 60 + let seconds = Int(time) % 60 + return String(format: "%d:%02d", minutes, seconds) + } + + // MARK: - Playback Controls (Larger) + + private var playbackControls: some View { + HStack(spacing: 20) { + if Defaults[.showShuffleAndRepeat] { + controlButton(icon: "shuffle", isActive: musicManager.isShuffled) { + Task { await musicManager.toggleShuffle() } + } + } + + controlButton(icon: "backward.fill", size: 18) { + Task { await musicManager.previousTrack() } + } + + playPauseButton + + controlButton(icon: "forward.fill", size: 18) { + Task { await musicManager.nextTrack() } + } + + if Defaults[.showShuffleAndRepeat] { + controlButton(icon: repeatIcon, isActive: musicManager.repeatMode != .off) { + Task { await musicManager.toggleRepeat() } + } + } + } + .frame(maxWidth: .infinity, alignment: .center) + .padding(.top, 2) + } + + private var playPauseButton: some View { + Button(action: { + Task { await musicManager.togglePlay() } + }) { + Image(systemName: musicManager.isPlaying ? "pause.fill" : "play.fill") + .font(.system(size: 24, weight: .semibold)) + .foregroundColor(.white) + .frame(width: 50, height: 50) + } + .buttonStyle(PlainButtonStyle()) + } + + private func controlButton(icon: String, size: CGFloat = 18, isActive: Bool = false, action: @escaping () -> Void) -> some View { + Button(action: action) { + Image(systemName: icon) + .font(.system(size: size, weight: .medium)) + .foregroundColor(isActive ? .red : .white.opacity(0.8)) + } + .buttonStyle(PlainButtonStyle()) + } + + private var repeatIcon: String { + switch musicManager.repeatMode { + case .off: return "repeat" + case .all: return "repeat" + case .one: return "repeat.1" + } + } +} + +// MARK: - Minimalistic Album Art + +struct MinimalisticAlbumArtView: View { + @ObservedObject var musicManager = MusicManager.shared + @ObservedObject var vm: DynamicIslandViewModel + let albumArtNamespace: Namespace.ID + + var body: some View { + Button { + musicManager.openMusicApp() + } label: { + Color.clear + .aspectRatio(1, contentMode: .fit) + .background( + Image(nsImage: musicManager.albumArt) + .resizable() + .aspectRatio(contentMode: .fill) + ) + .clipped() + .clipShape(RoundedRectangle(cornerRadius: 10)) + .matchedGeometryEffect(id: "albumArt", in: albumArtNamespace) + } + .buttonStyle(PlainButtonStyle()) + .opacity(musicManager.isPlaying ? 1 : 0.4) + } +} diff --git a/DynamicIsland/components/Notch/NotchHomeView.swift b/DynamicIsland/components/Notch/NotchHomeView.swift index c494630..f284cea 100644 --- a/DynamicIsland/components/Notch/NotchHomeView.swift +++ b/DynamicIsland/components/Notch/NotchHomeView.swift @@ -246,23 +246,29 @@ struct NotchHomeView: View { private var mainContent: some View { HStack(alignment: .top, spacing: 20) { - MusicPlayerView(albumArtNamespace: albumArtNamespace) - - if Defaults[.showCalendar] { - CalendarView() - .onHover { isHovering in - vm.isHoveringCalendar = isHovering - } - .environmentObject(vm) - } - - if Defaults[.showMirror], - webcamManager.cameraAvailable, - vm.notchState == .open { - CameraPreviewView(webcamManager: webcamManager) - .scaledToFit() - .opacity(vm.notchState == .closed ? 0 : 1) - .blur(radius: vm.notchState == .closed ? 20 : 0) + if Defaults[.enableMinimalisticUI] { + // Minimalistic mode: Only show compact music player + MinimalisticMusicPlayerView(albumArtNamespace: albumArtNamespace) + } else { + // Normal mode: Show full music player with optional calendar and webcam + MusicPlayerView(albumArtNamespace: albumArtNamespace) + + if Defaults[.showCalendar] { + CalendarView() + .onHover { isHovering in + vm.isHoveringCalendar = isHovering + } + .environmentObject(vm) + } + + if Defaults[.showMirror], + webcamManager.cameraAvailable, + vm.notchState == .open { + CameraPreviewView(webcamManager: webcamManager) + .scaledToFit() + .opacity(vm.notchState == .closed ? 0 : 1) + .blur(radius: vm.notchState == .closed ? 20 : 0) + } } } .transition(.opacity.animation(.smooth.speed(0.9)) diff --git a/DynamicIsland/components/Settings/SettingsView.swift b/DynamicIsland/components/Settings/SettingsView.swift index 524612e..ba61c56 100644 --- a/DynamicIsland/components/Settings/SettingsView.swift +++ b/DynamicIsland/components/Settings/SettingsView.swift @@ -19,6 +19,7 @@ import UniformTypeIdentifiers struct SettingsView: View { @StateObject var extensionManager = DynamicIslandExtensionManager() @State private var selectedTab = "General" + @Default(.enableMinimalisticUI) var enableMinimalisticUI let updaterController: SPUStandardUpdaterController? @@ -38,8 +39,10 @@ struct SettingsView: View { NavigationLink(value: "Media") { Label("Media", systemImage: "play.laptopcomputer") } - NavigationLink(value: "Calendar") { - Label("Calendar", systemImage: "calendar") + if !enableMinimalisticUI { + NavigationLink(value: "Calendar") { + Label("Calendar", systemImage: "calendar") + } } NavigationLink(value: "HUD") { Label("HUDs", systemImage: "dial.medium.fill") @@ -47,20 +50,22 @@ struct SettingsView: View { NavigationLink(value: "Battery") { Label("Battery", systemImage: "battery.100.bolt") } - NavigationLink(value: "Timer") { - Label("Timer", systemImage: "timer") - } - NavigationLink(value: "Stats") { - Label("Stats", systemImage: "chart.xyaxis.line") - } - NavigationLink(value: "Clipboard") { - Label("Clipboard", systemImage: "clipboard") - } - NavigationLink(value: "ScreenAssistant") { - Label("Screen Assistant", systemImage: "brain.head.profile") - } - NavigationLink(value: "ColorPicker") { - Label("Color Picker", systemImage: "eyedropper") + if !enableMinimalisticUI { + NavigationLink(value: "Timer") { + Label("Timer", systemImage: "timer") + } + NavigationLink(value: "Stats") { + Label("Stats", systemImage: "chart.xyaxis.line") + } + NavigationLink(value: "Clipboard") { + Label("Clipboard", systemImage: "clipboard") + } + NavigationLink(value: "ScreenAssistant") { + Label("Screen Assistant", systemImage: "brain.head.profile") + } + NavigationLink(value: "ColorPicker") { + Label("Color Picker", systemImage: "eyedropper") + } } if extensionManager.installedExtensions .contains(where: { $0.bundleIdentifier == downloadManagerExtension }) { @@ -68,8 +73,10 @@ struct SettingsView: View { Label("Downloads", systemImage: "square.and.arrow.down") } } - NavigationLink(value: "Shelf") { - Label("Shelf", systemImage: "books.vertical") + if !enableMinimalisticUI { + NavigationLink(value: "Shelf") { + Label("Shelf", systemImage: "books.vertical") + } } NavigationLink(value: "Shortcuts") { Label("Shortcuts", systemImage: "keyboard") @@ -165,9 +172,24 @@ struct GeneralSettings: View { @Default(.openNotchOnHover) var openNotchOnHover @Default(.enableScreenRecordingDetection) var enableScreenRecordingDetection @Default(.showRecordingIndicator) var showRecordingIndicator + @Default(.enableMinimalisticUI) var enableMinimalisticUI var body: some View { Form { + Section { + Defaults.Toggle("Enable Minimalistic UI", key: .enableMinimalisticUI) + .onChange(of: enableMinimalisticUI) { _, newValue in + if newValue { + // Auto-enable simpler animation mode + Defaults[.useModernCloseAnimation] = true + } + } + } header: { + Text("UI Mode") + } footer: { + Text("Minimalistic mode focuses on media controls and system HUDs, hiding all extra features for a clean, focused experience. Automatically enables simpler animations.") + } + Section { Defaults.Toggle("Menubar icon", key: .menubarIcon) LaunchAtLogin.Toggle("Launch at login") @@ -563,6 +585,7 @@ struct Media: View { @Default(.hideNotchOption) var hideNotchOption @Default(.enableSneakPeek) private var enableSneakPeek @Default(.sneakPeekStyles) var sneakPeekStyles + @Default(.enableMinimalisticUI) var enableMinimalisticUI var body: some View { Form { @@ -616,7 +639,21 @@ struct Media: View { ForEach(SneakPeekStyle.allCases) { style in Text(style.rawValue).tag(style) } - }.disabled(!enableSneakPeek) + } + .disabled(!enableSneakPeek || enableMinimalisticUI) + .onChange(of: enableMinimalisticUI) { _, isMinimalistic in + // Force standard sneak peek style when minimalistic UI is enabled + if isMinimalistic { + sneakPeekStyles = .standard + } + } + + if enableMinimalisticUI { + Text("Sneak peek style is locked to Standard in minimalistic mode") + .font(.caption) + .foregroundStyle(.secondary) + } + HStack { Stepper(value: $waitInterval, in: 0...10, step: 1) { HStack { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 57d060b..87ae850 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -349,6 +349,9 @@ extension Defaults.Keys { // Polling removed - now uses event-driven private API detection (CGSIsScreenWatcherPresent) // static let enableScreenRecordingPolling = Key("enableScreenRecordingPolling", default: false) + // MARK: Minimalistic UI Mode + static let enableMinimalisticUI = Key("enableMinimalisticUI", default: false) + // Helper to determine the default media controller based on macOS version static var defaultMediaController: MediaControllerType { if #available(macOS 15.4, *) { diff --git a/DynamicIsland/models/DynamicIslandViewModel.swift b/DynamicIsland/models/DynamicIslandViewModel.swift index 54e1aec..d1e5237 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -132,9 +132,12 @@ class DynamicIslandViewModel: NSObject, ObservableObject { } private func calculateDynamicNotchSize() -> CGSize { + // Use minimalistic size if minimalistic UI is enabled + let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + // Only apply dynamic sizing when on stats tab and stats are enabled guard DynamicIslandViewCoordinator.shared.currentView == .stats && Defaults[.enableStatsFeature] else { - return openNotchSize + return baseSize } let enabledGraphsCount = [ @@ -148,10 +151,10 @@ class DynamicIslandViewModel: NSObject, ObservableObject { // If 4+ graphs are enabled, increase width if enabledGraphsCount >= 4 { let extraWidth: CGFloat = CGFloat(enabledGraphsCount - 3) * 120 - return CGSize(width: openNotchSize.width + extraWidth, height: openNotchSize.height) + return CGSize(width: baseSize.width + extraWidth, height: baseSize.height) } - return openNotchSize + return baseSize } func close() { diff --git a/DynamicIsland/sizing/matters.swift b/DynamicIsland/sizing/matters.swift index 9d169b2..653f36d 100644 --- a/DynamicIsland/sizing/matters.swift +++ b/DynamicIsland/sizing/matters.swift @@ -15,7 +15,9 @@ let downloadSneakSize: CGSize = .init(width: 65, height: 1) let batterySneakSize: CGSize = .init(width: 160, height: 1) let openNotchSize: CGSize = .init(width: 640, height: 190) +let minimalisticOpenNotchSize: CGSize = .init(width: 420, height: 190) let cornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 19, bottom: 24), closed: (top: 6, bottom: 14)) +let minimalisticCornerRadiusInsets: (opened: (top: CGFloat, bottom: CGFloat), closed: (top: CGFloat, bottom: CGFloat)) = (opened: (top: 35, bottom: 35), closed: cornerRadiusInsets.closed) enum MusicPlayerImageSizes { static let cornerRadiusInset: (opened: CGFloat, closed: CGFloat) = (opened: 13.0, closed: 4.0)