diff --git a/composables/useDatabase.ts b/composables/useDatabase.ts index d64ff95..622aa6a 100644 --- a/composables/useDatabase.ts +++ b/composables/useDatabase.ts @@ -2,7 +2,9 @@ import { Database } from "bun:sqlite"; import { BunSQLiteDatabase, drizzle } from "drizzle-orm/bun-sqlite"; import * as schema from '../db/schema'; -let instance: BunSQLiteDatabase; +let instance: BunSQLiteDatabase & { + $client: Database; +}; export default function useDatabase() { if(!instance) @@ -13,6 +15,7 @@ export default function useDatabase() instance.run("PRAGMA journal_mode = WAL;"); instance.run("PRAGMA foreign_keys = true;"); + instance.run("PRAGMA optimize=0x10002;"); } return instance; diff --git a/db.sqlite b/db.sqlite index 10bf560..e801226 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index c97dcb9..944e302 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index dff6b90..0c6d66e 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/server/api/file/content/[id].get.ts b/server/api/file/content/[id].get.ts index d729636..8a59e69 100644 --- a/server/api/file/content/[id].get.ts +++ b/server/api/file/content/[id].get.ts @@ -35,7 +35,7 @@ export default defineEventHandler(async (e) => { return data.content; } - return; + return null; } catch(_e) { diff --git a/server/middleware/compress.ts b/server/middleware/compress.ts index 91ce94d..58af3c3 100644 --- a/server/middleware/compress.ts +++ b/server/middleware/compress.ts @@ -16,5 +16,10 @@ export default defineEventHandler(async (event) => { //@ts-expect-error _end.call(event.node.res, await Bun.zstdCompress(buffer), ...args); } + else + { + //@ts-expect-error + _end.call(event.node.res, body, ...args); + } }; }); \ No newline at end of file diff --git a/server/tasks/pull.ts b/server/tasks/pull.ts index adca089..5fcd95b 100644 --- a/server/tasks/pull.ts +++ b/server/tasks/pull.ts @@ -105,8 +105,8 @@ function reshapeContent(content: string, type: FileType): string | null return content; case "canvas": const data = JSON.parse(content) as CanvasContent; - data.edges?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); }); - data.nodes?.forEach(e => { console.log(e.color); e.color = typeof e.color === 'string' ? getColor(e.color) : undefined; console.log(e.color); }); + data.edges?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined); + data.nodes?.forEach(e => e.color = typeof e.color === 'string' ? getColor(e.color) : undefined); return JSON.stringify(data); default: case 'folder': diff --git a/shared/auth.util.ts b/shared/auth.util.ts index 94017a0..9d5e462 100644 --- a/shared/auth.util.ts +++ b/shared/auth.util.ts @@ -2,7 +2,7 @@ export function hasPermissions(userPermissions: string[], neededPermissions: str { for(let i = 0; i < neededPermissions.length; i++) { - const list = neededPermissions[i].split(' '); + const list = neededPermissions[i]!.split(' '); if(list.every(e => userPermissions.includes(e))) { diff --git a/shared/canvas.util.ts b/shared/canvas.util.ts index 8a6d9e3..5217e74 100644 --- a/shared/canvas.util.ts +++ b/shared/canvas.util.ts @@ -173,7 +173,6 @@ export class Node extends EventTarget { border: `border-light-40 dark:border-dark-40`, bg: `bg-light-40 dark:bg-dark-40` } } } - export class NodeEditable extends Node { edges: Set = new Set(); @@ -440,6 +439,7 @@ export class Canvas mount() { const dragMove = (e: MouseEvent) => { + e.preventDefault(); this.dragMove(e); }; const dragEnd = (e: MouseEvent) => { @@ -515,6 +515,7 @@ export class Canvas this.container.removeEventListener('touchmove', touchmove); }; const touchmove = (e: TouchEvent) => { + e.preventDefault(); const pos = center(e.touches); this._x = this.visualX = this._x - (this.lastX - pos.x) / this._zoom; this._y = this.visualY = this._y - (this.lastY - pos.y) / this._zoom; diff --git a/shared/components.util.ts b/shared/components.util.ts index 204fb70..9349301 100644 --- a/shared/components.util.ts +++ b/shared/components.util.ts @@ -502,32 +502,61 @@ export function tabgroup(tabs: Array<{ id: string, title: NodeChildren, content: }) return container as HTMLDivElement & { refresh: () => void }; } -export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none' }) +export function floater(container: HTMLElement, content: NodeChildren | (() => NodeChildren), settings?: { class?: Class, position?: Placement, pinned?: boolean, cover?: 'width' | 'height' | 'all' | 'none', events?: { show: Array, hide: Array, onshow?: (this: HTMLElement) => boolean, onhide?: (this: HTMLElement) => boolean }, title?: string }) { let viewport = document.getElementById('mainContainer') ?? undefined; + let diffX, diffY, targetWidth, targetHeight; const dragstart = (e: MouseEvent) => { e.preventDefault(); e.stopImmediatePropagation(); window.addEventListener('mousemove', dragmove); window.addEventListener('mouseup', dragend); + + const box = floating.content.getBoundingClientRect(); + diffX = e.clientX - box.x; + diffY = e.clientY - box.y; }; + const resizestart = (e: MouseEvent) => { + e.preventDefault(); + e.stopImmediatePropagation(); + window.addEventListener('mousemove', resizemove); + window.addEventListener('mouseup', resizeend); + }; + const dragmove = (e: MouseEvent) => { const box = floating.content.getBoundingClientRect(); - const viewbox = viewport?.getBoundingClientRect(); - box.x = clamp(box.x + e.movementX, viewbox?.left ?? 0, (viewbox?.right ?? Infinity) - box.width); - box.y = clamp(box.y + e.movementY, viewbox?.top ?? 0, (viewbox?.bottom ?? Infinity) - box.height); + const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight }; + box.x = clamp(e.clientX - diffX!, viewbox?.left ?? 0, viewbox.right - box.width); + box.y = clamp(e.clientY - diffY!, viewbox?.top ?? 0, viewbox.bottom - box.height); Object.assign(floating.content.style, { left: `${box.x}px`, top: `${box.y}px`, - }) + }); }; + const resizemove = (e: MouseEvent) => { + const box = floating.content.getBoundingClientRect(); + const viewbox = viewport?.getBoundingClientRect() ?? { x: 0, y: 0, width: window.innerWidth, height: window.innerHeight, left: 0, right: window.innerWidth, top: 0, bottom: window.innerHeight }; + box.width = clamp(e.clientX - box.x, 200, Math.min(750, viewbox.right - box.x)); + box.height = clamp(e.clientY - box.y, 150, Math.min(750, viewbox.bottom - box.y)); + + Object.assign(floating.content.style, { + width: `${box.width}px`, + height: `${box.height}px`, + }); + }; + const dragend = (e: MouseEvent) => { e.preventDefault(); window.removeEventListener('mousemove', dragmove); window.removeEventListener('mouseup', dragend); }; + const resizeend = (e: MouseEvent) => { + e.preventDefault(); + window.removeEventListener('mousemove', resizemove); + window.removeEventListener('mouseup', resizeend); + }; const floating = popper(container, { arrow: true, @@ -535,18 +564,37 @@ export function floater(container: HTMLElement, content: NodeChildren | (() => N offset: 12, cover: settings?.cover, placement: settings?.position, - class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45]', - content: () => [ settings?.pinned !== undefined ? dom('div', { class: 'hidden group-data-[pinned]:flex flex-row gap-4 justify-end border-b border-light-35 dark:border-dark-35 cursor-move', listeners: { mousedown: dragstart } }, [ tooltip(icon('radix-icons:cross-1', { width: 12, height: 12, listeners: { click: (e) => { e.preventDefault(); floating.hide(); } }, class: 'p-1 cursor-pointer' }), 'Fermer', 'right') ]) : undefined, div('min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] w-full overflow-auto box-content', typeof content === 'function' ? content() : content) ], + class: 'bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 group-data-[pinned]:bg-light-15 dark:group-data-[pinned]:bg-dark-15 group-data-[pinned]:border-light-50 dark:group-data-[pinned]:border-dark-50 text-light-100 dark:text-dark-100 z-[45] relative group-data-[pinned]:h-full', + content: () => [ settings?.pinned !== undefined ? div('hidden group-data-[pinned]:flex flex-row justify-end border-b border-light-35 dark:border-dark-35', [ dom('span', { class: 'w-full cursor-move text-xs', listeners: { mousedown: dragstart }, text: settings?.title }), tooltip(dom('div', { class: 'cursor-pointer flex', listeners: { mousedown: (e) => { e.stopImmediatePropagation(); floating.hide(); } } }, [icon('radix-icons:cross-1', { width: 12, height: 12, class: 'p-1' })]), 'Fermer', 'right') ]) : undefined, div('h-full group-data-[pinned]:h-[calc(100%-21px)] w-full min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px] group-data-[pinned]:min-h-[initial] group-data-[pinned]:min-w-[initial] group-data-[pinned]:max-h-[initial] group-data-[pinned]:max-w-[initial] overflow-auto box-content', typeof content === 'function' ? content() : content), dom('span', { class: 'hidden group-data-[pinned]:flex absolute bottom-0 right-0 cursor-nw-resize z-50', listeners: { mousedown: resizestart } }, [ icon('ph:notches', { width: 12, height: 12 }) ]) ], viewport }); if(settings?.pinned === false) + { floating.content.addEventListener('click', () => { + if(floating.content.hasAttribute('data-pinned')) + return; + + const box = floating.content.children.item(0)!.getBoundingClientRect(); + Object.assign(floating.content.style, { + width: `${box.width + 21}px`, + height: `${box.height + 21}px`, + }); floating.stop(); + floating.content.addEventListener('mousedown', function() { + if(!floating.content.hasAttribute('data-pinned')) + return; + this.parentElement?.appendChild(this); - }, { passive: true, capture: true }) - }, { once: true }); + }, { passive: true }); + }); + } + else + { + floating.stop(); + floating.show(); + } return container; } diff --git a/shared/content.util.ts b/shared/content.util.ts index 630c3f7..7319b24 100644 --- a/shared/content.util.ts +++ b/shared/content.util.ts @@ -124,6 +124,9 @@ export class Content if(Content._ready) return Promise.resolve(true); + if(Content.initPromise) + return Content.initPromise; + Content.initPromise = new Promise(async (res) => { try { diff --git a/shared/dom.util.ts b/shared/dom.util.ts index 987c526..b1105ba 100644 --- a/shared/dom.util.ts +++ b/shared/dom.util.ts @@ -116,9 +116,6 @@ export interface IconProperties rotate?: number|string; style?: Record | string; class?: Class; - listeners?: { - [K in keyof HTMLElementEventMap]?: Listener - }; } const iconCache: Map = new Map(); export function icon(name: string, properties?: IconProperties): HTMLElement @@ -160,17 +157,6 @@ export function icon(name: string, properties?: IconProperties): HTMLElement else for(const [k, v] of Object.entries(properties.style)) if(v !== undefined) element.attributeStyleMap.set(k, v); } - if(properties?.listeners) - { - for(let [k, v] of Object.entries(properties.listeners)) - { - const key = k as keyof HTMLElementEventMap, value = v as Listener; - if(typeof value === 'function') - element.addEventListener(key, value.bind(element)); - else if(value) - element.addEventListener(key, value.listener.bind(element), value.options); - } - } return element; } diff --git a/shared/floating.util.ts b/shared/floating.util.ts index ead1ae7..8fb5808 100644 --- a/shared/floating.util.ts +++ b/shared/floating.util.ts @@ -21,9 +21,12 @@ export interface PopperProperties extends FloatingProperties { content?: NodeChildren | (() => NodeChildren); delay?: number; - - onShow?: () => boolean | void; - onHide?: () => boolean | void; + events?: { + show: Array; + hide: Array; + onshow?: () => boolean; + onhide?: () => boolean; + }; } export interface ModalProperties @@ -36,13 +39,13 @@ export interface ModalProperties let teleport: HTMLDivElement; export function init() { - teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0' }); + teleport = dom('div', { attributes: { id: 'popper-container' }, class: 'absolute top-0 left-0 z-40' }); document.body.appendChild(teleport); } export function popper(container: HTMLElement, properties?: PopperProperties) { - let shown = false, manualStop = false, timeout: Timer; + let state: 'shown' | 'showing' | 'hidden' | 'hiding' | 'pinned' = 'hidden', manualStop = false, timeout: Timer; const arrow = svg('svg', { class: ' group-data-[pinned]:hidden absolute fill-light-35 dark:fill-dark-35', attributes: { width: "12", height: "8", viewBox: "0 0 20 10" } }, [svg('polygon', { attributes: { points: "0,0 20,0 10,10" } })]); const content = dom('div', { class: properties?.class, style: properties?.style }); const floater = dom('div', { class: 'fixed hidden group', attributes: { 'data-state': 'closed' } }, [ content, properties?.arrow ? arrow : undefined ]); @@ -86,23 +89,23 @@ export function popper(container: HTMLElement, properties?: PopperProperties) right: 'left', bottom: 'top', left: 'right', - }[side]!; + }[side]!; - const rotation = { + const rotation = { top: "0", bottom: "180", left: "270", right: "90" - }[side]!; + }[side]!; - Object.assign(arrow.style, { + Object.assign(arrow.style, { left: arrowX != null ? `${arrowX}px` : '', top: arrowY != null ? `${arrowY}px` : '', right: '', bottom: '', [staticSide]: `-7px`, transform: `rotate(${rotation}deg)`, - }); + }); } }); } @@ -110,7 +113,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties) let _stop: () => void | undefined, empty = true; function show() { - if(shown || !properties?.onShow || properties?.onShow() !== false) + if(state !== 'shown' && state !== 'showing' && state !== 'pinned' && (!properties?.events?.onshow || properties?.events?.onshow() !== false)) { if(typeof properties?.content === 'function') properties.content = properties.content(); @@ -122,9 +125,10 @@ export function popper(container: HTMLElement, properties?: PopperProperties) } clearTimeout(timeout); - + state = 'showing'; + timeout = setTimeout(() => { - if(!shown) + if(state !== 'shown') { teleport!.appendChild(floater); @@ -132,6 +136,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties) floater.classList.toggle('hidden', false); update(); + _stop && _stop(); _stop = FloatingUI.autoUpdate(container, floater, update, { animationFrame: true, layoutShift: false, @@ -139,35 +144,39 @@ export function popper(container: HTMLElement, properties?: PopperProperties) ancestorScroll: false, ancestorResize: false, }); + console.log("Starting", floater); } - shown = true; + state = 'shown'; }, properties?.delay ?? 0); } } function hide() { - if(!manualStop && (!properties?.onHide || properties?.onHide() !== false)) + if(state !== 'hiding' && state !== 'pinned' && (!properties?.events?.onhide || properties?.events?.onhide() !== false)) { clearTimeout(timeout); + state = 'hiding'; timeout = setTimeout(() => { floater.remove(); _stop && _stop(); + console.log('Stoping', floater); floater.setAttribute('data-state', 'closed'); floater.classList.toggle('hidden', true); - shown = false; - }, shown ? properties?.delay ?? 0 : 0); + state = 'hidden'; + }, properties?.delay ?? 0); } } function start() { - manualStop = false; + state = 'hidden'; floater.toggleAttribute('data-pinned', false); update(); + _stop && _stop(); _stop = FloatingUI.autoUpdate(container, floater, update, { animationFrame: true, layoutShift: false, @@ -175,33 +184,28 @@ export function popper(container: HTMLElement, properties?: PopperProperties) ancestorScroll: false, ancestorResize: false, }); + console.log('Starting', floater); } function stop() { - manualStop = true; + state = 'pinned'; floater.toggleAttribute('data-pinned', true); _stop && _stop(); + console.log('Stoping', floater); clearTimeout(timeout); } function link(element: HTMLElement) { - Object.entries({ - 'mouseenter': show, - 'mousemove': show, - 'mouseleave': hide, - 'focus': show, - 'blur': hide, - } as Record void>).forEach(([event, listener]) => { - element.addEventListener(event, listener); - }); + (properties?.events?.show ?? ['mouseenter', 'mousemove', 'focus']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, show)); + (properties?.events?.hide ?? ['mouseleave', 'blur']).forEach((e: keyof HTMLElementEventMap) => element.addEventListener(e, hide)); } link(container); link(floater); return { container, content: floater, stop, start, show: () => { - if(!shown) + if(state !== 'shown') { teleport!.appendChild(floater); @@ -210,10 +214,11 @@ export function popper(container: HTMLElement, properties?: PopperProperties) update(); } - shown = true; + state = 'shown'; }, hide: () => { floater.remove(); _stop && _stop(); + console.log('Stoping', floater); floater.setAttribute('data-state', 'closed'); floater.classList.toggle('hidden', true); @@ -221,7 +226,7 @@ export function popper(container: HTMLElement, properties?: PopperProperties) manualStop = false; floater.toggleAttribute('data-pinned', false); - shown = false; + state = 'hidden'; } }; } export function followermenu(target: FloatingUI.ReferenceElement, content: NodeChildren, properties?: FollowerProperties) diff --git a/shared/proses.ts b/shared/proses.ts index f53a417..6516c48 100644 --- a/shared/proses.ts +++ b/shared/proses.ts @@ -41,7 +41,7 @@ export const a: Prose = { { const canvas = new Canvas((_content as LocalContent<'canvas'>).content); queueMicrotask(() => canvas.mount()); - return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]); + return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); } return div(''); })).current], { position: 'bottom-start', pinned: false }) : element; @@ -55,41 +55,32 @@ export const preview: Prose = { const overview = Content.getFromPath(pathname === '' && hash.length > 0 ? unifySlug(router.currentRoute.value.params.path ?? '') : pathname); - const el = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [ + const element = dom('span', { class: ['cursor-pointer text-accent-blue inline-flex items-center', properties?.class] }, [ ...(children ?? []), overview && overview.type !== 'markdown' ? icon(iconByType[overview.type], { class: 'w-4 h-4 inline-block', inline: true }) : undefined ]); const magicKeys = useMagicKeys(); - return overview ? popper(el, { - arrow: true, - delay: 150, - offset: 12, - cover: "height", - placement: 'bottom-start', - class: ['data-[side=bottom]:animate-slideUpAndFade data-[side=right]:animate-slideLeftAndFade data-[side=left]:animate-slideRightAndFade data-[side=top]:animate-slideDownAndFade w-[300px] bg-light-10 dark:bg-dark-10 border border-light-35 dark:border-dark-35 data-[state=open]:transition-transform text-light-100 dark:text-dark-100 w-full z-[45]', - { 'min-w-[200px] min-h-[150px] max-w-[600px] max-h-[600px]': !properties?.size || properties.size === 'large', 'max-w-[400px] max-h-[250px]': properties.size === 'small' } - ], - content: () => { - return [async('large', Content.getContent(overview.id).then((_content) => { - if(_content?.type === 'markdown') - { - return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); - } - if(_content?.type === 'canvas') - { - const canvas = new Canvas((_content as LocalContent<'canvas'>).content); - queueMicrotask(() => canvas.mount()); - return dom('div', { class: 'w-[600px] h-[600px] relative' }, [canvas.container]); - } - return div(''); - })).current]; - }, - onShow() { - if(!magicKeys.current.has('control') || magicKeys.current.has('meta')) - return false; - }, - }).container : el; + return !!overview ? floater(element, () => [async('large', Content.getContent(overview.id).then((_content) => { + if(_content?.type === 'markdown') + { + return render((_content as LocalContent<'markdown'>).content ?? '', hash.length > 0 ? hash.substring(1) : undefined, { class: 'w-full max-h-full overflow-auto py-4 px-6' }); + } + if(_content?.type === 'canvas') + { + const canvas = new Canvas((_content as LocalContent<'canvas'>).content); + queueMicrotask(() => canvas.mount()); + return dom('div', { class: 'w-[600px] h-[600px] group-data-[pinned]:h-full group-data-[pinned]:w-full h-[600px] relative w-[600px] relative' }, [canvas.container]); + } + return div(''); + })).current], { position: 'bottom-start', pinned: false, + events: { + show: ['mouseenter', 'mousemove'], + hide: ['mouseleave'], + onshow() { + return !magicKeys.current.has('control') || !magicKeys.current.has('meta'); + } + }, }) : element; } } export const callout: Prose = {