feat: add Obsidian bookmarks integration with toggle UI
- Integrated BookmarksService to display and filter bookmarked notes in quick links and paginated list - Added bookmark toggle button in note viewer with visual feedback (filled/outline icon states) - Implemented accordion behavior in mobile sidebar where opening one section closes others - Updated responsive breakpoints to treat tablet as mobile (desktop layout starts at 1024px)
This commit is contained in:
parent
59d8a9f83a
commit
7331077ffa
@ -9,6 +9,7 @@ import { TagFilterStore } from '../../core/stores/tag-filter.store';
|
|||||||
import { NotesListStateService, SortBy, ViewMode } from '../../services/notes-list-state.service';
|
import { NotesListStateService, SortBy, ViewMode } from '../../services/notes-list-state.service';
|
||||||
import { FilterService } from '../../services/filter.service';
|
import { FilterService } from '../../services/filter.service';
|
||||||
import { NoteContextMenuService } from '../../services/note-context-menu.service';
|
import { NoteContextMenuService } from '../../services/note-context-menu.service';
|
||||||
|
import { BookmarksService } from '../../services/bookmarks.service';
|
||||||
import { EditorStateService } from '../../../services/editor-state.service';
|
import { EditorStateService } from '../../../services/editor-state.service';
|
||||||
import { NoteContextMenuComponent } from '../../../components/note-context-menu/note-context-menu.component';
|
import { NoteContextMenuComponent } from '../../../components/note-context-menu/note-context-menu.component';
|
||||||
import { KeyboardShortcutsService } from '../../services/keyboard-shortcuts.service';
|
import { KeyboardShortcutsService } from '../../services/keyboard-shortcuts.service';
|
||||||
@ -346,6 +347,7 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
readonly contextMenu = inject(NoteContextMenuService);
|
readonly contextMenu = inject(NoteContextMenuService);
|
||||||
private editorState = inject(EditorStateService);
|
private editorState = inject(EditorStateService);
|
||||||
private keyboard = inject(KeyboardShortcutsService);
|
private keyboard = inject(KeyboardShortcutsService);
|
||||||
|
private bookmarksService = inject(BookmarksService);
|
||||||
private destroy$ = new Subject<void>();
|
private destroy$ = new Subject<void>();
|
||||||
private preservedOffset: number | null = null;
|
private preservedOffset: number | null = null;
|
||||||
private useUnifiedSync = true;
|
private useUnifiedSync = true;
|
||||||
@ -367,7 +369,7 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
folderFilter = input<string | null>(null);
|
folderFilter = input<string | null>(null);
|
||||||
query = input<string>('');
|
query = input<string>('');
|
||||||
tagFilter = input<string | null>(null);
|
tagFilter = input<string | null>(null);
|
||||||
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null>(null);
|
quickLinkFilter = input<'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | 'bookmarks' | null>(null);
|
||||||
selectedId = input<string | null>(null);
|
selectedId = input<string | null>(null);
|
||||||
kindFilter = input<'image' | 'video' | 'pdf' | 'markdown' | 'excalidraw' | 'code' | 'all' | null>(null);
|
kindFilter = input<'image' | 'video' | 'pdf' | 'markdown' | 'excalidraw' | 'code' | 'all' | null>(null);
|
||||||
|
|
||||||
@ -561,6 +563,11 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
if (quickKey2) {
|
if (quickKey2) {
|
||||||
items = items.filter(n => {
|
items = items.filter(n => {
|
||||||
const full = byId.get(n.id);
|
const full = byId.get(n.id);
|
||||||
|
// Special handling for bookmarks: check BookmarksService
|
||||||
|
if (quickKey2 === 'bookmarks') {
|
||||||
|
return this.bookmarksService.isBookmarked(n.filePath) && this.matchesKind(n.filePath, 'markdown');
|
||||||
|
}
|
||||||
|
// Standard quick links: check frontmatter
|
||||||
const fm = full?.frontmatter || {};
|
const fm = full?.frontmatter || {};
|
||||||
return fm[quickKey2] === true && this.matchesKind(n.filePath, 'markdown');
|
return fm[quickKey2] === true && this.matchesKind(n.filePath, 'markdown');
|
||||||
});
|
});
|
||||||
@ -927,7 +934,8 @@ export class PaginatedNotesListComponent implements OnInit, OnDestroy {
|
|||||||
'template': { icon: '📑', name: 'Template' },
|
'template': { icon: '📑', name: 'Template' },
|
||||||
'task': { icon: '🗒️', name: 'Task' },
|
'task': { icon: '🗒️', name: 'Task' },
|
||||||
'private': { icon: '🔒', name: 'Private' },
|
'private': { icon: '🔒', name: 'Private' },
|
||||||
'archive': { icon: '🗃️', name: 'Archive' }
|
'archive': { icon: '🗃️', name: 'Archive' },
|
||||||
|
'bookmarks': { icon: '🔖', name: 'Obsidian Bookmarks' }
|
||||||
};
|
};
|
||||||
return displays[quickLink] || null;
|
return displays[quickLink] || null;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import { Component, EventEmitter, Output, inject } from '@angular/core';
|
import { Component, EventEmitter, Output, inject } from '@angular/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { VaultService } from '../../../services/vault.service';
|
import { VaultService } from '../../../services/vault.service';
|
||||||
|
import { BookmarksService } from '../../services/bookmarks.service';
|
||||||
import { BadgeCountComponent } from '../../shared/ui/badge-count.component';
|
import { BadgeCountComponent } from '../../shared/ui/badge-count.component';
|
||||||
|
|
||||||
interface QuickLinkCountsUi {
|
interface QuickLinkCountsUi {
|
||||||
@ -27,6 +28,12 @@ interface QuickLinkCountsUi {
|
|||||||
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
|
<app-badge-count class="ml-auto" [count]="counts().all" color="slate"></app-badge-count>
|
||||||
</button>
|
</button>
|
||||||
</li>
|
</li>
|
||||||
|
<li>
|
||||||
|
<button (click)="select('bookmarks')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||||
|
<span class="flex items-center gap-2"><span>🔖</span> <span>Obsidian Bookmarks</span></span>
|
||||||
|
<app-badge-count class="ml-auto" [count]="bookmarks.count()" color="blue"></app-badge-count>
|
||||||
|
</button>
|
||||||
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
<button (click)="select('favorites')" class="flex items-center gap-2 px-3 py-2 rounded-lg cursor-pointer hover:bg-slate-500/10 dark:hover:bg-surface2/10 transition w-full text-left">
|
||||||
<span class="flex items-center gap-2"><span>❤️</span> <span>Favoris</span></span>
|
<span class="flex items-center gap-2"><span>❤️</span> <span>Favoris</span></span>
|
||||||
@ -75,6 +82,7 @@ interface QuickLinkCountsUi {
|
|||||||
})
|
})
|
||||||
export class QuickLinksComponent {
|
export class QuickLinksComponent {
|
||||||
private vault = inject(VaultService);
|
private vault = inject(VaultService);
|
||||||
|
readonly bookmarks = inject(BookmarksService);
|
||||||
@Output() quickLinkSelected = new EventEmitter<string>();
|
@Output() quickLinkSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
counts = () => this.vault.counts() as QuickLinkCountsUi;
|
counts = () => this.vault.counts() as QuickLinkCountsUi;
|
||||||
|
|||||||
@ -36,7 +36,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- Section Tests (dev-only) -->
|
<!-- Section Tests (dev-only) -->
|
||||||
<section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
|
<section *ngIf="env.features.showTestSection" class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.tests = !open.tests">
|
(click)="toggleSection('tests')">
|
||||||
<span class="flex items-center gap-2">🧪 <span>Section Tests</span></span>
|
<span class="flex items-center gap-2">🧪 <span>Section Tests</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tests">{{ open.tests ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -62,7 +62,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- Quick Links accordion -->
|
<!-- Quick Links accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.quick = !open.quick">
|
(click)="toggleSection('quick')">
|
||||||
<span class="flex items-center gap-2">⚡ <span>Quick Links</span></span>
|
<span class="flex items-center gap-2">⚡ <span>Quick Links</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.quick">{{ open.quick ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -74,7 +74,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- Folders accordion -->
|
<!-- Folders accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.folders = !open.folders">
|
(click)="toggleSection('folders')">
|
||||||
<span class="flex items-center gap-2">📁 <span>Folders</span></span>
|
<span class="flex items-center gap-2">📁 <span>Folders</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.folders">{{ open.folders ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -86,7 +86,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- Tags accordion -->
|
<!-- Tags accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.tags = !open.tags">
|
(click)="toggleSection('tags')">
|
||||||
<span class="flex items-center gap-2">🏷️ <span>Tags</span></span>
|
<span class="flex items-center gap-2">🏷️ <span>Tags</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.tags">{{ open.tags ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -106,7 +106,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- AI Tools accordion -->
|
<!-- AI Tools accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.ai = !open.ai">
|
(click)="toggleSection('ai')">
|
||||||
<span class="flex items-center gap-2">🤖 <span>AI Tools</span></span>
|
<span class="flex items-center gap-2">🤖 <span>AI Tools</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.ai">{{ open.ai ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.ai">{{ open.ai ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -131,7 +131,7 @@ import { AIToolsService, type AIToolItem } from '../../services/ai-tools.service
|
|||||||
<!-- Trash accordion -->
|
<!-- Trash accordion -->
|
||||||
<section class="border-b border-border dark:border-gray-800">
|
<section class="border-b border-border dark:border-gray-800">
|
||||||
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
<button class="w-full flex items-center justify-between px-4 py-3 text-sm font-semibold hover:bg-surface1 dark:hover:bg-card active:bg-surface2 dark:active:bg-gray-700 transition-colors"
|
||||||
(click)="open.trash = !open.trash; onFolder('.trash')">
|
(click)="toggleSection('trash'); onFolder('.trash')">
|
||||||
<span class="flex items-center gap-2">🗑️ <span>Trash</span></span>
|
<span class="flex items-center gap-2">🗑️ <span>Trash</span></span>
|
||||||
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
|
<span class="text-xs text-muted transition-transform duration-200" [class.rotate-90]="!open.trash">{{ open.trash ? '▾' : '▸' }}</span>
|
||||||
</button>
|
</button>
|
||||||
@ -201,7 +201,7 @@ export class AppSidebarDrawerComponent {
|
|||||||
@Output() aboutSelected = new EventEmitter<void>();
|
@Output() aboutSelected = new EventEmitter<void>();
|
||||||
@Output() aiToolSelected = new EventEmitter<string>();
|
@Output() aiToolSelected = new EventEmitter<string>();
|
||||||
|
|
||||||
open = { quick: true, folders: true, tags: false, ai: false, trash: false, tests: true };
|
open = { quick: true, folders: false, tags: false, ai: false, trash: false, tests: false };
|
||||||
|
|
||||||
onSelect(id: string) {
|
onSelect(id: string) {
|
||||||
this.noteSelected.emit(id);
|
this.noteSelected.emit(id);
|
||||||
@ -259,4 +259,13 @@ export class AppSidebarDrawerComponent {
|
|||||||
this.aiToolSelected.emit(tool.id);
|
this.aiToolSelected.emit(tool.id);
|
||||||
this.mobileNav.sidebarOpen.set(false);
|
this.mobileNav.sidebarOpen.set(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle section: open requested section, close all others
|
||||||
|
* Mimics desktop sidebar accordion behavior
|
||||||
|
*/
|
||||||
|
toggleSection(which: 'quick' | 'folders' | 'tags' | 'ai' | 'trash' | 'tests'): void {
|
||||||
|
this.open = { quick: false, folders: false, tags: false, ai: false, trash: false, tests: false };
|
||||||
|
(this.open as any)[which] = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import { TestExcalidrawPageComponent } from '../../features/tests/test-excalidra
|
|||||||
import { ParametersPage } from '../../features/parameters/parameters.page';
|
import { ParametersPage } from '../../features/parameters/parameters.page';
|
||||||
import { AboutPanelComponent } from '../../features/about/about-panel.component';
|
import { AboutPanelComponent } from '../../features/about/about-panel.component';
|
||||||
import { UrlStateService } from '../../services/url-state.service';
|
import { UrlStateService } from '../../services/url-state.service';
|
||||||
|
import { BookmarksService } from '../../services/bookmarks.service';
|
||||||
import { FilterService } from '../../services/filter.service';
|
import { FilterService } from '../../services/filter.service';
|
||||||
import { NoteInfoModalComponent } from '../../features/note-info/note-info-modal.component';
|
import { NoteInfoModalComponent } from '../../features/note-info/note-info-modal.component';
|
||||||
import { NoteInfoModalService } from '../../services/note-info-modal.service';
|
import { NoteInfoModalService } from '../../services/note-info-modal.service';
|
||||||
@ -375,6 +376,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
responsive = inject(ResponsiveService);
|
responsive = inject(ResponsiveService);
|
||||||
mobileNav = inject(MobileNavService);
|
mobileNav = inject(MobileNavService);
|
||||||
urlState = inject(UrlStateService);
|
urlState = inject(UrlStateService);
|
||||||
|
bookmarks = inject(BookmarksService);
|
||||||
filters = inject(FilterService);
|
filters = inject(FilterService);
|
||||||
noteInfo = inject(NoteInfoModalService);
|
noteInfo = inject(NoteInfoModalService);
|
||||||
inPageSearch = inject(InPageSearchService);
|
inPageSearch = inject(InPageSearchService);
|
||||||
@ -426,7 +428,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'ai' | 'trash' | 'help' | 'about' | 'tests' | 'playground' | null = null;
|
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'ai' | 'trash' | 'help' | 'about' | 'tests' | 'playground' | null = null;
|
||||||
private flyoutCloseTimer: any = null;
|
private flyoutCloseTimer: any = null;
|
||||||
tagFilter: string | null = null;
|
tagFilter: string | null = null;
|
||||||
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null;
|
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | 'bookmarks' | null = null;
|
||||||
private suppressNextNoteSelection = false;
|
private suppressNextNoteSelection = false;
|
||||||
|
|
||||||
// --- URL State <-> Layout sync ---
|
// --- URL State <-> Layout sync ---
|
||||||
@ -459,6 +461,11 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
return 'private';
|
return 'private';
|
||||||
case 'archive':
|
case 'archive':
|
||||||
return 'archive';
|
return 'archive';
|
||||||
|
case 'bookmarks':
|
||||||
|
case 'obsidian bookmarks':
|
||||||
|
case 'obsidian-bookmarks':
|
||||||
|
case '🔖 obsidian bookmarks':
|
||||||
|
return 'bookmarks';
|
||||||
default:
|
default:
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@ -473,6 +480,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
case 'task': return 'Tâches';
|
case 'task': return 'Tâches';
|
||||||
case 'private': return 'Privé';
|
case 'private': return 'Privé';
|
||||||
case 'archive': return 'Archive';
|
case 'archive': return 'Archive';
|
||||||
|
case 'bookmarks': return 'Bookmarks';
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -544,16 +552,11 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.quickLinkFilter = internal;
|
this.quickLinkFilter = internal;
|
||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
if (internal === 'favoris') {
|
this.autoSelectFirstNote();
|
||||||
this.suppressNextNoteSelection = true;
|
|
||||||
}
|
|
||||||
if (!hasNote && !this.suppressNextNoteSelection) {
|
|
||||||
this.autoSelectFirstNote();
|
|
||||||
}
|
|
||||||
if (!this.responsive.isDesktop()) {
|
if (!this.responsive.isDesktop()) {
|
||||||
this.mobileNav.setActiveTab('list');
|
this.mobileNav.setActiveTab('list');
|
||||||
}
|
}
|
||||||
} else if (!hasNote && !this.suppressNextNoteSelection) {
|
} else {
|
||||||
this.autoSelectFirstNote();
|
this.autoSelectFirstNote();
|
||||||
}
|
}
|
||||||
// Auto-open quick flyout when quick filter is active
|
// Auto-open quick flyout when quick filter is active
|
||||||
@ -568,7 +571,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
this.folderFilter = null;
|
this.folderFilter = null;
|
||||||
this.tagFilter = null;
|
this.tagFilter = null;
|
||||||
this.quickLinkFilter = null;
|
this.quickLinkFilter = null;
|
||||||
if (!hasNote) this.autoSelectFirstNote();
|
this.autoSelectFirstNote();
|
||||||
}
|
}
|
||||||
this.suppressNextNoteSelection = false;
|
this.suppressNextNoteSelection = false;
|
||||||
// Close any open flyout when no filters
|
// Close any open flyout when no filters
|
||||||
@ -630,10 +633,15 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
|
|
||||||
// Apply Quick Link filter
|
// Apply Quick Link filter
|
||||||
if (quickLink) {
|
if (quickLink) {
|
||||||
list = list.filter(n => {
|
if (quickLink === 'bookmarks') {
|
||||||
const frontmatter = n.frontmatter || {};
|
// Only markdown files and only those present in BookmarksService
|
||||||
return frontmatter[quickLink] === true;
|
list = list.filter(n => this.bookmarks.isBookmarked(n.filePath) && /\.md$/i.test(n.filePath || ''));
|
||||||
});
|
} else {
|
||||||
|
list = list.filter(n => {
|
||||||
|
const frontmatter = n.frontmatter || {} as any;
|
||||||
|
return frontmatter[quickLink as keyof typeof frontmatter] === true;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply query if present
|
// Apply query if present
|
||||||
@ -677,10 +685,8 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
|
|
||||||
onQueryChange(query: string) {
|
onQueryChange(query: string) {
|
||||||
this.listQuery = query;
|
this.listQuery = query;
|
||||||
// Only auto-select when query is cleared; while typing keep focus in search (handled by notes-list)
|
// Auto-select first note on any list change including search updates
|
||||||
if (!query) {
|
this.autoSelectFirstNote();
|
||||||
this.autoSelectFirstNote();
|
|
||||||
}
|
|
||||||
// Sync URL search term
|
// Sync URL search term
|
||||||
this.urlState.updateSearch(query);
|
this.urlState.updateSearch(query);
|
||||||
}
|
}
|
||||||
@ -836,8 +842,7 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onQuickLink(_id: string) {
|
onQuickLink(_id: string) {
|
||||||
const suppressAutoSelect = _id === 'all' || _id === 'favorites';
|
this.suppressNextNoteSelection = false;
|
||||||
this.suppressNextNoteSelection = suppressAutoSelect;
|
|
||||||
|
|
||||||
if (_id === 'all') {
|
if (_id === 'all') {
|
||||||
// Show all pages: clear filters and focus list
|
// Show all pages: clear filters and focus list
|
||||||
@ -965,11 +970,25 @@ export class AppShellNimbusLayoutComponent implements AfterViewInit {
|
|||||||
if (label) {
|
if (label) {
|
||||||
this.urlState.setQuickWithMarkdown(label);
|
this.urlState.setQuickWithMarkdown(label);
|
||||||
}
|
}
|
||||||
|
} else if (_id === 'bookmarks') {
|
||||||
|
// Filter by bookmarks (Obsidian bookmarks.json)
|
||||||
|
this.folderFilter = null;
|
||||||
|
this.tagFilter = null;
|
||||||
|
this.quickLinkFilter = 'bookmarks';
|
||||||
|
this.listQuery = '';
|
||||||
|
try { this.filters.clearKinds(); } catch {}
|
||||||
|
try { this.filters.clearTags(); } catch {}
|
||||||
|
if (!this.responsive.isDesktop()) {
|
||||||
|
this.mobileNav.setActiveTab('list');
|
||||||
|
}
|
||||||
|
this.scheduleCloseFlyout(150);
|
||||||
|
const label = this.mapInternalQuickToUrl('bookmarks');
|
||||||
|
if (label) {
|
||||||
|
this.urlState.setQuickWithMarkdown(label);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Auto-select first note after filter changes
|
// Auto-select first note after filter changes
|
||||||
if (!this.suppressNextNoteSelection) {
|
this.autoSelectFirstNote();
|
||||||
this.autoSelectFirstNote();
|
|
||||||
}
|
|
||||||
this.suppressNextNoteSelection = false;
|
this.suppressNextNoteSelection = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
276
src/app/services/bookmarks.service.ts
Normal file
276
src/app/services/bookmarks.service.ts
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
import { Injectable, signal, computed, inject } from '@angular/core';
|
||||||
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
|
import { firstValueFrom } from 'rxjs';
|
||||||
|
import { ToastService } from '../shared/toast/toast.service';
|
||||||
|
import { VaultService } from '../../services/vault.service';
|
||||||
|
|
||||||
|
export interface BookmarkItem {
|
||||||
|
type: 'file' | 'folder' | 'group' | 'search';
|
||||||
|
path?: string;
|
||||||
|
title?: string;
|
||||||
|
ctime?: number;
|
||||||
|
items?: BookmarkItem[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BookmarksDocument {
|
||||||
|
items: BookmarkItem[];
|
||||||
|
rev?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root'
|
||||||
|
})
|
||||||
|
export class BookmarksService {
|
||||||
|
private http = inject(HttpClient);
|
||||||
|
private toast = inject(ToastService);
|
||||||
|
private vault = inject(VaultService);
|
||||||
|
|
||||||
|
// State
|
||||||
|
private bookmarksDoc = signal<BookmarksDocument>({ items: [] });
|
||||||
|
private loading = signal(false);
|
||||||
|
private lastRev = signal<string | undefined>(undefined);
|
||||||
|
|
||||||
|
// Computed
|
||||||
|
readonly items = computed(() => this.bookmarksDoc().items);
|
||||||
|
readonly isLoading = computed(() => this.loading());
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Count all bookmarked file paths (recursively)
|
||||||
|
*/
|
||||||
|
readonly count = computed(() => {
|
||||||
|
return this.countFiles(this.items());
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get all bookmarked file paths (flattened)
|
||||||
|
*/
|
||||||
|
readonly bookmarkedPaths = computed(() => {
|
||||||
|
return this.extractFilePaths(this.items());
|
||||||
|
});
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
// Load bookmarks on startup
|
||||||
|
this.loadBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load bookmarks from server
|
||||||
|
*/
|
||||||
|
async loadBookmarks(): Promise<void> {
|
||||||
|
this.loading.set(true);
|
||||||
|
try {
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.http.get<BookmarksDocument>('/api/vault/bookmarks')
|
||||||
|
);
|
||||||
|
this.bookmarksDoc.set(response);
|
||||||
|
this.lastRev.set(response.rev);
|
||||||
|
|
||||||
|
// Validate and auto-repair on load (no-op if already valid)
|
||||||
|
try {
|
||||||
|
await this.validateAndRepair();
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('[BookmarksService] Validation skipped:', e);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[BookmarksService] Failed to load bookmarks:', error);
|
||||||
|
this.toast.error('Failed to load bookmarks');
|
||||||
|
} finally {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save bookmarks to server
|
||||||
|
*/
|
||||||
|
private async saveBookmarks(): Promise<void> {
|
||||||
|
this.loading.set(true);
|
||||||
|
try {
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
if (this.lastRev()) {
|
||||||
|
headers = headers.set('If-Match', this.lastRev()!);
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await firstValueFrom(
|
||||||
|
this.http.put<{ rev: string }>('/api/vault/bookmarks', { items: this.bookmarksDoc().items }, { headers })
|
||||||
|
);
|
||||||
|
this.lastRev.set(response.rev);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('[BookmarksService] Failed to save bookmarks:', error);
|
||||||
|
this.toast.error('Failed to save bookmarks');
|
||||||
|
throw error;
|
||||||
|
} finally {
|
||||||
|
this.loading.set(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if a file is bookmarked
|
||||||
|
*/
|
||||||
|
isBookmarked(filePath: string): boolean {
|
||||||
|
return this.bookmarkedPaths().includes(filePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Toggle bookmark for a file
|
||||||
|
*/
|
||||||
|
async toggleBookmark(filePath: string, title?: string): Promise<void> {
|
||||||
|
const currentItems = [...this.items()];
|
||||||
|
const isCurrentlyBookmarked = this.isBookmarked(filePath);
|
||||||
|
|
||||||
|
if (isCurrentlyBookmarked) {
|
||||||
|
// Remove bookmark
|
||||||
|
const filtered = this.removeFileFromItems(currentItems, filePath);
|
||||||
|
this.bookmarksDoc.update(doc => ({ ...doc, items: filtered }));
|
||||||
|
this.toast.success('Removed from bookmarks');
|
||||||
|
} else {
|
||||||
|
// Add bookmark
|
||||||
|
const newItem: BookmarkItem = {
|
||||||
|
type: 'file',
|
||||||
|
path: filePath,
|
||||||
|
title: title || filePath,
|
||||||
|
ctime: Date.now()
|
||||||
|
};
|
||||||
|
currentItems.push(newItem);
|
||||||
|
this.bookmarksDoc.update(doc => ({ ...doc, items: currentItems }));
|
||||||
|
this.toast.success('Added to bookmarks');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save to server
|
||||||
|
await this.saveBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively count file items
|
||||||
|
*/
|
||||||
|
private countFiles(items: BookmarkItem[]): number {
|
||||||
|
let count = 0;
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === 'file') {
|
||||||
|
count++;
|
||||||
|
} else if (item.items) {
|
||||||
|
count += this.countFiles(item.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively extract all file paths
|
||||||
|
*/
|
||||||
|
private extractFilePaths(items: BookmarkItem[]): string[] {
|
||||||
|
const paths: string[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === 'file' && item.path) {
|
||||||
|
paths.push(item.path);
|
||||||
|
} else if (item.items) {
|
||||||
|
paths.push(...this.extractFilePaths(item.items));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return paths;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively remove a file from items
|
||||||
|
*/
|
||||||
|
private removeFileFromItems(items: BookmarkItem[], filePath: string): BookmarkItem[] {
|
||||||
|
const result: BookmarkItem[] = [];
|
||||||
|
for (const item of items) {
|
||||||
|
if (item.type === 'file' && item.path === filePath) {
|
||||||
|
// Skip this item (remove it)
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (item.items) {
|
||||||
|
// Recursively filter nested items
|
||||||
|
const filteredChildren = this.removeFileFromItems(item.items, filePath);
|
||||||
|
result.push({ ...item, items: filteredChildren });
|
||||||
|
} else {
|
||||||
|
result.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear all bookmarks
|
||||||
|
*/
|
||||||
|
async clearAll(): Promise<void> {
|
||||||
|
this.bookmarksDoc.set({ items: [] });
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.toast.success('All bookmarks cleared');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Refresh bookmarks from server
|
||||||
|
*/
|
||||||
|
async refresh(): Promise<void> {
|
||||||
|
await this.loadBookmarks();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate and repair bookmarks against current vault metadata.
|
||||||
|
* - Normalizes paths (slashes, trim leading/trailing)
|
||||||
|
* - Removes entries for files that do not exist
|
||||||
|
* - Fixes case/extension mismatches to canonical file paths when possible
|
||||||
|
* Saves to server only when changes are detected.
|
||||||
|
*/
|
||||||
|
async validateAndRepair(): Promise<void> {
|
||||||
|
const original = this.bookmarksDoc();
|
||||||
|
const repairedItems = this.repairItems(original.items);
|
||||||
|
const changed = JSON.stringify(original.items) !== JSON.stringify(repairedItems);
|
||||||
|
if (changed) {
|
||||||
|
this.bookmarksDoc.set({ items: repairedItems, rev: original.rev });
|
||||||
|
await this.saveBookmarks();
|
||||||
|
this.toast.success('Bookmarks validated and repaired');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Helpers ---
|
||||||
|
private normalizePath(p?: string): string {
|
||||||
|
const s = String(p || '').replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
private repairItems(items: BookmarkItem[]): BookmarkItem[] {
|
||||||
|
// Build a lookup of canonical paths from vault metadata
|
||||||
|
const meta = (() => { try { return this.vault.allFilesMetadata() || []; } catch { return []; } })();
|
||||||
|
const canonicalByLc = new Map<string, string>();
|
||||||
|
for (const m of meta) {
|
||||||
|
const path = this.normalizePath((m as any).path || (m as any).filePath || '');
|
||||||
|
if (path) canonicalByLc.set(path.toLowerCase(), path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fix = (arr: BookmarkItem[]): BookmarkItem[] => {
|
||||||
|
const out: BookmarkItem[] = [];
|
||||||
|
for (const item of arr || []) {
|
||||||
|
if (!item) continue;
|
||||||
|
if (item.type === 'file') {
|
||||||
|
const raw = this.normalizePath(item.path);
|
||||||
|
if (!raw) continue; // drop empty
|
||||||
|
// try exact
|
||||||
|
let canonical = canonicalByLc.get(raw.toLowerCase());
|
||||||
|
// try with .md if missing
|
||||||
|
if (!canonical && !/\.[^\/]+$/.test(raw)) {
|
||||||
|
canonical = canonicalByLc.get((raw + '.md').toLowerCase());
|
||||||
|
}
|
||||||
|
if (!canonical) {
|
||||||
|
// file not found -> drop
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
out.push({ ...item, path: canonical });
|
||||||
|
} else if (Array.isArray(item.items) && item.items.length > 0) {
|
||||||
|
const repairedChildren = fix(item.items);
|
||||||
|
// keep group/folder/search only if it still has children (or if not a container)
|
||||||
|
if (repairedChildren.length > 0) {
|
||||||
|
out.push({ ...item, items: repairedChildren });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// keep non-file items without children as-is
|
||||||
|
out.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
};
|
||||||
|
|
||||||
|
return fix(items || []);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -10,8 +10,11 @@ export class ResponsiveService {
|
|||||||
isDesktop = signal<boolean>(false);
|
isDesktop = signal<boolean>(false);
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.breakpointObserver.observe('(max-width: 767px)').subscribe(r => this.isMobile.set(r.matches));
|
// Mobile layout: up to 1023px
|
||||||
this.breakpointObserver.observe('(min-width: 768px) and (max-width: 1023px)').subscribe(r => this.isTablet.set(r.matches));
|
this.breakpointObserver.observe('(max-width: 1023px)').subscribe(r => this.isMobile.set(r.matches));
|
||||||
|
// Tablet breakpoint disabled - go directly from desktop to mobile
|
||||||
|
this.breakpointObserver.observe('(min-width: 99999px)').subscribe(r => this.isTablet.set(r.matches));
|
||||||
|
// Desktop layout: 1024px and above
|
||||||
this.breakpointObserver.observe('(min-width: 1024px)').subscribe(r => this.isDesktop.set(r.matches));
|
this.breakpointObserver.observe('(min-width: 1024px)').subscribe(r => this.isDesktop.set(r.matches));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,6 +23,7 @@ import { EditorStateService } from '../../../services/editor-state.service';
|
|||||||
import { ClipboardService } from '../../../app/shared/services/clipboard.service';
|
import { ClipboardService } from '../../../app/shared/services/clipboard.service';
|
||||||
import { ToastService } from '../../../app/shared/toast/toast.service';
|
import { ToastService } from '../../../app/shared/toast/toast.service';
|
||||||
import { VaultService } from '../../../services/vault.service';
|
import { VaultService } from '../../../services/vault.service';
|
||||||
|
import { BookmarksService } from '../../../app/services/bookmarks.service';
|
||||||
import { Subscription } from 'rxjs';
|
import { Subscription } from 'rxjs';
|
||||||
import mermaid from 'mermaid';
|
import mermaid from 'mermaid';
|
||||||
|
|
||||||
@ -234,6 +235,23 @@ export interface WikiLinkActivation {
|
|||||||
|
|
||||||
<!-- Row 2: state icons (toggle buttons) -->
|
<!-- Row 2: state icons (toggle buttons) -->
|
||||||
<div class="flex flex-wrap items-center gap-3">
|
<div class="flex flex-wrap items-center gap-3">
|
||||||
|
<!-- Bookmarks button -->
|
||||||
|
<button type="button"
|
||||||
|
class="inline-flex items-center gap-1 transition-colors focus:outline-none"
|
||||||
|
[ngClass]="isBookmarked() ? 'text-blue-500' : 'text-muted'"
|
||||||
|
[title]="isBookmarked() ? 'Bookmarked' : 'Add to bookmarks'"
|
||||||
|
[attr.aria-label]="isBookmarked() ? 'Bookmarked' : 'Add to bookmarks'"
|
||||||
|
(click)="toggleBookmark()">
|
||||||
|
@if (isBookmarked()) {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="currentColor" stroke="none">
|
||||||
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
} @else {
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path d="M19 21l-7-5-7 5V5a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2z" />
|
||||||
|
</svg>
|
||||||
|
}
|
||||||
|
</button>
|
||||||
@if (hasState('favoris')) {
|
@if (hasState('favoris')) {
|
||||||
<button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" (click)="toggleState('favoris')">
|
<button type="button" class="inline-flex items-center gap-1 transition-colors focus:outline-none" [ngClass]="state('favoris') ? 'text-rose-500' : 'text-muted'" title="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" aria-label="{{ state('favoris') ? 'Ajouté aux favoris' : 'Non favori' }}" (click)="toggleState('favoris')">
|
||||||
@if (state('favoris')) {
|
@if (state('favoris')) {
|
||||||
@ -396,6 +414,7 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
private readonly toast = inject(ToastService);
|
private readonly toast = inject(ToastService);
|
||||||
private readonly vault = inject(VaultService);
|
private readonly vault = inject(VaultService);
|
||||||
private readonly editorState = inject(EditorStateService);
|
private readonly editorState = inject(EditorStateService);
|
||||||
|
private readonly bookmarks = inject(BookmarksService);
|
||||||
private readonly tagPaletteSize = 12;
|
private readonly tagPaletteSize = 12;
|
||||||
private readonly tagColorCache = new Map<string, number>();
|
private readonly tagColorCache = new Map<string, number>();
|
||||||
private readonly copyFeedbackTimers = new Map<HTMLElement, number>();
|
private readonly copyFeedbackTimers = new Map<HTMLElement, number>();
|
||||||
@ -450,6 +469,20 @@ export class NoteViewerComponent implements OnDestroy {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
|
|
||||||
|
isBookmarked = computed(() => {
|
||||||
|
const currentNote = this.note();
|
||||||
|
if (!currentNote?.filePath) return false;
|
||||||
|
return this.bookmarks.isBookmarked(currentNote.filePath);
|
||||||
|
});
|
||||||
|
|
||||||
|
async toggleBookmark(): Promise<void> {
|
||||||
|
const currentNote = this.note();
|
||||||
|
if (!currentNote?.filePath) return;
|
||||||
|
|
||||||
|
const title = String(currentNote.frontmatter?.title || currentNote.fileName || currentNote.filePath);
|
||||||
|
await this.bookmarks.toggleBookmark(currentNote.filePath, title);
|
||||||
|
}
|
||||||
|
|
||||||
async toggleState(key: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task'): Promise<void> {
|
async toggleState(key: 'publish' | 'favoris' | 'archive' | 'draft' | 'private' | 'template' | 'task'): Promise<void> {
|
||||||
const currentNote = this.note();
|
const currentNote = this.note();
|
||||||
if (!currentNote?.id) return;
|
if (!currentNote?.id) return;
|
||||||
|
|||||||
33
vault/.obsidian/bookmarks.json
vendored
33
vault/.obsidian/bookmarks.json
vendored
@ -10,21 +10,32 @@
|
|||||||
"ctime": 1759433952208,
|
"ctime": 1759433952208,
|
||||||
"path": "HOME.md",
|
"path": "HOME.md",
|
||||||
"title": "HOME"
|
"title": "HOME"
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "file",
|
|
||||||
"ctime": 1759677937745,
|
|
||||||
"path": "folder1/test2.md",
|
|
||||||
"title": "test2"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"type": "file",
|
"type": "file",
|
||||||
"ctime": 1759434060575,
|
"path": "folder-4/test-add-properties.md",
|
||||||
"path": "test.md",
|
"title": "test-add-properties.md",
|
||||||
"title": "Page de test Markdown"
|
"ctime": 1762268601102
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"path": "Allo-3/Nouvelle note 13.md",
|
||||||
|
"title": "Nouvelle note 13.md",
|
||||||
|
"ctime": 1762268909461
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"path": "Allo-3/Nouveau-markdown.md",
|
||||||
|
"title": "Nouveau-markdown.md",
|
||||||
|
"ctime": 1762268911797
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "file",
|
||||||
|
"path": "tata/Les Compléments Alimentaires Un Guide Général.md",
|
||||||
|
"title": "Les Compléments Alimentaires Un Guide Général.md",
|
||||||
|
"ctime": 1762268914159
|
||||||
}
|
}
|
||||||
],
|
]
|
||||||
"rev": "pu1hkm-417"
|
|
||||||
}
|
}
|
||||||
5
vault/.obsidian/workspace.json
vendored
5
vault/.obsidian/workspace.json
vendored
@ -74,7 +74,8 @@
|
|||||||
"title": "Bookmarks"
|
"title": "Bookmarks"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
],
|
||||||
|
"currentTab": 2
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"direction": "horizontal",
|
"direction": "horizontal",
|
||||||
@ -178,7 +179,7 @@
|
|||||||
"obsidian-excalidraw-plugin:New drawing": false
|
"obsidian-excalidraw-plugin:New drawing": false
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"active": "2e9abbba0bbc33e1",
|
"active": "aaf62e01f34df49b",
|
||||||
"lastOpenFiles": [
|
"lastOpenFiles": [
|
||||||
"big/note_500.md",
|
"big/note_500.md",
|
||||||
"big/note_499.md",
|
"big/note_499.md",
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user