mirror of
https://github.com/FBukovina/Atoll.git
synced 2026-10-27 15:14:33 +00:00
added a separate tab view for clipboard and added the possibility to write notes
This commit is contained in:
parent
44c053a761
commit
4106e845ee
10 changed files with 688 additions and 6 deletions
|
|
@ -565,6 +565,10 @@ struct ContentView: View {
|
|||
NotchStatsView()
|
||||
case .colorPicker:
|
||||
NotchColorPickerView()
|
||||
case .notes:
|
||||
NotchNotesView()
|
||||
case .clipboard:
|
||||
NotchNotesView()
|
||||
}
|
||||
}
|
||||
.transition(.asymmetric(
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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" : {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
545
DynamicIsland/components/Notch/NotchNotesView.swift
Normal file
545
DynamicIsland/components/Notch/NotchNotesView.swift
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,6 +41,12 @@ struct TabSelectionView: View {
|
|||
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
|
||||
}
|
||||
var body: some View {
|
||||
|
|
|
|||
|
|
@ -30,6 +30,8 @@ public enum NotchViews {
|
|||
case timer
|
||||
case stats
|
||||
case colorPicker
|
||||
case notes
|
||||
case clipboard
|
||||
}
|
||||
|
||||
enum SettingsEnum {
|
||||
|
|
|
|||
|
|
@ -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, *) {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue