From dad7aad555655a722c4dbdc6eca3e098eb77f5d3 Mon Sep 17 00:00:00 2001 From: Hariharan Mudaliar Date: Tue, 2 Sep 2025 19:40:45 +0530 Subject: [PATCH] Untrack Files --- SettingsView.swift.md | 1635 ----------------------------------------- logic.md | 159 ---- 2 files changed, 1794 deletions(-) delete mode 100644 SettingsView.swift.md delete mode 100644 logic.md diff --git a/SettingsView.swift.md b/SettingsView.swift.md deleted file mode 100644 index d8817e4..0000000 --- a/SettingsView.swift.md +++ /dev/null @@ -1,1635 +0,0 @@ -// -// SettingsView.swift -// DynamicIsland -// -// Created by Richard Kunkli on 07/08/2024. -// Modified by Hariharan Mudaliar - -import AVFoundation -import Defaults -import EventKit -import KeyboardShortcuts -import LaunchAtLogin -import LottieUI -import Sparkle -import SwiftUI -import SwiftUIIntrospect -import UniformTypeIdentifiers - -struct SettingsView: View { - @StateObject var extensionManager = DynamicIslandExtensionManager() - @StateObject private var calendarManager = CalendarManager() - - @State private var selectedTab = "General" - - let updaterController: SPUStandardUpdaterController? - - 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: "Timer") { - Label("Timer", systemImage: "timer") - } - 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: "Stats") { - Label("Stats", systemImage: "chart.line.uptrend.xyaxis") - } - NavigationLink(value: "Clipboard") { - Label("Clipboard", systemImage: "doc.on.clipboard") - } - 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 "Timer": - TimerSettings() - case "Calendar": - CalendarSettings() - case "HUD": - HUD() - case "Battery": - Charge() - case "Downloads": - Downloads() - case "Shelf": - Shelf() - case "Stats": - Stats() - case "Clipboard": - ClipboardSettings() - 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) - .environmentObject(calendarManager) - .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: DynamicIslandViewModel - @ObservedObject var coordinator = DynamicIslandViewCoordinator.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(.alwaysHideInFullscreen) var alwaysHideInFullscreen - - var body: some View { - Form { - Section { - Defaults.Toggle("Menubar icon", key: .menubarIcon) - 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("Automatically switch displays", key: .automaticallySwitchDisplay) - .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") - } - - NotchBehaviour() - - 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("Enable gestures", key: .enableGestures) - .disabled(!openNotchOnHover) - if enableGestures { - Toggle("Media change with horizontal gestures", isOn: .constant(false)) - .disabled(true) - Defaults.Toggle("Close gesture", key: .closeGestureEnabled) - 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("Extend hover area", key: .extendHoverArea) - Defaults.Toggle("Enable haptics", key: .enableHaptics) - Defaults.Toggle("Open notch on hover", key: .openNotchOnHover) - 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("Show battery indicator", key: .showBatteryIndicator) - Defaults.Toggle("Show power status notifications", key: .showPowerStatusNotifications) - } header: { - Text("General") - } - Section { - Defaults.Toggle("Show battery percentage", key: .showBatteryPercentage) - Defaults.Toggle("Show power status icons", key: .showPowerStatusIcons) - } header: { - Text("Battery Information") - } - } - .navigationTitle("Battery") - } -} - -struct Downloads: View { - @Default(.selectedDownloadIndicatorStyle) var selectedDownloadIndicatorStyle - @Default(.selectedDownloadIconStyle) var selectedDownloadIconStyle - var body: some View { - Form { - warningBadge("We don't support downloads yet", "It will be supported later on.") - Section { - Defaults.Toggle("Show download progress", key: .enableDownloadListener) - .disabled(true) - Defaults.Toggle("Enable Safari Downloads", key: .enableSafariDownloads) - .disabled(!Defaults[.enableDownloadListener]) - Picker("Download indicator style", selection: $selectedDownloadIndicatorStyle) { - Text("Progress bar") - .tag(DownloadIndicatorStyle.progress) - Text("Percentage") - .tag(DownloadIndicatorStyle.percentage) - } - Picker("Download icon style", selection: $selectedDownloadIconStyle) { - Text("Only app icon") - .tag(DownloadIconStyle.onlyAppIcon) - Text("Only download icon") - .tag(DownloadIconStyle.onlyIcon) - Text("Both") - .tag(DownloadIconStyle.iconAndAppIcon) - } - - } header: { - HStack { - Text("Download indicators") - comingSoonTag() - } - } - Section { - List { - ForEach([].indices, id: \.self) { index in - Text("\(index)") - } - } - .frame(minHeight: 96) - .overlay { - if true { - Text("No excluded apps") - .foregroundStyle(Color(.secondaryLabelColor)) - } - } - .actionBar(padding: 0) { - Group { - Button {} label: { - Image(systemName: "plus") - .frame(width: 25, height: 16, alignment: .center) - .contentShape(Rectangle()) - .foregroundStyle(.secondary) - } - - Divider() - Button {} label: { - Image(systemName: "minus") - .frame(width: 20, height: 16, alignment: .center) - .contentShape(Rectangle()) - .foregroundStyle(.secondary) - } - } - } - } header: { - HStack(spacing: 4) { - Text("Exclude apps") - comingSoonTag() - } - } - } - .navigationTitle("Downloads") - } -} - -struct HUD: View { - @EnvironmentObject var vm: DynamicIslandViewModel - @Default(.inlineHUD) var inlineHUD - @Default(.enableGradient) var enableGradient - @ObservedObject var coordinator = DynamicIslandViewCoordinator.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("Enable glowing effect", key: .systemEventIndicatorShadow) - Defaults.Toggle("Use accent color", key: .systemEventIndicatorUseAccent) - } header: { - HStack { - Text("Appearance") - } - } - } - .navigationTitle("HUDs") - } -} - -struct Media: View { - @Default(.waitInterval) var waitInterval - @Default(.mediaController) var mediaController - @ObservedObject var coordinator = DynamicIslandViewCoordinator.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 { - Toggle( - "Enable music live activity", - isOn: $coordinator.musicLiveActivityEnabled - ) - .animation(.easeInOut, value: coordinator.musicLiveActivityEnabled) - - 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 DynamicIsland 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) { _, newValue in - Defaults[.alwaysHideInFullscreen] = newValue == .always - Defaults[.enableFullscreenMediaDetection] = newValue != .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 - } - } -} - -struct CalendarSettings: View { - @ObservedObject private var calendarManager = CalendarManager() - @Default(.showCalendar) var showCalendar: Bool - - var body: some View { - Form { - if calendarManager.authorizationStatus != .fullAccess { - Text("Calendar access is denied. Please enable it in System Settings.") - .foregroundColor(.red) - .multilineTextAlignment(.center) - .padding() - Button("Open System Settings") { - if let settingsURL = URL(string: "x-apple.systempreferences:com.apple.preference.security?Privacy_Calendars") { - NSWorkspace.shared.open(settingsURL) - } - } - } else { - Toggle("Show calendar", isOn: $showCalendar) - Section(header: Text("Select Calendars")) { - List { - ForEach(calendarManager.allCalendars, id: \.id) { calendar in - Toggle(isOn: Binding( - get: { calendarManager.getCalendarSelected(calendar) }, - set: { isSelected in - Task { - await calendarManager.setCalendarSelected(calendar, isSelected: isSelected) - } - } - )) { - Text(calendar.title) - } - } - } - } - } - } - .onAppear { - Task { - await calendarManager.checkCalendarAuthorization() - } - } - // Add navigation title if it's missing or adjust as needed - .navigationTitle("Calendar") - } -} - -struct TimerSettings: View { - @ObservedObject private var coordinator = DynamicIslandViewCoordinator.shared - @Default(.enableTimerFeature) var enableTimerFeature - @AppStorage("customTimerDuration") private var customTimerDuration: Double = 600 // 10 minutes default - @State private var customMinutes: Int = 10 - @State private var customSeconds: Int = 0 - - var body: some View { - Form { - Section { - Defaults.Toggle("Enable timer feature", key: .enableTimerFeature) - - if enableTimerFeature { - Toggle( - "Enable timer live activity", - isOn: $coordinator.timerLiveActivityEnabled - ) - .animation(.easeInOut, value: coordinator.timerLiveActivityEnabled) - } - } header: { - Text("Timer Feature") - } footer: { - Text("Enable or disable the timer functionality in the Dynamic Island. The live activity toggle controls whether timer progress is shown in the expanded view.") - } - - if enableTimerFeature { - Section { - VStack(alignment: .leading, spacing: 16) { - Text("Custom Timer Duration") - .font(.headline) - - HStack(spacing: 16) { - VStack { - Text("Minutes") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) - - Picker(selection: $customMinutes, label: Text("Minutes")) { - ForEach(0...59, id: \.self) { minute in - Text("\(minute)").tag(minute) - } - } - .pickerStyle(.menu) - .frame(width: 80, height: 40) - } - - VStack { - Text("Seconds") - .font(.system(size: 14, weight: .medium)) - .foregroundStyle(.secondary) - - Picker(selection: $customSeconds, label: Text("Seconds")) { - ForEach(0...59, id: \.self) { second in - Text("\(second)").tag(second) - } - } - .pickerStyle(.menu) - .frame(width: 80, height: 40) - } - } - - HStack { - Text("Current custom timer: \(customTimerDisplayText)") - .foregroundStyle(.secondary) - Spacer() - Button("Update") { - customTimerDuration = Double(customMinutes * 60 + customSeconds) - } - .disabled(customMinutes == 0 && customSeconds == 0) - } - } - } header: { - Text("Custom Timer") - } footer: { - Text("Set a custom duration for the timer. This will be used when you press the 'Custom' button in the timer interface.") - } - - Section { - VStack(alignment: .leading, spacing: 8) { - HStack { - Text("Timer Sound") - .font(.system(size: 16, weight: .medium)) - Spacer() - Button("Choose File") { - selectCustomTimerSound() - } - .buttonStyle(.bordered) - } - - if let customTimerSoundPath = UserDefaults.standard.string(forKey: "customTimerSoundPath") { - Text("Custom: \(URL(fileURLWithPath: customTimerSoundPath).lastPathComponent)") - .font(.caption) - .foregroundColor(.secondary) - } else { - Text("Default: dynamic.m4a") - .font(.caption) - .foregroundColor(.secondary) - } - - Button("Reset to Default") { - UserDefaults.standard.removeObject(forKey: "customTimerSoundPath") - } - .buttonStyle(.bordered) - .disabled(UserDefaults.standard.string(forKey: "customTimerSoundPath") == nil) - } - } header: { - Text("Timer Sound") - } footer: { - Text("Choose a custom sound file that will play when the timer completes. Supported formats: MP3, M4A, WAV, AIFF.") - } - } - } - .navigationTitle("Timer") - .onAppear { - let totalMinutes = Int(customTimerDuration) / 60 - customMinutes = totalMinutes - customSeconds = Int(customTimerDuration) % 60 - } - } - - private var customTimerDisplayText: String { - let totalMinutes = Int(customTimerDuration) / 60 - let seconds = Int(customTimerDuration) % 60 - let hours = totalMinutes / 60 - let minutes = totalMinutes % 60 - - if hours > 0 { - return "\(hours):\(String(format: "%02d", minutes)):\(String(format: "%02d", seconds))" - } else if minutes > 0 { - return "\(minutes):\(String(format: "%02d", seconds))" - } else { - return "\(seconds)s" - } - } - - private func selectCustomTimerSound() { - let panel = NSOpenPanel() - panel.title = "Select Timer Sound" - panel.allowedContentTypes = [.audio] - panel.allowsMultipleSelection = false - panel.canChooseDirectories = false - panel.canChooseFiles = true - - if panel.runModal() == .OK { - if let url = panel.url { - UserDefaults.standard.set(url.path, forKey: "customTimerSoundPath") - } - } - } -} - -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") - } - - 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") - } -} - -struct Shelf: View { - var body: some View { - Form { - Section { - Defaults.Toggle("Enable shelf", key: .dynamicShelf) - Defaults.Toggle("Open shelf tab by default if items added", key: .openShelfByDefault) - } header: { - HStack { - Text("General") - } - } - } - .navigationTitle("Shelf") - } -} - -struct Extensions: View { - @EnvironmentObject var extensionManager: DynamicIslandExtensionManager - @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 - - 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) - } -} - -struct Appearance: View { - @ObservedObject var coordinator = DynamicIslandViewCoordinator.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 - - @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("Settings icon in notch", key: .settingsIconInNotch) - Defaults.Toggle("Enable window shadow", key: .enableShadow) - Defaults.Toggle("Corner radius scaling", key: .cornerRadiusScaling) - Defaults.Toggle("Use simpler close animation", key: .useModernCloseAnimation) - } header: { - Text("General") - } - - Section { - Defaults.Toggle("Enable colored spectrograms", key: .coloredSpectrogram) - Defaults - .Toggle("Player tinting", key: .playerColorTinting) - Defaults.Toggle("Enable blur effect behind album art", key: .lightingEffect) - 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 { - 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 - ) - - 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) - } - } - } - - Section { - Defaults.Toggle("Enable boring mirror", key: .showMirror) - .disabled(!checkVideoInput()) - Picker("Mirror shape", selection: $mirrorShape) { - Text("Circle") - .tag(MirrorShapeEnum.circle) - Text("Square") - .tag(MirrorShapeEnum.rectangle) - } - Defaults.Toggle("Show cool face animation while inactivity", key: .showNotHumanFace) - } 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 - ) - ) - - 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 let _ = AVCaptureDevice.default(for: .video) { - return true - } - - 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) - } - Section { - KeyboardShortcuts.Recorder("Start Demo Timer:", name: .startDemoTimer) - } header: { - Text("Timer") - } footer: { - Text("Starts a 5-minute demo timer to test the timer live activity feature.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - } - .navigationTitle("Shortcuts") - } -} - -struct Stats: View { - @ObservedObject var statsManager = StatsManager.shared - @Default(.enableStatsFeature) var enableStatsFeature - @Default(.showCpuGraph) var showCpuGraph - @Default(.showMemoryGraph) var showMemoryGraph - @Default(.showGpuGraph) var showGpuGraph - @Default(.showNetworkGraph) var showNetworkGraph - @Default(.showDiskGraph) var showDiskGraph - - var enabledGraphsCount: Int { - [showCpuGraph, showMemoryGraph, showGpuGraph, showNetworkGraph, showDiskGraph].filter { $0 }.count - } - - var body: some View { - Form { - Section { - Defaults.Toggle("Enable system stats monitoring", key: .enableStatsFeature) - .onChange(of: enableStatsFeature) { _, newValue in - if newValue { - statsManager.startMonitoring() - } else { - statsManager.stopMonitoring() - } - } - } header: { - Text("General") - } footer: { - Text("When enabled, the Stats tab will display real-time system performance graphs. This feature requires system permissions and may use additional battery.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - - if enableStatsFeature { - Section { - Defaults.Toggle("CPU Usage", key: .showCpuGraph) - Defaults.Toggle("Memory Usage", key: .showMemoryGraph) - Defaults.Toggle("GPU Usage", key: .showGpuGraph) - Defaults.Toggle("Network Activity", key: .showNetworkGraph) - Defaults.Toggle("Disk I/O", key: .showDiskGraph) - } header: { - Text("Graph Visibility") - } footer: { - if enabledGraphsCount >= 4 { - Text("With \(enabledGraphsCount) graphs enabled, the Dynamic Island will expand horizontally to accommodate all graphs in a single row.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } else { - Text("Each graph can be individually enabled or disabled. Network activity shows download/upload speeds, and disk I/O shows read/write speeds.") - .multilineTextAlignment(.trailing) - .foregroundStyle(.secondary) - .font(.caption) - } - } - - Section { - HStack { - Text("Monitoring Status") - Spacer() - HStack(spacing: 4) { - Circle() - .fill(statsManager.isMonitoring ? .green : .red) - .frame(width: 8, height: 8) - Text(statsManager.isMonitoring ? "Active" : "Stopped") - .foregroundStyle(.secondary) - } - } - - if statsManager.isMonitoring { - if showCpuGraph { - HStack { - Text("CPU Usage") - Spacer() - Text(statsManager.cpuUsageString) - .foregroundStyle(.secondary) - } - } - - if showMemoryGraph { - HStack { - Text("Memory Usage") - Spacer() - Text(statsManager.memoryUsageString) - .foregroundStyle(.secondary) - } - } - - if showGpuGraph { - HStack { - Text("GPU Usage") - Spacer() - Text(statsManager.gpuUsageString) - .foregroundStyle(.secondary) - } - } - - if showNetworkGraph { - HStack { - Text("Network Download") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.networkDownload)) - .foregroundStyle(.secondary) - } - - HStack { - Text("Network Upload") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.networkUpload)) - .foregroundStyle(.secondary) - } - } - - if showDiskGraph { - HStack { - Text("Disk Read") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.diskRead)) - .foregroundStyle(.secondary) - } - - HStack { - Text("Disk Write") - Spacer() - Text(String(format: "%.1f MB/s", statsManager.diskWrite)) - .foregroundStyle(.secondary) - } - } - - HStack { - Text("Last Updated") - Spacer() - Text(statsManager.lastUpdated, style: .relative) - .foregroundStyle(.secondary) - } - } - } header: { - Text("Live Performance Data") - } - - Section { - HStack { - Button(statsManager.isMonitoring ? "Stop Monitoring" : "Start Monitoring") { - if statsManager.isMonitoring { - statsManager.stopMonitoring() - } else { - statsManager.startMonitoring() - } - } - .buttonStyle(.borderedProminent) - .foregroundColor(statsManager.isMonitoring ? .red : .blue) - - Spacer() - - Button("Clear Data") { - statsManager.clearHistory() - } - .buttonStyle(.bordered) - .disabled(statsManager.isMonitoring) - } - } header: { - Text("Controls") - } - } - } - .navigationTitle("Stats") - } -} - -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)) -} - -func comingSoonTag() -> some View { - 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) -} - -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() - } - } -} - -struct ClipboardSettings: View { - @ObservedObject var clipboardManager = ClipboardManager.shared - @Default(.enableClipboardManager) var enableClipboardManager - @Default(.clipboardHistorySize) var clipboardHistorySize - @Default(.showClipboardIcon) var showClipboardIcon - - var body: some View { - Form { - Section { - Defaults.Toggle("Enable Clipboard Manager", key: .enableClipboardManager) - .onChange(of: enableClipboardManager) { enabled in - if enabled { - clipboardManager.startMonitoring() - } else { - clipboardManager.stopMonitoring() - } - } - } header: { - Text("Clipboard Manager") - } footer: { - Text("Monitor clipboard changes and keep a history of recent copies. Use Cmd+Shift+V to quickly access clipboard history.") - } - - if enableClipboardManager { - Section { - Defaults.Toggle("Show Clipboard Icon", key: .showClipboardIcon) - - HStack { - Text("History Size") - Spacer() - Picker("History Size", selection: $clipboardHistorySize) { - Text("3 items").tag(3) - Text("5 items").tag(5) - Text("7 items").tag(7) - Text("10 items").tag(10) - } - .pickerStyle(.menu) - .frame(width: 100) - } - - HStack { - Text("Current Items") - Spacer() - Text("\(clipboardManager.clipboardHistory.count)") - .foregroundColor(.secondary) - } - - HStack { - Text("Monitoring Status") - Spacer() - Text(clipboardManager.isMonitoring ? "Active" : "Stopped") - .foregroundColor(clipboardManager.isMonitoring ? .green : .secondary) - } - } header: { - Text("Settings") - } footer: { - Text("The clipboard icon appears in the header next to the settings gear when enabled. Use the global shortcut Cmd+Shift+V or click the icon to access history.") - } - - Section { - Button("Clear Clipboard History") { - clipboardManager.clearHistory() - } - .foregroundColor(.red) - .disabled(clipboardManager.clipboardHistory.isEmpty) - } header: { - Text("Actions") - } footer: { - Text("This will permanently delete all stored clipboard history. The clipboard button is located to the left of the settings gear in the header.") - } - - if !clipboardManager.clipboardHistory.isEmpty { - Section { - ForEach(clipboardManager.clipboardHistory) { item in - VStack(alignment: .leading, spacing: 4) { - HStack { - Image(systemName: item.type.icon) - .foregroundColor(.blue) - .frame(width: 16) - Text(item.type.displayName) - .font(.caption) - .foregroundColor(.secondary) - Spacer() - Text(timeAgoString(from: item.timestamp)) - .font(.caption2) - .foregroundColor(.secondary) - } - Text(item.preview) - .font(.system(.body, design: .monospaced)) - .lineLimit(2) - } - .padding(.vertical, 2) - } - } header: { - Text("Current History") - } - } - } - } - .formStyle(.grouped) - .navigationTitle("Clipboard") - .onAppear { - if enableClipboardManager && !clipboardManager.isMonitoring { - clipboardManager.startMonitoring() - } - } - } - - private func timeAgoString(from date: Date) -> String { - let interval = Date().timeIntervalSince(date) - - if interval < 60 { - return "Just now" - } else if interval < 3600 { - let minutes = Int(interval / 60) - return "\(minutes)m ago" - } else if interval < 86400 { - let hours = Int(interval / 3600) - return "\(hours)h ago" - } else { - let days = Int(interval / 86400) - return "\(days)d ago" - } - } -} - -#Preview { - HUD() -} \ No newline at end of file diff --git a/logic.md b/logic.md deleted file mode 100644 index 5c6977f..0000000 --- a/logic.md +++ /dev/null @@ -1,159 +0,0 @@ -# Dynamic Island Alignment Logic Analysis - -## Problem Statement -The Dynamic Island window sometimes gets misaligned (moves to the left) when switching between tabs, especially when going from stats tab (with 4+ graphs that make the window wider) back to other tabs. - -## Current Implementation Analysis - -### When Window is Created -1. `createDynamicIslandWindow()` creates window with `openNotchSize` (fixed size) -2. `positionWindow()` centers it using current window width -3. Window is perfectly centered initially - -### When User Switches to Stats Tab (4+ graphs) -1. `coordinator.$currentView` triggers `updateWindowSizeIfNeeded()` -2. `calculateRequiredNotchSize()` returns larger size (openNotchSize.width + extraWidth) -3. `updateWindowSizeIfNeeded()` resizes window and re-centers using new width -4. Window should stay centered with new larger size - -### When User Switches Away from Stats Tab -1. `coordinator.$currentView` triggers `updateWindowSizeIfNeeded()` again -2. `calculateRequiredNotchSize()` returns normal `openNotchSize` -3. `updateWindowSizeIfNeeded()` resizes window back and re-centers using normal width -4. **THIS IS WHERE THE PROBLEM OCCURS** - -## Root Cause Analysis - -### The Issue: Timing and State Mismatch - -The problem is likely that `coordinator.currentView` and the actual UI state are not perfectly synchronized. Here's what probably happens: - -1. User clicks another tab -2. `coordinator.$currentView` fires immediately -3. `updateWindowSizeIfNeeded()` calculates size as if we're NOT on stats tab -4. BUT the ContentView might still be showing stats content (transition not complete) -5. Window gets resized to small size while content is still large -6. This causes misalignment because content and window size don't match - -### Why It Sometimes Works -- When the UI transition is fast enough, everything stays in sync -- When there's any delay or animation, they get out of sync - -## Potential Solutions - -### Option 1: Delay the Window Resize -Add a small delay to ensure UI transition is complete before resizing window. - -### Option 2: Remove Window Resizing, Use Content Clipping -Let the window stay large and let the content handle its own sizing internally. - -### Option 3: Better State Synchronization -Make sure window resizing only happens when we're 100% sure the content has updated. - -### Option 4: Use ContentView's dynamicNotchSize Instead -The ContentView already has `dynamicNotchSize` logic. Maybe the window should use that instead of having its own calculation. - -## Recommended Approach - -**Option 2** seems safest: Remove dynamic window resizing entirely and let the content handle sizing. - -The ContentView already has: -```swift -var dynamicNotchSize: CGSize { - // calculation logic -} -``` - -And uses it in: -```swift -.frame(maxWidth: dynamicNotchSize.width, maxHeight: dynamicNotchSize.height) -``` - -This means the content already knows how to size itself. The window can stay at the maximum possible size, and the content will center itself within that space. - -## Test Plan - -1. Remove all dynamic window resizing logic -2. Set window to maximum possible size (openNotchSize.width + max extra width) -3. Let ContentView handle all dynamic sizing internally -4. Test alignment stays perfect - -This eliminates the timing issue completely because the window never changes size after initial creation. - -## CRITICAL DISCOVERY - Clipboard is NOT a Tab View - -### Major Finding -**The `.clipboard` case doesn't exist in `NotchViews` enum!** - -```swift -public enum NotchViews { - case home - case shelf - case timer - case stats -} -``` - -### What This Means -- All my protective logic for `coordinator.currentView == .clipboard` is **ineffective** -- Clipboard functionality works differently - likely as a popover or overlay, not a tab -- The hover collapse issue is NOT about tab transitions but about popover interactions - -### Investigation Results -1. **ContentView switch statement** only handles: `.home`, `.shelf`, `.timer`, `.stats` -2. **No clipboard tab content rendering** in the main view switch -3. **Clipboard must be implemented as popover/overlay** system - -### New Hypothesis -The clipboard "area" the user refers to is likely: -1. **ClipboardHistoryPopover** - a popover that opens over the Dynamic Island -2. **ClipboardManager integration** - clipboard content shown in existing tabs -3. **Overlay system** - clipboard UI overlaid on top of main content - -### Critical Issue Identified -My previous fix added protections for `.clipboard` view that **DOESN'T EXIST**, meaning: -- Protections are not working -- Hover collapse still happening during clipboard interactions -- Need to find actual clipboard interaction boundaries - -### Next Steps -1. **Find real clipboard implementation** - how is clipboard content actually shown? -2. **Identify clipboard hover boundaries** - what UI element represents "clipboard area"? -3. **Fix actual interaction conflicts** - coordinate between clipboard popover and main hover logic - -#### When This Caused Problems: - -1. **"Show on all displays" toggle**: Creates new window with default `openNotchSize`, losing dynamic sizing -2. **Screen changes**: `adjustWindowPosition()` creates new windows with wrong size -3. **App restarts/other events**: Any time a new window is created, it ignores current dynamic sizing needs - -#### The Wrong Code: -```swift -// OLD - ALWAYS used openNotchSize -let window = DynamicIslandWindow( - contentRect: NSRect(x: 0, y: 0, width: openNotchSize.width, height: openNotchSize.height), - ... -``` - -#### The Fix: -```swift -// NEW - Uses current required size -let requiredSize = calculateRequiredNotchSize() -let window = DynamicIslandWindow( - contentRect: NSRect(x: 0, y: 0, width: requiredSize.width, height: requiredSize.height), - ... -``` - -### Why This Fixes Everything: -βœ… **"Show on all displays" toggle**: New windows created with correct size -βœ… **Tab switching**: No more size mismatch when repositioning -βœ… **Screen changes**: New windows respect current graph settings -βœ… **Consistent behavior**: All window creation uses same sizing logic - -### The Real Issue Was: -- Windows created with wrong size, then positioned correctly = **misalignment** -- Content expecting larger window, but window is small = **positioning errors** -- Dynamic sizing only worked if you never triggered window recreation - -### Result: -Perfect alignment in ALL scenarios - no more window size/content size mismatches!