From 4106e845ee3986ec21b2d31c55102f8a12758eff Mon Sep 17 00:00:00 2001 From: delli Date: Sun, 14 Dec 2025 23:24:04 +0100 Subject: [PATCH] added a separate tab view for clipboard and added the possibility to write notes --- DynamicIsland/ContentView.swift | 4 + DynamicIsland/DynamicIslandApp.swift | 11 + DynamicIsland/Localizable.xcstrings | 49 +- .../Notch/DynamicIslandHeader.swift | 10 + .../components/Notch/NotchNotesView.swift | 545 ++++++++++++++++++ .../components/Settings/SettingsView.swift | 39 +- .../components/Tabs/TabSelectionView.swift | 6 + DynamicIsland/enums/generic.swift | 2 + DynamicIsland/models/Constants.swift | 24 + .../models/DynamicIslandViewModel.swift | 4 + 10 files changed, 688 insertions(+), 6 deletions(-) create mode 100644 DynamicIsland/components/Notch/NotchNotesView.swift diff --git a/DynamicIsland/ContentView.swift b/DynamicIsland/ContentView.swift index 60fd3ea..fb0bdcb 100644 --- a/DynamicIsland/ContentView.swift +++ b/DynamicIsland/ContentView.swift @@ -565,6 +565,10 @@ struct ContentView: View { NotchStatsView() case .colorPicker: NotchColorPickerView() + case .notes: + NotchNotesView() + case .clipboard: + NotchNotesView() } } .transition(.asymmetric( diff --git a/DynamicIsland/DynamicIslandApp.swift b/DynamicIsland/DynamicIslandApp.swift index 08c92c9..526be2c 100644 --- a/DynamicIsland/DynamicIslandApp.swift +++ b/DynamicIsland/DynamicIslandApp.swift @@ -578,6 +578,17 @@ class AppDelegate: NSObject, NSApplicationDelegate { } else { NotificationCenter.default.post(name: NSNotification.Name("ToggleClipboardPopover"), object: nil) } + case .separateTab: + if self.vm.notchState == .closed { + self.vm.open() + self.coordinator.currentView = .notes + } else { + if self.coordinator.currentView == .notes { + self.vm.close() + } else { + self.coordinator.currentView = .notes + } + } } } diff --git a/DynamicIsland/Localizable.xcstrings b/DynamicIsland/Localizable.xcstrings index 4d9a948..32ec5e0 100644 --- a/DynamicIsland/Localizable.xcstrings +++ b/DynamicIsland/Localizable.xcstrings @@ -5340,6 +5340,10 @@ } } }, + "Click + to create a note" : { + "comment" : "A description below the \"+\" button in the note list view, instructing the user to create a new note.", + "isCommentAutoGenerated" : true + }, "Click for details" : { "comment" : "A hint text displayed below the graph in the Stats Card, indicating that tapping it will reveal more details.", "isCommentAutoGenerated" : true, @@ -5909,6 +5913,10 @@ } } }, + "Copy text to see it here" : { + "comment" : "A description below the \"No copies yet\" text, instructing the user to copy text to see it in the clipboard.", + "isCommentAutoGenerated" : true + }, "Core %lld" : { "comment" : "A label showing the ID of a CPU core. The argument is the ID of the CPU core.", "isCommentAutoGenerated" : true, @@ -7102,6 +7110,10 @@ }, "Do Not Disturb" : { + }, + "Done" : { + "comment" : "A button label that indicates that the user has finished editing a note.", + "isCommentAutoGenerated" : true }, "Download" : { "localizations" : { @@ -8197,6 +8209,10 @@ }, "Enable timer live activity" : { + }, + "Enabling notes will add a quick access button to the Dynamic Island header." : { + "comment" : "A footer description for the \"Enable Notes\" toggle in the Notes settings view.", + "isCommentAutoGenerated" : true }, "Engine Utilization" : { "comment" : "A label describing the utilization of the GPU's engine components.", @@ -13675,6 +13691,10 @@ }, "No Colors Yet" : { + }, + "No copies yet" : { + "comment" : "A message displayed when the clipboard is empty, indicating that there are no copied items.", + "isCommentAutoGenerated" : true }, "No custom animation available" : { "localizations" : { @@ -14105,6 +14125,9 @@ "No Graphs Enabled" : { "comment" : "A label displayed when no graphs are enabled in the Stats tab.", "isCommentAutoGenerated" : true + }, + "No notes yet" : { + }, "No presets configured. Add a preset to make it appear in the timer popover." : { "comment" : "A message displayed when there are no timer presets configured, encouraging the user to add one.", @@ -14657,6 +14680,10 @@ "comment" : "A section header for adjusting the expanded notch width.", "isCommentAutoGenerated" : true }, + "Notes" : { + "comment" : "The title of the Notes settings page.", + "isCommentAutoGenerated" : true + }, "Notify before" : { "comment" : "A label describing the slider that allows the user to set a reminder lead time.", "isCommentAutoGenerated" : true @@ -14924,8 +14951,9 @@ "comment" : "A label indicating that the timer is running overtime.", "isCommentAutoGenerated" : true }, - "Panel mode shows clipboard in a floating window near the notch. Popover mode shows clipboard as a dropdown attached to the clipboard button." : { - + "Panel mode shows clipboard in a floating window near the notch." : { + "comment" : "A description of the \"Panel\" clipboard display mode.", + "isCommentAutoGenerated" : true }, "Panel mode shows color picker in a floating window. Popover mode shows color picker as a dropdown attached to the color picker button." : { @@ -15077,8 +15105,9 @@ } } }, - "Popover mode shows clipboard as a dropdown attached to the clipboard button. Panel mode shows clipboard in a floating window near the notch." : { - + "Popover mode shows clipboard as a dropdown attached to the clipboard button." : { + "comment" : "A description of the \"Popover mode\" in the Clipboard Settings view.", + "isCommentAutoGenerated" : true }, "Popover mode shows color picker as a dropdown attached to the color picker button. Panel mode shows color picker in a floating window." : { @@ -16598,6 +16627,10 @@ } } }, + "Separate Tab mode integrates Copied Items and Notes into a single view. If both are enabled, Notes appear on the right and Clipboard on the left." : { + "comment" : "A description of the \"Separate Tab\" clipboard display mode.", + "isCommentAutoGenerated" : true + }, "Session · ↓ %@ · ↑ %@" : { "comment" : "A row displaying the total downloaded and uploaded bytes for the network session.", "isCommentAutoGenerated" : true, @@ -18230,6 +18263,10 @@ } } }, + "Start typing..." : { + "comment" : "A placeholder text shown when a note's content is empty.", + "isCommentAutoGenerated" : true + }, "Starts a 5-minute demo timer to test the timer live activity feature. Only works when timer feature is enabled." : { }, @@ -18903,6 +18940,10 @@ "comment" : "A section header for the timer widget settings.", "isCommentAutoGenerated" : true }, + "Title" : { + "comment" : "A label for the title input field in the note editor.", + "isCommentAutoGenerated" : true + }, "Toggle Notch Open:" : { "localizations" : { "ar" : { diff --git a/DynamicIsland/components/Notch/DynamicIslandHeader.swift b/DynamicIsland/components/Notch/DynamicIslandHeader.swift index 88c1536..0bef420 100644 --- a/DynamicIsland/components/Notch/DynamicIslandHeader.swift +++ b/DynamicIsland/components/Notch/DynamicIslandHeader.swift @@ -76,6 +76,8 @@ struct DynamicIslandHeader: View { ClipboardPanelManager.shared.toggleClipboardPanel() case .popover: showClipboardPopover.toggle() + case .separateTab: + coordinator.currentView = .notes } }) { Capsule() @@ -207,6 +209,8 @@ struct DynamicIslandHeader: View { .transition(.opacity) } + + if Defaults[.showBatteryIndicator] { DynamicIslandBatteryView( batteryWidth: 30, @@ -238,6 +242,12 @@ struct DynamicIslandHeader: View { ClipboardPanelManager.shared.toggleClipboardPanel() case .popover: showClipboardPopover.toggle() + case .separateTab: + if coordinator.currentView == .notes { + coordinator.currentView = .home + } else { + coordinator.currentView = .notes + } } } } diff --git a/DynamicIsland/components/Notch/NotchNotesView.swift b/DynamicIsland/components/Notch/NotchNotesView.swift new file mode 100644 index 0000000..7247a9d --- /dev/null +++ b/DynamicIsland/components/Notch/NotchNotesView.swift @@ -0,0 +1,545 @@ +// +// NotchNotesView.swift +// DynamicIsland +// +// Created by Assistant +// + +import SwiftUI +import Defaults + +struct NotchNotesView: View { + @FocusState private var isFocused: Bool + @Default(.savedNotes) var savedNotes + @Default(.clipboardDisplayMode) var clipboardDisplayMode + @Default(.enableClipboardManager) var enableClipboardManager + + @State private var selectedNoteId: UUID? + @State private var isEditingNewNote = false + + // Editor State + @State private var editorTitle: String = "" + @State private var editorContent: String = "" + @State private var editorColorIndex: Int = 0 + @State private var editorNoteId: UUID? + + @Default(.enableNotes) var enableNotes + + var showSplitView: Bool { + return enableClipboardManager && clipboardDisplayMode == .separateTab + } + + var body: some View { + HStack(spacing: 0) { + if showSplitView { + NotchClipboardList() + .frame(maxWidth: .infinity) + + if enableNotes { + Divider() + .background(Color.white.opacity(0.15)) + } + } + + if enableNotes { + ZStack { + if isEditingNewNote || selectedNoteId != nil { + NoteEditorView( + title: $editorTitle, + content: $editorContent, + colorIndex: $editorColorIndex, + onSave: saveNote, + onCancel: cancelEdit, + isNew: isEditingNewNote + ) + .transition(.asymmetric(insertion: .move(edge: .trailing), removal: .move(edge: .trailing))) + } else { + NoteListView( + notes: savedNotes, + onSelect: selectNote, + onCreate: createNote, + onDelete: deleteNote, + onDeleteItem: deleteNoteItem, + onClearAll: clearAllNotes + ) + .transition(.asymmetric(insertion: .move(edge: .leading), removal: .move(edge: .leading))) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .clipped() // Prevent overflow during transition + .animation(.spring(response: 0.35, dampingFraction: 0.8), value: isEditingNewNote) + .animation(.spring(response: 0.35, dampingFraction: 0.8), value: selectedNoteId) + } + } + } + + // MARK: - Actions + + private func createNote() { + editorTitle = "" + editorContent = "" + editorColorIndex = 0 // Default Yellow + editorNoteId = UUID() + isEditingNewNote = true + } + + private func selectNote(_ note: NoteItem) { + editorTitle = note.title + editorContent = note.content + editorColorIndex = note.colorIndex + editorNoteId = note.id + selectedNoteId = note.id + isEditingNewNote = false + } + + private func saveNote() { + if let id = editorNoteId { + let newNote = NoteItem( + id: id, + title: editorTitle.isEmpty ? "Untitled Note" : editorTitle, + content: editorContent, + creationDate: Date(), + colorIndex: editorColorIndex + ) + + var notes = savedNotes + if let index = notes.firstIndex(where: { $0.id == id }) { + notes[index] = newNote + } else { + notes.insert(newNote, at: 0) + } + savedNotes = notes + } + + closeEditor() + } + + private func deleteNote(_ indexSet: IndexSet) { + var notes = savedNotes + notes.remove(atOffsets: indexSet) + savedNotes = notes + } + + private func deleteNoteItem(_ note: NoteItem) { + var notes = savedNotes + if let index = notes.firstIndex(where: { $0.id == note.id }) { + notes.remove(at: index) + savedNotes = notes + } + } + + private func clearAllNotes() { + savedNotes.removeAll() + } + + private func cancelEdit() { + closeEditor() + } + + private func closeEditor() { + isEditingNewNote = false + selectedNoteId = nil + // Tiny delay to clear state after animation + DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) { + editorTitle = "" + editorContent = "" + } + } +} + +// MARK: - Subviews + +struct NotchClipboardList: View { + @ObservedObject var clipboardManager = ClipboardManager.shared + @State private var hoveredItemId: UUID? + @State private var justCopiedId: UUID? + + var body: some View { + VStack(spacing: 0) { + // Header + HStack { + Text("Clipboard") + .font(.system(size: 18, weight: .semibold, design: .rounded)) + .foregroundStyle(.white) + + Spacer() + + if !clipboardManager.clipboardHistory.isEmpty { + Button(action: { clipboardManager.clearHistory() }) { + Image(systemName: "trash") + .font(.system(size: 14)) + .foregroundStyle(.white.opacity(0.6)) + .padding(6) + .background(Color.white.opacity(0.1)) + .clipShape(Circle()) + } + .buttonStyle(PlainButtonStyle()) + } + } + .padding(.horizontal, 20) + .padding(.top, 12) + .padding(.bottom, 12) + + if clipboardManager.clipboardHistory.isEmpty { + VStack(spacing: 8) { + Image(systemName: "doc.on.clipboard") + .font(.system(size: 32)) + .foregroundStyle(.secondary.opacity(0.3)) + Text("No copies yet") + .font(.headline) + .foregroundStyle(.secondary) + Text("Copy text to see it here") + .font(.caption) + .foregroundStyle(.secondary.opacity(0.7)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .offset(y: -20) + } else { + ScrollView { + LazyVStack(spacing: 4) { // Tighter spacing + ForEach(clipboardManager.clipboardHistory) { item in + NotchClipboardItemRow( + item: item, + isHovered: hoveredItemId == item.id, + justCopied: justCopiedId == item.id + ) + .contentShape(Rectangle()) + .onHover { isHovered in + hoveredItemId = isHovered ? item.id : nil + } + .onTapGesture { + clipboardManager.copyToClipboard(item) + withAnimation { + justCopiedId = item.id + } + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + if justCopiedId == item.id { + withAnimation { + justCopiedId = nil + } + } + } + } + } + } + .padding(.horizontal, 16) + .padding(.bottom, 20) + } + } + } + } +} + +struct NotchClipboardItemRow: View { + let item: ClipboardItem + let isHovered: Bool + let justCopied: Bool + + var body: some View { + HStack(spacing: 12) { + Image(systemName: justCopied ? "checkmark.circle.fill" : item.type.icon) + .font(.system(size: 14)) + .foregroundColor(justCopied ? .green : .blue) + .frame(width: 20) + + VStack(alignment: .leading, spacing: 4) { + Text(item.preview) + .font(.system(size: 13)) + .foregroundStyle(.white) + .lineLimit(1) + + HStack { + Text(item.type.displayName) + .font(.system(size: 10)) + .foregroundStyle(.secondary) + Spacer() + Text(timeAgoString(from: item.timestamp)) + .font(.system(size: 10)) + .foregroundStyle(.secondary) + } + } + } + .padding(10) + .background(Color(nsColor: .controlBackgroundColor).opacity(isHovered ? 0.3 : 0.15)) + .clipShape(RoundedRectangle(cornerRadius: 8)) + } + + private func timeAgoString(from date: Date) -> String { + let interval = Date().timeIntervalSince(date) + if interval < 60 { return "Just now" } + if interval < 3600 { return "\(Int(interval/60))m" } + return "\(Int(interval/3600))h" + } +} + + +struct NoteListView: View { + let notes: [NoteItem] + let onSelect: (NoteItem) -> Void + let onCreate: () -> Void + let onDelete: (IndexSet) -> Void // Keep for swipe support + let onDeleteItem: (NoteItem) -> Void // New for button support + let onClearAll: () -> Void + + var body: some View { + VStack(spacing: 0) { + // Header + HStack { + Text("Notes") + .font(.system(size: 18, weight: .semibold, design: .rounded)) // Slightly smaller, cleaner + .foregroundStyle(.white) + + Spacer() + + if !notes.isEmpty { + Button(action: onClearAll) { + Image(systemName: "trash") + .font(.system(size: 14)) + .foregroundStyle(.white.opacity(0.6)) + .padding(6) + .background(Color.white.opacity(0.1)) + .clipShape(Circle()) + } + .buttonStyle(PlainButtonStyle()) + } + + Button(action: onCreate) { + Image(systemName: "plus") + .font(.system(size: 14, weight: .semibold)) // Reduced size to match trash + .foregroundStyle(.white) + .padding(6) + .background(Color.white.opacity(0.1)) + .clipShape(Circle()) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(.horizontal, 20) + .padding(.top, 12) // Reduced padding to move header higher + .padding(.bottom, 12) + + if notes.isEmpty { + VStack(spacing: 8) { + Image(systemName: "note.text") + .font(.system(size: 32)) + .foregroundStyle(.secondary.opacity(0.3)) + Text("No notes yet") + .font(.headline) + .foregroundStyle(.secondary) + Text("Click + to create a note") + .font(.caption) + .foregroundStyle(.secondary.opacity(0.7)) + } + .frame(maxWidth: .infinity, maxHeight: .infinity) + .offset(y: -20) // Visually center slightly upwards + } else { + ScrollView { + LazyVStack(spacing: 4) { // Matched tighter spacing + ForEach(notes) { note in + NoteRow(note: note, onDelete: { onDeleteItem(note) }) + .contentShape(Rectangle()) + .onTapGesture { + onSelect(note) + } + } + } + .padding(.horizontal, 16) // Matched padding + .padding(.bottom, 20) + } + } + } + } +} + +struct NoteRow: View { + let note: NoteItem + let onDelete: () -> Void + @State private var isHovered = false + @State private var isCopied = false + + var body: some View { + HStack(spacing: 12) { // Matched spacing + // Color Indicator + RoundedRectangle(cornerRadius: 3) + .fill(note.color) + .frame(width: 4) + .padding(.vertical, 2) + + VStack(alignment: .leading, spacing: 4) { // Matched spacing + Text(note.title) + .font(.system(size: 13, weight: .medium, design: .rounded)) // Matched size + .foregroundStyle(.white) + .lineLimit(1) + + Text(note.content.isEmpty ? "No content" : note.content) + .font(.system(size: 11)) // Slightly smaller for secondary text + .foregroundStyle(.secondary) + .lineLimit(1) // Single line description like clipboard + .multilineTextAlignment(.leading) + } + + Spacer() + + if isHovered { + HStack(spacing: 4) { + Button(action: { + let pasteboard = NSPasteboard.general + pasteboard.clearContents() + pasteboard.setString(note.content, forType: .string) + + withAnimation { + isCopied = true + } + + DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { + withAnimation { + isCopied = false + } + } + }) { + Image(systemName: isCopied ? "checkmark" : "doc.on.doc") + .font(.system(size: 12)) // Matched icon size + .foregroundStyle(isCopied ? .green : .white.opacity(0.8)) + .padding(6) + } + .buttonStyle(PlainButtonStyle()) + + Button(action: onDelete) { + Image(systemName: "trash") + .font(.system(size: 12)) // Matched icon size + .foregroundStyle(.red.opacity(0.8)) + .padding(6) + } + .buttonStyle(PlainButtonStyle()) + } + .transition(.opacity.animation(.easeInOut(duration: 0.2))) + } else { + Text(note.creationDate.formatted(.dateTime.day().month())) + .font(.system(size: 10)) // Matched timestamp size + .foregroundStyle(.secondary.opacity(0.5)) + } + } + .padding(10) // Matched padding + .background(Color(nsColor: .controlBackgroundColor).opacity(isHovered ? 0.5 : 0.3)) + .clipShape(RoundedRectangle(cornerRadius: 8)) // Matched corner radius + .onHover { hovering in + withAnimation(.easeInOut(duration: 0.2)) { + isHovered = hovering + } + } + } +} + +struct NoteEditorView: View { + @Binding var title: String + @Binding var content: String + @Binding var colorIndex: Int + let onSave: () -> Void + let cancelAction: () -> Void + let isNew: Bool + + init(title: Binding, content: Binding, colorIndex: Binding, onSave: @escaping () -> Void, onCancel: @escaping () -> Void, isNew: Bool) { + self._title = title + self._content = content + self._colorIndex = colorIndex + self.onSave = onSave + self.cancelAction = onCancel + self.isNew = isNew + } + + @FocusState private var isContentFocused: Bool + + var body: some View { + VStack(spacing: 0) { + // Toolbar + HStack { + Button(action: cancelAction) { + HStack(spacing: 4) { + Image(systemName: "chevron.left") + Text("Notes") + } + .font(.system(size: 15, weight: .medium, design: .rounded)) + .foregroundStyle(.white) + } + .buttonStyle(PlainButtonStyle()) + + Spacer() + + Button(action: onSave) { + Text("Done") + .font(.system(size: 15, weight: .bold, design: .rounded)) + .foregroundStyle(NoteItem.colors[colorIndex]) + } + .buttonStyle(PlainButtonStyle()) + } + .padding(16) + + // Title & Color Picker Row + HStack(alignment: .center, spacing: 12) { + TextField("Title", text: $title) + .font(.system(size: 17, weight: .bold, design: .rounded)) + .textFieldStyle(.plain) + + Spacer() // Push colors to the right + + // Color Picker + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(0.. String { + SettingsTab.notes.highlightID(for: title) + } + + var body: some View { + Form { + Section { + Defaults.Toggle("Enable Notes", key: .enableNotes) + } header: { + Text("General") + } footer: { + Text("Enabling notes will add a quick access button to the Dynamic Island header.") + .font(.caption) + .foregroundStyle(.secondary) + } + } + .navigationTitle("Notes") + } +} diff --git a/DynamicIsland/components/Tabs/TabSelectionView.swift b/DynamicIsland/components/Tabs/TabSelectionView.swift index 9d552a9..7916761 100644 --- a/DynamicIsland/components/Tabs/TabSelectionView.swift +++ b/DynamicIsland/components/Tabs/TabSelectionView.swift @@ -40,6 +40,12 @@ struct TabSelectionView: View { if Defaults[.enableStatsFeature] { tabsArray.append(TabModel(label: "Stats", icon: "chart.xyaxis.line", view: .stats)) } + + if Defaults[.enableNotes] || (Defaults[.enableClipboardManager] && Defaults[.clipboardDisplayMode] == .separateTab) { + let label = Defaults[.enableNotes] ? "Notes" : "Clipboard" + let icon = Defaults[.enableNotes] ? "note.text" : "doc.on.clipboard" + tabsArray.append(TabModel(label: label, icon: icon, view: .notes)) + } return tabsArray } diff --git a/DynamicIsland/enums/generic.swift b/DynamicIsland/enums/generic.swift index cbcaa27..14080ea 100644 --- a/DynamicIsland/enums/generic.swift +++ b/DynamicIsland/enums/generic.swift @@ -30,6 +30,8 @@ public enum NotchViews { case timer case stats case colorPicker + case notes + case clipboard } enum SettingsEnum { diff --git a/DynamicIsland/models/Constants.swift b/DynamicIsland/models/Constants.swift index 4d6dc3d..30f7e85 100644 --- a/DynamicIsland/models/Constants.swift +++ b/DynamicIsland/models/Constants.swift @@ -105,11 +105,13 @@ enum CalendarSelectionState: Codable, Defaults.Serializable { enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable { case popover = "popover" // Traditional popover attached to button case panel = "panel" // Floating panel near notch + case separateTab = "separateTab" // Separate tab in Dynamic Island var displayName: String { switch self { case .popover: return "Popover" case .panel: return "Panel" + case .separateTab: return "Separate Tab" } } @@ -117,6 +119,7 @@ enum ClipboardDisplayMode: String, CaseIterable, Codable, Defaults.Serializable switch self { case .popover: return "Shows clipboard as a dropdown attached to the clipboard button" case .panel: return "Shows clipboard in a floating panel near the notch" + case .separateTab: return "Shows copied items in a separate tab within the Dynamic Island (merges with Notes if enabled)" } } } @@ -377,6 +380,23 @@ struct AIModel: Codable, Identifiable, Defaults.Serializable { } } +struct NoteItem: Codable, Identifiable, Defaults.Serializable, Hashable { + var id: UUID = UUID() + var title: String + var content: String + var creationDate: Date + var colorIndex: Int // 0: Yellow, 1: Blue, 2: Red, 3: Green + + static let colors: [Color] = [.yellow, .blue, .red, .green, .purple, .orange] + + var color: Color { + if colorIndex >= 0 && colorIndex < NoteItem.colors.count { + return NoteItem.colors[colorIndex] + } + return .yellow + } +} + extension Defaults.Keys { // MARK: General static let menubarIcon = Key("menubarIcon", default: true) @@ -639,6 +659,10 @@ extension Defaults.Keys { // MARK: Lyrics Feature static let enableLyrics = Key("enableLyrics", default: true) + // MARK: Notes Feature + static let enableNotes = Key("enableNotes", default: false) + static let savedNotes = Key<[NoteItem]>("savedNotes", default: []) + // 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 c55a852..1193bfa 100644 --- a/DynamicIsland/models/DynamicIslandViewModel.swift +++ b/DynamicIsland/models/DynamicIslandViewModel.swift @@ -269,6 +269,10 @@ class DynamicIslandViewModel: NSObject, ObservableObject { private func calculateDynamicNotchSize() -> CGSize { // Use minimalistic size if minimalistic UI is enabled let baseSize = Defaults[.enableMinimalisticUI] ? minimalisticOpenNotchSize : openNotchSize + if DynamicIslandViewCoordinator.shared.currentView == .notes { + return CGSize(width: baseSize.width, height: 360) + } + return statsAdjustedNotchSize( from: baseSize, isStatsTabActive: DynamicIslandViewCoordinator.shared.currentView == .stats,