feat: add note context menu with color and metadata controls
- Added PATCH endpoint for updating note frontmatter with YAML parsing and merging - Added DELETE endpoint to move notes to .trash directory with timestamped filenames - Implemented note context menu with actions like duplicate, share, favorite, and delete - Added color picker to context menu with gradient background visualization - Extended NoteFrontmatter type with readOnly and color properties - Added YAML frontmatter parser with support
This commit is contained in:
@@ -6,11 +6,13 @@ import { ScrollableOverlayDirective } from '../../shared/overlay-scrollbar/scrol
|
||||
import { TagFilterStore } from '../../core/stores/tag-filter.store';
|
||||
import { NotesListStateService, SortBy, ViewMode } from '../../services/notes-list-state.service';
|
||||
import { NoteCreationService } from '../../services/note-creation.service';
|
||||
import { NoteContextMenuComponent } from '../../../components/note-context-menu/note-context-menu.component';
|
||||
import { NoteContextMenuService } from '../../services/note-context-menu.service';
|
||||
|
||||
@Component({
|
||||
selector: 'app-notes-list',
|
||||
standalone: true,
|
||||
imports: [CommonModule, ScrollableOverlayDirective],
|
||||
imports: [CommonModule, ScrollableOverlayDirective, NoteContextMenuComponent],
|
||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||
template: `
|
||||
<div class="h-full flex flex-col">
|
||||
@@ -132,7 +134,10 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
||||
class="note-row cursor-pointer"
|
||||
[class.active]="selectedId() === n.id"
|
||||
[ngClass]="getListItemClasses()"
|
||||
(click)="openNote.emit(n.id)">
|
||||
[ngStyle]="getNoteGradientStyle(n)"
|
||||
[attr.data-note-path]="n.filePath"
|
||||
(click)="openNote.emit(n.id)"
|
||||
(contextmenu)="openContextMenu($event, n)">
|
||||
<!-- Compact View -->
|
||||
<div *ngIf="state.viewMode() === 'compact'" class="note-inner">
|
||||
<div class="title text-xs truncate">{{ n.title }}</div>
|
||||
@@ -157,6 +162,17 @@ import { NoteCreationService } from '../../services/note-creation.service';
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Note Context Menu -->
|
||||
<app-note-context-menu
|
||||
[x]="contextMenuService.x()"
|
||||
[y]="contextMenuService.y()"
|
||||
[visible]="contextMenuService.visible()"
|
||||
[note]="contextMenuService.targetNote()"
|
||||
(action)="onContextMenuAction($event)"
|
||||
(color)="onContextMenuColor($event)"
|
||||
(closed)="contextMenuService.close()">
|
||||
</app-note-context-menu>
|
||||
`,
|
||||
styles: [`
|
||||
:host {
|
||||
@@ -349,6 +365,7 @@ export class NotesListComponent {
|
||||
private store = inject(TagFilterStore);
|
||||
readonly state = inject(NotesListStateService);
|
||||
private noteCreationService = inject(NoteCreationService);
|
||||
readonly contextMenuService = inject(NoteContextMenuService);
|
||||
|
||||
private q = signal('');
|
||||
activeTag = signal<string | null>(null);
|
||||
@@ -543,4 +560,71 @@ export class NotesListComponent {
|
||||
this.state.setRequestStats(false, 0);
|
||||
});
|
||||
}
|
||||
|
||||
// Context menu methods
|
||||
openContextMenu(event: MouseEvent, note: Note) {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
this.contextMenuService.openForNote(note, { x: event.clientX, y: event.clientY });
|
||||
}
|
||||
|
||||
async onContextMenuAction(action: string) {
|
||||
const note = this.contextMenuService.targetNote();
|
||||
if (!note) return;
|
||||
|
||||
switch (action) {
|
||||
case 'duplicate':
|
||||
await this.contextMenuService.duplicateNote(note);
|
||||
break;
|
||||
case 'share':
|
||||
await this.contextMenuService.shareNote(note);
|
||||
break;
|
||||
case 'fullscreen':
|
||||
this.contextMenuService.openFullScreen(note);
|
||||
break;
|
||||
case 'copy-link':
|
||||
await this.contextMenuService.copyInternalLink(note);
|
||||
break;
|
||||
case 'favorite':
|
||||
await this.contextMenuService.toggleFavorite(note);
|
||||
break;
|
||||
case 'info':
|
||||
this.contextMenuService.showPageInfo(note);
|
||||
break;
|
||||
case 'readonly':
|
||||
await this.contextMenuService.toggleReadOnly(note);
|
||||
break;
|
||||
case 'delete':
|
||||
await this.contextMenuService.deleteNote(note);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async onContextMenuColor(color: string) {
|
||||
const note = this.contextMenuService.targetNote();
|
||||
if (!note) return;
|
||||
|
||||
await this.contextMenuService.changeNoteColor(note, color);
|
||||
}
|
||||
|
||||
// Get gradient style for note based on color
|
||||
getNoteGradientStyle(note: Note): Record<string, string> | null {
|
||||
const color = note.frontmatter?.color;
|
||||
if (!color) return null;
|
||||
|
||||
// Convert hex to rgba with transparency for gradient
|
||||
const hexMatch = /^#([0-9a-fA-F]{6})$/.exec(color);
|
||||
let gradientColor = color;
|
||||
if (hexMatch) {
|
||||
const hex = hexMatch[1];
|
||||
const r = parseInt(hex.slice(0,2), 16);
|
||||
const g = parseInt(hex.slice(2,4), 16);
|
||||
const b = parseInt(hex.slice(4,6), 16);
|
||||
gradientColor = `rgba(${r}, ${g}, ${b}, 0.14)`;
|
||||
}
|
||||
|
||||
return {
|
||||
backgroundImage: `linear-gradient(to left, ${gradientColor} 0%, transparent 65%)`
|
||||
} as Record<string, string>;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user