Compress stored content for improved caching size and speed. Add loading component on every Content ready awaiting to reduce first render time.

This commit is contained in:
Clément Pons
2025-10-28 17:57:20 +01:00
parent 1c3211d28e
commit fde752b6ed
6 changed files with 77 additions and 61 deletions

View File

@@ -3,7 +3,7 @@ import { Canvas, CanvasEditor } from "#shared/canvas.util";
import render from "#shared/markdown.util";
import { confirm, contextmenu, tooltip } from "#shared/floating.util";
import { cancelPropagation, dom, icon, text, type Node } from "#shared/dom.util";
import { loading } from "#shared/components.util";
import { async, loading } from "#shared/components.util";
import prose, { h1, h2 } from "#shared/proses";
import { getID, parsePath } from '#shared/general.util';
import { TreeDOM, type Recursive } from '#shared/tree';
@@ -302,8 +302,9 @@ export class Content
const handle = await Content.root.getFileHandle(path, options);
const file = await handle.getFile();
const text = await file.text();
return text;
//@ts-ignore
const response = await new Response(file.stream().pipeThrough(new DecompressionStream('gzip')));
return await response.text();
}
catch(e)
{
@@ -330,15 +331,18 @@ export class Content
//Easy to use, but not very performant.
private static async write(path: string, content: string, options?: FileSystemGetFileOptions): Promise<void>
{
const size = new TextEncoder().encode(content).byteLength;
try
{
const parent = path.split('/').slice(0, -1).join('/'), basename = path.split('/').slice(-1).join('/');
const handle = await (await Content.goto(parent, { create: true }) ?? Content.root).getFileHandle(basename, options);
const file = await handle.createWritable({ keepExistingData: false });
await file.write(content);
await file.close();
await new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode(content));
controller.close();
}
}).pipeThrough(new CompressionStream("gzip")).pipeTo(file);
}
catch(e)
{
@@ -373,26 +377,27 @@ export class Content
return handlers[overview.type].fromString(content);
}
static render(parent: HTMLElement, path: string): Omit<LocalContent, 'content'> | undefined
static async render(parent: HTMLElement, path: string): Promise<Omit<LocalContent, 'content'> | undefined>
{
parent.appendChild(dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]))
await Content.ready;
const overview = Content.getFromPath(path);
if(!!overview)
{
const load = dom('div', { class: 'flex, flex-1 justify-center items-center' }, [loading('normal')]);
parent.appendChild(load);
function _render<T extends FileType>(content: LocalContent<T>): void
{
const el = handlers[content.type].render(content);
el && parent.replaceChild(el, load);
el && parent.replaceChildren(el);
}
Content.getContent(overview.id).then(content => _render(content!));
}
else
{
parent.appendChild(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
parent.replaceChildren(dom('h2', { class: 'flex-1 text-center', text: "Impossible d'afficher le contenu demandé" }));
}
return overview;
@@ -514,7 +519,7 @@ const handlers: { [K in FileType]: ContentTypeHandler<K> } = {
//TODO: Edition link
]),
]),
render(content.content),
render(content.content, undefined, { class: 'pb-64' }),
])
},
renderEditor: (content) => {
@@ -572,7 +577,7 @@ export const iconByType: Record<FileType, string> = {
export class Editor
{
tree: TreeDOM;
tree!: TreeDOM;
container: HTMLDivElement;
selected?: Recursive<LocalContent & { element?: HTMLElement }>;
@@ -692,29 +697,31 @@ export class Editor
},
}, () => { this.tree.tree.each(e => Content.set(e.id, e)); Content.save(); });
this.tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
}, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
Content.ready.then(() => {
this.tree = new TreeDOM((item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full cursor-pointer font-medium'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent)} }, [
icon('radix-icons:chevron-right', { class: 'h-4 w-4 transition-transform absolute group-data-[state=open]:rotate-90', style: { 'left': `${depth / 2 - 1.5}em` } }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
}, (item, depth) => {
return dom('div', { class: 'group flex items-center ps-2 outline-none relative cursor-pointer', style: { 'padding-left': `${depth / 2 - 0.5}em` } }, [dom('div', { class: ['flex flex-1 items-center hover:border-accent-blue hover:text-accent-purple max-w-full'], attributes: { 'data-private': item.private }, listeners: { contextmenu: (e) => this.contextmenu(e, item as LocalContent), click: () => this.select(item as LocalContent) } }, [
icon(iconByType[item.type], { class: 'w-5 h-5', width: 20, height: 20 }),
dom('div', { class: 'pl-1.5 py-1.5 flex-1 truncate', text: item.title, attributes: { title: item.title } }),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.toggleNavigable(e, item as LocalContent) } }, [icon(item.navigable ? 'radix-icons:eye-open' : 'radix-icons:eye-none', { class: ['mx-1', { 'opacity-50': !item.navigable }] })]), 'Navigable', 'left'),
tooltip(dom('span', { class: 'flex', listeners: { click: e => this.togglePrivate(e, item as LocalContent) } }, [icon(item.private ? 'radix-icons:lock-closed' : 'radix-icons:lock-open-2', { class: ['mx-1', { 'opacity-50': !item.private }] })]), 'Privé', 'right'),
])]);
});
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
this.cleanup = this.setupDnD();
});
this.instruction = dom('div', { class: 'absolute h-full w-full top-0 right-0 border-light-50 dark:border-dark-50' });
this.cleanup = this.setupDnD();
this.container = dom('div', { class: 'flex flex-1 flex-col items-start justify-start max-h-full relative' }, [dom('div', { class: 'py-4 flex-1 w-full max-h-full flex overflow-auto xl:px-12 lg:px-8 px-6 relative' })]);
this.select(this.tree.tree.find(useRouter().currentRoute.value.hash.substring(1)) as Recursive<LocalContent & { element?: HTMLElement }> | undefined);
}
private contextmenu(e: MouseEvent, item: Recursive<LocalContent>)
{