// // 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..