From fde752b6edb78415d63f9602ab459161b338e79e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pons?= Date: Tue, 28 Oct 2025 17:57:20 +0100 Subject: [PATCH] Compress stored content for improved caching size and speed. Add loading component on every Content ready awaiting to reduce first render time. --- db.sqlite | Bin 761856 -> 761856 bytes error.vue | 17 +++++---- layouts/default.vue | 42 ++++++++++++--------- pages/explore/[...path].vue | 4 +- pages/explore/edit/index.vue | 4 +- shared/content.util.ts | 71 +++++++++++++++++++---------------- 6 files changed, 77 insertions(+), 61 deletions(-) diff --git a/db.sqlite b/db.sqlite index c0229c89dad3e31ec2499a40ed36f605e8374e30..99213a8158ef29435203851692d78d8ba7d9b494 100644 GIT binary patch delta 199 zcmZoTpx1CfZv$fhF9QPu6aNwh{w4gE_{$~>1c+}|4XEYkc)DLliY1hjW%9;+6}ybD z9=~;@L0ps66vIU0)I?p2)Wk$x6C)!t-6V@-L){ch%j6^j19NkuG=ofruRL5}W7jb7 zU*li1nJ3{Czm_IYJ0mgLr8gD)<7fKH)6~$`z}VKn)Yib<*1*!%z}nWp*4Dt@*1*x$ Nz}eQowXA_#0RX7hJ5vAv delta 199 zcmZoTpx1CfZv$fhFaIS5CjK%8{w4fn{AH5`0>n3~2GsI%JXxkA#S+TNGI?XZie1K6 zkKYUo40@a_mS)L`X-TGOx(3O{X1XTHCMmj>hRG?qDM`r&DHh4bsfp&s8SkevaPeOQ z8r#IczlOhQGf%=Pel4JWEk17lkQQ(FUbTLVj518Z9YTU!Hr RTLVX117}+U*Rlp~1prZLJz)R< diff --git a/error.vue b/error.vue index cab5788..a630b98 100644 --- a/error.vue +++ b/error.vue @@ -1,16 +1,19 @@ \ No newline at end of file diff --git a/pages/explore/edit/index.vue b/pages/explore/edit/index.vue index 7e4f9c3..8b3c3f8 100644 --- a/pages/explore/edit/index.vue +++ b/pages/explore/edit/index.vue @@ -86,7 +86,7 @@ function push() } onMounted(async () => { - if(tree.value && container.value && await Content.ready) + if(tree.value && container.value) { const load = loading('normal'); tree.value.appendChild(load); @@ -100,7 +100,7 @@ onMounted(async () => { editor = new Editor(); - tree.value.replaceChild(editor.tree.container, load); + Content.ready.then(() => tree.value!.replaceChild(editor.tree.container, load)); container.value.appendChild(editor.container); } }); diff --git a/shared/content.util.ts b/shared/content.util.ts index facc0a7..83599fc 100644 --- a/shared/content.util.ts +++ b/shared/content.util.ts @@ -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 { - 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 | undefined + static async render(parent: HTMLElement, path: string): Promise | 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(content: LocalContent): 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 } = { //TODO: Edition link ]), ]), - render(content.content), + render(content.content, undefined, { class: 'pb-64' }), ]) }, renderEditor: (content) => { @@ -572,7 +577,7 @@ export const iconByType: Record = { export class Editor { - tree: TreeDOM; + tree!: TreeDOM; container: HTMLDivElement; selected?: Recursive; @@ -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 | 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 | undefined); } private contextmenu(e: MouseEvent, item: Recursive) {