feat: add app icons and update UI with logo branding

This commit is contained in:
Bruno Charest 2025-10-22 21:07:41 -04:00
parent 397f1b4b80
commit 58bd57543b
15 changed files with 172 additions and 19 deletions

View File

@ -29,7 +29,11 @@
"src/styles.css" "src/styles.css"
], ],
"assets": [ "assets": [
"src/assets" {
"glob": "**/*",
"input": "src/assets",
"output": "assets"
}
] ]
}, },
"configurations": { "configurations": {

View File

@ -4,6 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>ObsiViewer - Obsidian Vault Viewer</title> <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> <script>
(function () { (function () {
try { try {

View File

@ -19,8 +19,12 @@ archive: false
draft: false draft: false
private: false private: false
--- ---
![ObsiViewer](assets/favicon-96x96.png)
# 🧭 Guide d'utilisateur ObsiViewer # 🧭 Guide d'utilisateur ObsiViewer
Bienvenue dans **ObsiViewer** ! Bienvenue dans **ObsiViewer** !
Ce guide vous aidera à comprendre les principales fonctionnalités : Ce guide vous aidera à comprendre les principales fonctionnalités :

View File

@ -56,8 +56,8 @@ import { trigger, transition, style, animate } from '@angular/animations';
<div class="text-center space-y-6"> <div class="text-center space-y-6">
<!-- Logo/Icon --> <!-- Logo/Icon -->
<div class="flex justify-center"> <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>
</div> </div>

View File

@ -19,7 +19,10 @@ import { TrashExplorerComponent } from '../../layout/sidebar/trash/trash-explore
[class.translate-x-0]="mobileNav.sidebarOpen()"> [class.translate-x-0]="mobileNav.sidebarOpen()">
<!-- Header --> <!-- 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"> <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 <button
(click)="mobileNav.toggleSidebar()" (click)="mobileNav.toggleSidebar()"
class="p-2 rounded-full hover:bg-surface1 dark:hover:bg-card transition-all active:scale-95 transform flex-shrink-0"> class="p-2 rounded-full hover:bg-surface1 dark:hover:bg-card transition-all active:scale-95 transform flex-shrink-0">

View File

@ -17,7 +17,10 @@ import { VaultService } from '../../../services/vault.service';
<div class="h-full flex flex-col overflow-hidden select-none"> <div class="h-full flex flex-col overflow-hidden select-none">
<!-- Header --> <!-- Header -->
<div class="h-12 flex items-center justify-between px-3 border-b border-border dark:border-gray-800"> <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> <button (click)="toggleSidebarRequest.emit()" class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" title="Hide Sidebar"></button>
</div> </div>

View File

@ -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 { CommonModule } from '@angular/common';
import { UiModeService } from '../../shared/services/ui-mode.service'; import { UiModeService } from '../../shared/services/ui-mode.service';
import { ResponsiveService } from '../../shared/services/responsive.service'; import { ResponsiveService } from '../../shared/services/responsive.service';
@ -70,17 +70,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
</ng-container> </ng-container>
<ng-template #collapsedRail> <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"> <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" (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('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('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('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> <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> </aside>
<!-- Flyouts --> <!-- 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="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="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> <button class="p-2 rounded hover:bg-surface1 dark:hover:bg-card" (click)="hoveredFlyout=null"></button>
</div> </div>
<div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay> <div class="h-[calc(100%-3rem)] overflow-y-auto" appScrollableOverlay>
@ -96,6 +109,30 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
</li> </li>
</ul> </ul>
</div> </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> <div *ngSwitchDefault class="p-3 text-sm text-muted">Empty</div>
</ng-container> </ng-container>
</div> </div>
@ -115,7 +152,7 @@ import { AboutPanelComponent } from '../../features/about/about-panel.component'
[quickLinkFilter]="quickLinkFilter" [quickLinkFilter]="quickLinkFilter"
[query]="listQuery" [query]="listQuery"
(openNote)="onOpenNote($event)" (openNote)="onOpenNote($event)"
(queryChange)="listQuery = $event" (queryChange)="onQueryChange($event)"
/> />
</div> </div>
</section> </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> <app-file-explorer [nodes]="effectiveFileTree" [selectedNoteId]="selectedNoteId" [foldersOnly]="true" (folderSelected)="onFolderSelected($event)" (fileSelected)="onOpenNote($event)"></app-file-explorer>
</div> </div>
<div [hidden]="mobileNav.activeTab() !== 'list'" class="h-full overflow-y-auto" appScrollableOverlay> <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>
<div [hidden]="mobileNav.activeTab() !== 'page'" class="note-content-area h-full overflow-y-auto px-3 py-4" appScrollableOverlay> <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> <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') { @if (mobileNav.activeTab() === 'list') {
<div class="h-full flex flex-col overflow-hidden animate-fadeIn"> <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> </div>
} }
@ -296,23 +333,78 @@ export class AppShellNimbusLayoutComponent {
folderFilter: string | null = null; folderFilter: string | null = null;
listQuery: string = ''; listQuery: string = '';
hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | null = null; hoveredFlyout: 'quick' | 'folders' | 'tags' | 'trash' | 'help' | 'about' | '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' | 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 { toggleNoteFullScreen(): void {
this.noteFullScreen = !this.noteFullScreen; this.noteFullScreen = !this.noteFullScreen;
document.body.classList.toggle('note-fullscreen-active', 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() { nextTab() {
const order: Array<'sidebar' | 'list' | 'page' | 'toc'> = ['sidebar', 'list', 'page', 'toc']; const order: Array<'sidebar' | 'list' | 'page' | 'toc'> = ['sidebar', 'list', 'page', 'toc'];
const idx = order.indexOf(this.mobileNav.activeTab()); const idx = order.indexOf(this.mobileNav.activeTab());
@ -340,6 +432,7 @@ export class AppShellNimbusLayoutComponent {
onFolderSelected(path: string) { onFolderSelected(path: string) {
this.folderFilter = path || null; this.folderFilter = path || null;
this.tagFilter = null; // clear tag when focusing folder this.tagFilter = null; // clear tag when focusing folder
this.autoSelectFirstNote();
if (this.responsive.isMobile() || this.responsive.isTablet()) { if (this.responsive.isMobile() || this.responsive.isTablet()) {
this.mobileNav.setActiveTab('list'); this.mobileNav.setActiveTab('list');
} }
@ -347,6 +440,8 @@ export class AppShellNimbusLayoutComponent {
onFolderSelectedFromDrawer(path: string) { onFolderSelectedFromDrawer(path: string) {
this.folderFilter = path || null; this.folderFilter = path || null;
this.tagFilter = null; // clear tag when focusing folder
this.autoSelectFirstNote();
this.mobileNav.setActiveTab('list'); this.mobileNav.setActiveTab('list');
this.mobileNav.sidebarOpen.set(false); this.mobileNav.sidebarOpen.set(false);
} }
@ -433,6 +528,8 @@ export class AppShellNimbusLayoutComponent {
} }
this.scheduleCloseFlyout(150); this.scheduleCloseFlyout(150);
} }
// Auto-select first note after filter changes
this.autoSelectFirstNote();
} }
onTagSelected(tagName: string) { onTagSelected(tagName: string) {
@ -443,6 +540,8 @@ export class AppShellNimbusLayoutComponent {
// Clear other filters and search to focus on tag results // Clear other filters and search to focus on tag results
this.quickLinkFilter = null; this.quickLinkFilter = null;
this.listQuery = ''; this.listQuery = '';
// Auto-select first note after filter changes
this.autoSelectFirstNote();
// Ensure the list is visible: exit fullscreen if active // Ensure the list is visible: exit fullscreen if active
if (this.noteFullScreen) { if (this.noteFullScreen) {
this.noteFullScreen = false; this.noteFullScreen = false;

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

BIN
src/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

3
src/assets/favicon.svg Normal file

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 316 KiB

View 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"
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 191 KiB

View File

@ -340,6 +340,7 @@ export class VaultService implements OnDestroy {
if (!item?.path) continue; if (!item?.path) continue;
const path = this.normalizePath(item.path); const path = this.normalizePath(item.path);
if (this.isBuiltinPath(path)) continue;
this.indexMetadata(item, path); this.indexMetadata(item, path);
const parentFolder = this.buildFolderStructure(path, folders, root); const parentFolder = this.buildFolderStructure(path, folders, root);
@ -426,6 +427,9 @@ export class VaultService implements OnDestroy {
const sortedNotes = this.getSortedNotes(); const sortedNotes = this.getSortedNotes();
for (const note of sortedNotes) { for (const note of sortedNotes) {
if (this.isBuiltinPath(note.originalPath) || this.isBuiltinPath(note.filePath)) {
continue;
}
const segments = note.originalPath.split('/').filter(Boolean); const segments = note.originalPath.split('/').filter(Boolean);
const folderSegments = segments.slice(0, -1); const folderSegments = segments.slice(0, -1);
@ -559,7 +563,7 @@ export class VaultService implements OnDestroy {
for (const note of this.allNotes()) { for (const note of this.allNotes()) {
const path = this.normalizePath(note.originalPath || note.filePath || ''); 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('/'); const parts = path.split('/');
parts.pop(); // Remove filename parts.pop(); // Remove filename
@ -1072,6 +1076,13 @@ export class VaultService implements OnDestroy {
return (path || '').replace(/\\/g, '/'); 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 { private normalizeLineEndings(content: string): string {
return content.replace(/\r\n/g, '\n'); return content.replace(/\r\n/g, '\n');
} }