added a separate tab view for clipboard and added the possibility to write notes

This commit is contained in:
delli 2025-12-14 23:24:04 +01:00
commit 4106e845ee
10 changed files with 688 additions and 6 deletions

View file

@ -565,6 +565,10 @@ struct ContentView: View {
NotchStatsView()
case .colorPicker:
NotchColorPickerView()
case .notes:
NotchNotesView()
case .clipboard:
NotchNotesView()
}
}
.transition(.asymmetric(

View file

@ -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
}
}
}
}

View file

@ -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" : {

View file

@ -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
}
}
}
}

View file

@ -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<String>, content: Binding<String>, colorIndex: Binding<Int>, 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..<NoteItem.colors.count, id: \.self) { index in
Circle()
.fill(NoteItem.colors[index])
.frame(width: 16, height: 16)
.overlay(
Circle()
.stroke(Color.white, lineWidth: colorIndex == index ? 2 : 0)
.padding(-2)
)
.onTapGesture {
withAnimation {
colorIndex = index
}
}
}
}
.padding(.vertical, 4) // Prevent vertical selection ring clipping
.padding(.horizontal, 4) // Prevent horizontal selection ring clipping
}
.frame(maxWidth: 160) // Limit width so title gets space
}
.padding(.leading, 16)
.padding(.trailing, 4) // Reduced trailing padding to move circles right
.padding(.bottom, 8)
Divider()
.background(Color.white.opacity(0.1))
// Content Input
ZStack(alignment: .topLeading) {
if content.isEmpty { // Placeholder
Text("Start typing...")
.font(.system(size: 13, design: .rounded)) // Reduced from 14
.foregroundStyle(.secondary.opacity(0.5))
.padding(.top, 10)
.padding(.leading, 12)
.allowsHitTesting(false)
}
TextEditor(text: $content)
.font(.system(size: 13, design: .rounded)) // Reduced from 14
.foregroundStyle(.white)
.lineSpacing(4)
.scrollContentBackground(.hidden)
.padding(8)
.focused($isContentFocused)
.frame(maxHeight: .infinity)
.background(Color.white.opacity(0.05))
}
}
.frame(maxWidth: .infinity, maxHeight: .infinity) // Ensure VStack takes full space
.background(Color.black) // Ensure solid background
.onAppear {
if isNew {
isContentFocused = true
}
}
}
}

View file

@ -35,6 +35,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case downloads
case shelf
case shortcuts
case notes
case about
var id: String { rawValue }
@ -58,6 +59,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .downloads: return "Downloads"
case .shelf: return "Shelf"
case .shortcuts: return "Shortcuts"
case .notes: return "Notes"
case .about: return "About"
}
}
@ -81,6 +83,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .downloads: return "square.and.arrow.down"
case .shelf: return "books.vertical"
case .shortcuts: return "keyboard"
case .notes: return "note.text"
case .about: return "info.circle"
}
}
@ -104,6 +107,7 @@ private enum SettingsTab: String, CaseIterable, Identifiable {
case .downloads: return .gray
case .shelf: return .brown
case .shortcuts: return .orange
case .notes: return .yellow
case .about: return .secondary
}
}
@ -381,6 +385,7 @@ struct SettingsView: View {
.osd,
.battery,
.stats,
.notes,
.clipboard,
.screenAssistant,
.colorPicker,
@ -812,6 +817,10 @@ struct SettingsView: View {
SettingsForm(tab: .shortcuts) {
Shortcuts()
}
case .notes:
SettingsForm(tab: .notes) {
NotesSettingsView()
}
case .about:
if let controller = updaterController {
SettingsForm(tab: .about) {
@ -4081,9 +4090,11 @@ struct ClipboardSettings: View {
} footer: {
switch clipboardDisplayMode {
case .popover:
Text("Popover mode shows clipboard as a dropdown attached to the clipboard button. Panel mode shows clipboard in a floating window near the notch.")
Text("Popover mode shows clipboard as a dropdown attached to the clipboard button.")
case .panel:
Text("Panel mode shows clipboard in a floating window near the notch. Popover mode shows clipboard as a dropdown attached to the clipboard button.")
Text("Panel mode shows clipboard in a floating window near the notch.")
case .separateTab:
Text("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.")
}
}
@ -4690,3 +4701,27 @@ struct SettingsPermissionCallout: View {
#Preview {
HUD()
}
struct NotesSettingsView: View {
@EnvironmentObject var vm: DynamicIslandViewModel
@ObservedObject var coordinator = DynamicIslandViewCoordinator.shared
private func highlightID(_ title: String) -> 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")
}
}

View file

@ -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
}

View file

@ -30,6 +30,8 @@ public enum NotchViews {
case timer
case stats
case colorPicker
case notes
case clipboard
}
enum SettingsEnum {

View file

@ -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<Bool>("menubarIcon", default: true)
@ -639,6 +659,10 @@ extension Defaults.Keys {
// MARK: Lyrics Feature
static let enableLyrics = Key<Bool>("enableLyrics", default: true)
// MARK: Notes Feature
static let enableNotes = Key<Bool>("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, *) {

View file

@ -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,