feat: add app icons and update UI with logo branding
This commit is contained in:
parent
397f1b4b80
commit
58bd57543b
@ -29,7 +29,11 @@
|
||||
"src/styles.css"
|
||||
],
|
||||
"assets": [
|
||||
"src/assets"
|
||||
{
|
||||
"glob": "**/*",
|
||||
"input": "src/assets",
|
||||
"output": "assets"
|
||||
}
|
||||
]
|
||||
},
|
||||
"configurations": {
|
||||
|
||||
@ -4,6 +4,11 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>ObsiViewer - Obsidian Vault Viewer</title>
|
||||
<base href="/" />
|
||||
<link rel="icon" type="image/x-icon" href="/assets/favicon.ico">
|
||||
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
||||
<link rel="apple-touch-icon" href="/assets/apple-touch-icon.png">
|
||||
<link rel="manifest" href="/assets/site.webmanifest">
|
||||
<script>
|
||||
(function () {
|
||||
try {
|
||||
|
||||
@ -19,8 +19,12 @@ archive: false
|
||||
draft: false
|
||||
private: false
|
||||
---
|
||||

|
||||
|
||||
# 🧭 Guide d'utilisateur ObsiViewer
|
||||
|
||||
|
||||
|
||||
Bienvenue dans **ObsiViewer** !
|
||||
|
||||
Ce guide vous aidera à comprendre les principales fonctionnalités :
|
||||
|
||||
@ -56,8 +56,8 @@ import { trigger, transition, style, animate } from '@angular/animations';
|
||||
<div class="text-center space-y-6">
|
||||
<!-- Logo/Icon -->
|
||||
<div class="flex justify-center">
|
||||
<div class="w-20 h-20 rounded-2xl bg-gradient-to-br from-nimbus-400 to-nimbus-600 flex items-center justify-center text-4xl shadow-lg">
|
||||
📖
|
||||
<div class="w-40 h-40 rounded-2xl bg-gradient-to-br from-nimbus-400 to-nimbus-600 flex items-center justify-center shadow-lg overflow-hidden">
|
||||
<img src="assets/favicon.svg" alt="ObsiViewer" class="w-40 h-40" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@ -19,7 +19,10 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
|
||||
[class.translate-x-0]="mobileNav.sidebarOpen()">
|
||||
<!-- Header -->
|
||||
<div class="flex items-center justify-between p-4 border-b border-border dark:border-gray-800 bg-gradient-to-r from-nimbus-50 to-transparent dark:from-nimbus-900/20">
|
||||
<h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 flex-shrink-0" />
|
||||
<h2 class="text-lg font-semibold truncate">{{ vaultName || 'ObsiViewer' }}</h2>
|
||||
</div>
|
||||
<button
|
||||
(click)="mobileNav.toggleSidebar()"
|
||||
class="p-2 rounded-full hover:bg-surface1 dark:hover:bg-card transition-all active:scale-95 transform flex-shrink-0">
|
||||
|
||||
@ -17,7 +17,10 @@ import { VaultService } from '../../../services/vault.service';
|
||||
<div class="h-full flex flex-col overflow-hidden select-none">
|
||||
<!-- Header -->
|
||||
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
|
||||
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 flex-shrink-0" />
|
||||
<div class="text-sm font-semibold truncate">{{ vaultName }} - ObsiViewer</div>
|
||||
</div>
|
||||
<button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar">⟨⟨</button>
|
||||
</div>
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { Component, EventEmitter, HostListener, Input, Output, inject } from '@angular/core';
|
||||
import { Component, EventEmitter, HostListener, Input, Output, inject, effect } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { UiModeService } from '../../shared/services/ui-mode.service';
|
||||
import { ResponsiveService } from '../../shared/services/responsive.service';
|
||||
@ -70,17 +70,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
||||
</ng-container>
|
||||
<ng-template #collapsedRail>
|
||||
<aside class="border-r border-border dark:border-gray-800 h-full w-14 flex flex-col items-center py-3 gap-3">
|
||||
<img src="assets/favicon.svg" alt="ObsiViewer" class="h-6 w-6 mb-2" />
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="toggleSidebarRequest.emit()" title="Show Sidebar">☰</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('quick')" (mouseleave)="scheduleCloseFlyout()" title="Quick Links">▦</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('folders')" (mouseleave)="scheduleCloseFlyout()" title="Folders">📁</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('tags')" (mouseleave)="scheduleCloseFlyout()" title="Tags">🏷️</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('trash')" (mouseleave)="scheduleCloseFlyout()" title="Trash">🗑️</button>
|
||||
<div class="h-px w-8 bg-border dark:bg-gray-800 my-1"></div>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('playground')" (mouseleave)="scheduleCloseFlyout()" title="Markdown Playground">🧪</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('help')" (mouseleave)="scheduleCloseFlyout()" title="Help">🆘</button>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (mouseenter)="openFlyout('about')" (mouseleave)="scheduleCloseFlyout()" title="About">ℹ️</button>
|
||||
</aside>
|
||||
|
||||
<!-- Flyouts -->
|
||||
<div class="absolute left-14 top-0 bottom-0 w-80 max-w-[70vw] bg-card dark:bg-main border-r border-border dark:border-gray-800 shadow-xl z-50" *ngIf="hoveredFlyout as f" (mouseenter)="cancelCloseFlyout()" (mouseleave)="scheduleCloseFlyout()">
|
||||
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800">
|
||||
<div class="text-sm font-semibold">{{ f === 'quick' ? 'Quick Links' : (f === 'folders' ? 'Folders' : (f === 'tags' ? 'Tags' : 'Trash')) }}</div>
|
||||
<div class="text-sm font-semibold">{{
|
||||
f === 'quick' ? 'Quick Links' :
|
||||
(f === 'folders' ? 'Folders' :
|
||||
(f === 'tags' ? 'Tags' :
|
||||
(f === 'trash' ? 'Trash' :
|
||||
(f === 'help' ? 'Help' :
|
||||
(f === 'about' ? 'About' :
|
||||
(f === 'playground' ? 'Playground' : ''))))))
|
||||
}}</div>
|
||||
<button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="hoveredFlyout=null">✕</button>
|
||||
</div>
|
||||
<div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay>
|
||||
@ -96,6 +109,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div *ngSwitchCase="'trash'" class="p-2">
|
||||
<ng-container *ngIf="(vault.trashTree() || []).length > 0; else emptyTrash">
|
||||
<app-file-explorer
|
||||
[nodes]="vault.trashTree()"
|
||||
[selectedNoteId]="selectedNoteId"
|
||||
[foldersOnly]="true"
|
||||
[useTrashCounts]="true"
|
||||
(folderSelected)="onFolderSelected($event)"
|
||||
(fileSelected)="noteSelected.emit($event)">
|
||||
</app-file-explorer>
|
||||
</ng-container>
|
||||
<ng-template #emptyTrash>
|
||||
<div class="px-3 py-2 text-muted text-sm">La corbeille est vide</div>
|
||||
</ng-template>
|
||||
</div>
|
||||
<div *ngSwitchCase="'help'" class="p-3">
|
||||
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onHelpPageSelected()">🆘 <span>Help Page</span></button>
|
||||
</div>
|
||||
<div *ngSwitchCase="'about'" class="p-3">
|
||||
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onAboutSelected()">ℹ️ <span>About</span></button>
|
||||
</div>
|
||||
<div *ngSwitchCase="'playground'" class="p-3">
|
||||
<button type="button" class="w-full text-left flex items-center gap-2 px-3 py-2 text-sm rounded-lg hover:bg-surface1 dark:hover:bg-card" (click)="onMarkdownPlaygroundSelected()">🧪 <span>Markdown Playground</span></button>
|
||||
</div>
|
||||
<div *ngSwitchDefault class="p-3 text-sm text-muted">Empty</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
@ -115,7 +152,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
||||
[quickLinkFilter]="quickLinkFilter"
|
||||
[query]="listQuery"
|
||||
(openNote)="onOpenNote($event)"
|
||||
(queryChange)="listQuery = $event"
|
||||
(queryChange)="onQueryChange($event)"
|
||||
/>
|
||||
</div>
|
||||
</section>
|
||||
@ -190,7 +227,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
||||
<app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
|
||||
</div>
|
||||
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay>
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||
<app-notes-list [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onOpenNote($event)"></app-notes-list>
|
||||
</div>
|
||||
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay>
|
||||
<app-markdown-playground *ngIf="activeView === 'markdown-playground'"></app-markdown-playground>
|
||||
@ -218,7 +255,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
|
||||
|
||||
@if (mobileNav.activeTab() === 'list') {
|
||||
<div class="h-full flex flex-col overflow-hidden animate-fadeIn">
|
||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="listQuery=$event" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
||||
<app-notes-list class="flex-1" [notes]="vault.allNotes()" [folderFilter]="folderFilter" [tagFilter]="tagFilter" [quickLinkFilter]="quickLinkFilter" [query]="listQuery" (queryChange)="onQueryChange($event)" (openNote)="onNoteSelectedMobile($event)"></app-notes-list>
|
||||
</div>
|
||||
}
|
||||
|
||||
@ -296,23 +333,78 @@ export class AppShellNimbusLayoutComponent {
|
||||
|
||||
folderFilter: string | null = null;
|
||||
listQuery: string = '';
|
||||
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | null = null;
|
||||
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | 'help' | 'about' | 'playground' | null = null;
|
||||
private flyoutCloseTimer: any = null;
|
||||
tagFilter: string | null = null;
|
||||
quickLinkFilter: 'favoris' | 'publish' | 'draft' | 'template' | 'task' | 'private' | 'archive' | null = null;
|
||||
|
||||
// Auto-select first note when filters change
|
||||
private autoSelectFirstNote() {
|
||||
const filteredNotes = this.getFilteredNotes();
|
||||
if (filteredNotes.length > 0 && filteredNotes[0].id !== this.selectedNoteId) {
|
||||
this.noteSelected.emit(filteredNotes[0].id);
|
||||
}
|
||||
}
|
||||
|
||||
private getFilteredNotes(): Note[] {
|
||||
const q = (this.listQuery || '').toLowerCase().trim();
|
||||
const folder = (this.folderFilter || '').toLowerCase().replace(/^\/+|\/+$/g, '');
|
||||
const tag = (this.tagFilter || '').toLowerCase();
|
||||
const quickLink = this.quickLinkFilter;
|
||||
let list = this.vault.allNotes();
|
||||
|
||||
if (folder) {
|
||||
if (folder === '.trash') {
|
||||
// All files anywhere under .trash (including subfolders)
|
||||
list = list.filter(n => {
|
||||
const filePath = (n.filePath || n.originalPath || '').toLowerCase().replace(/\\/g, '/');
|
||||
return filePath.startsWith('.trash/') || filePath.includes('/.trash/');
|
||||
});
|
||||
} else {
|
||||
list = list.filter(n => {
|
||||
const originalPath = (n.originalPath || '').toLowerCase().replace(/^\/+|\/+$/g, '');
|
||||
return originalPath === folder || originalPath.startsWith(folder + '/');
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (tag) {
|
||||
list = list.filter(n => Array.isArray(n.tags) && n.tags.some(t => (t || '').toLowerCase() === tag));
|
||||
}
|
||||
|
||||
// Apply Quick Link filter
|
||||
if (quickLink) {
|
||||
list = list.filter(n => {
|
||||
const frontmatter = n.frontmatter || {};
|
||||
return frontmatter[quickLink] === true;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply query if present
|
||||
if (q) {
|
||||
list = list.filter(n => {
|
||||
const title = (n.title || '').toLowerCase();
|
||||
const filePath = (n.filePath || '').toLowerCase();
|
||||
return title.includes(q) || filePath.includes(q);
|
||||
});
|
||||
}
|
||||
|
||||
// Sort by most recent first (same as notes-list component)
|
||||
const parseDate = (s?: string) => (s ? Date.parse(s) : 0) || 0;
|
||||
const score = (n: Note) => n.mtime || parseDate(n.updatedAt) || parseDate(n.createdAt) || 0;
|
||||
return [...list].sort((a, b) => (score(b) - score(a)));
|
||||
}
|
||||
|
||||
onQueryChange(query: string) {
|
||||
this.listQuery = query;
|
||||
this.autoSelectFirstNote();
|
||||
}
|
||||
|
||||
toggleNoteFullScreen(): void {
|
||||
this.noteFullScreen = !this.noteFullScreen;
|
||||
document.body.classList.toggle('note-fullscreen-active', this.noteFullScreen);
|
||||
}
|
||||
|
||||
@HostListener('document:keydown.escape')
|
||||
handleEscape(): void {
|
||||
if (!this.noteFullScreen) return;
|
||||
this.noteFullScreen = false;
|
||||
document.body.classList.remove('note-fullscreen-active');
|
||||
}
|
||||
|
||||
nextTab() {
|
||||
const order: Array<'sidebar' | 'list' | 'page' | 'toc'> = ['sidebar', 'list', 'page', 'toc'];
|
||||
const idx = order.indexOf(this.mobileNav.activeTab());
|
||||
@ -340,6 +432,7 @@ export class AppShellNimbusLayoutComponent {
|
||||
onFolderSelected(path: string) {
|
||||
this.folderFilter = path || null;
|
||||
this.tagFilter = null; // clear tag when focusing folder
|
||||
this.autoSelectFirstNote();
|
||||
if (this.responsive.isMobile() || this.responsive.isTablet()) {
|
||||
this.mobileNav.setActiveTab('list');
|
||||
}
|
||||
@ -347,6 +440,8 @@ export class AppShellNimbusLayoutComponent {
|
||||
|
||||
onFolderSelectedFromDrawer(path: string) {
|
||||
this.folderFilter = path || null;
|
||||
this.tagFilter = null; // clear tag when focusing folder
|
||||
this.autoSelectFirstNote();
|
||||
this.mobileNav.setActiveTab('list');
|
||||
this.mobileNav.sidebarOpen.set(false);
|
||||
}
|
||||
@ -433,6 +528,8 @@ export class AppShellNimbusLayoutComponent {
|
||||
}
|
||||
this.scheduleCloseFlyout(150);
|
||||
}
|
||||
// Auto-select first note after filter changes
|
||||
this.autoSelectFirstNote();
|
||||
}
|
||||
|
||||
onTagSelected(tagName: string) {
|
||||
@ -443,6 +540,8 @@ export class AppShellNimbusLayoutComponent {
|
||||
// Clear other filters and search to focus on tag results
|
||||
this.quickLinkFilter = null;
|
||||
this.listQuery = '';
|
||||
// Auto-select first note after filter changes
|
||||
this.autoSelectFirstNote();
|
||||
// Ensure the list is visible: exit fullscreen if active
|
||||
if (this.noteFullScreen) {
|
||||
this.noteFullScreen = false;
|
||||
|
||||
BIN
src/assets/apple-touch-icon.png
Normal file
BIN
src/assets/apple-touch-icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
src/assets/favicon-96x96.png
Normal file
BIN
src/assets/favicon-96x96.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
BIN
src/assets/favicon.ico
Normal file
BIN
src/assets/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 15 KiB |
3
src/assets/favicon.svg
Normal file
3
src/assets/favicon.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 316 KiB |
21
src/assets/site.webmanifest
Normal file
21
src/assets/site.webmanifest
Normal file
@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "MyWebSite",
|
||||
"short_name": "MySite",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
src/assets/web-app-manifest-192x192.png
Normal file
BIN
src/assets/web-app-manifest-192x192.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
BIN
src/assets/web-app-manifest-512x512.png
Normal file
BIN
src/assets/web-app-manifest-512x512.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 191 KiB |
@ -340,6 +340,7 @@ export class VaultService implements OnDestroy {
|
||||
if (!item?.path) continue;
|
||||
|
||||
const path = this.normalizePath(item.path);
|
||||
if (this.isBuiltinPath(path)) continue;
|
||||
this.indexMetadata(item, path);
|
||||
|
||||
const parentFolder = this.buildFolderStructure(path, folders, root);
|
||||
@ -426,6 +427,9 @@ export class VaultService implements OnDestroy {
|
||||
const sortedNotes = this.getSortedNotes();
|
||||
|
||||
for (const note of sortedNotes) {
|
||||
if (this.isBuiltinPath(note.originalPath) || this.isBuiltinPath(note.filePath)) {
|
||||
continue;
|
||||
}
|
||||
const segments = note.originalPath.split('/').filter(Boolean);
|
||||
const folderSegments = segments.slice(0, -1);
|
||||
|
||||
@ -559,7 +563,7 @@ export class VaultService implements OnDestroy {
|
||||
|
||||
for (const note of this.allNotes()) {
|
||||
const path = this.normalizePath(note.originalPath || note.filePath || '');
|
||||
if (!path || this.isInTrash(path)) continue;
|
||||
if (!path || this.isInTrash(path) || this.isBuiltinPath(path)) continue;
|
||||
|
||||
const parts = path.split('/');
|
||||
parts.pop(); // Remove filename
|
||||
@ -1072,6 +1076,13 @@ export class VaultService implements OnDestroy {
|
||||
return (path || '').replace(/\\/g, '/');
|
||||
}
|
||||
|
||||
private isBuiltinPath(path: string): boolean {
|
||||
const normalized = this.normalizePath(path).replace(/^\/+/g, '');
|
||||
if (!normalized) return false;
|
||||
const [firstSegment] = normalized.split('/');
|
||||
return firstSegment === '__builtin__';
|
||||
}
|
||||
|
||||
private normalizeLineEndings(content: string): string {
|
||||
return content.replace(/\r\n/g, '\n');
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user