diff --git a/applications/uk.ewsgit.files/backend/index.ts b/applications/uk.ewsgit.files/backend/index.ts index 06b9ec7..3df1ab8 100644 --- a/applications/uk.ewsgit.files/backend/index.ts +++ b/applications/uk.ewsgit.files/backend/index.ts @@ -7,6 +7,7 @@ import { initTRPC } from "@trpc/server"; import { octetInputParser } from "@trpc/server/http"; import { randomUUIDv7 } from "bun"; import z from "zod"; +import fastFolderSize from "fast-folder-size/sync.js"; const log = instance.log.createLogger("uk.ewsgit.files"); @@ -384,9 +385,16 @@ const router = t.router({ } try { - const directoryContents = await fs.readdir(resolvedPath); + const directoryContents = await fs.readdir(resolvedPath, { withFileTypes: true }); - return { items: directoryContents, status: "ok" }; + directoryContents.sort((a, b) => { + if (a.isDirectory() && !b.isDirectory()) return -1; + if (!a.isDirectory() && b.isDirectory()) return 1; + + return a.name.localeCompare(b.name, undefined, { sensitivity: "base" }); + }); + + return { items: directoryContents.map((ent) => ent.name), status: "ok" }; } catch (_) { return { status: "invalid_path" }; } @@ -452,6 +460,16 @@ const router = t.router({ } }), }, + quota: procedure.output(z.object({ currentUsage: z.number(), maximum: z.number() })).query(async (opt) => { + const quotaMax = (await (await opt.ctx.user()).getQuota()) || 8000000000; + + const resp = { + maximum: Number(quotaMax), + currentUsage: fastFolderSize((await opt.ctx.user()).getPath()) || -1, + }; + + return resp; + }), }); export type TRPCRouter = typeof router; diff --git a/applications/uk.ewsgit.files/backend/package.json b/applications/uk.ewsgit.files/backend/package.json index 0849ec7..279de11 100644 --- a/applications/uk.ewsgit.files/backend/package.json +++ b/applications/uk.ewsgit.files/backend/package.json @@ -5,6 +5,7 @@ "private": true, "version": "0.0.1", "dependencies": { + "@onlineworkspace/workspace-instance": "workspace:*", "@trpc/server": "^11.6.0", "@types/bun": "^1.3.0", "@types/cookies": "^0.9.1", @@ -18,11 +19,11 @@ "dockerode": "^4.0.7", "dotenv": "^17.2.1", "express": "^5.1.0", + "fast-folder-size": "^2.4.0", "minimist": "^1.2.8", "sharp": "^0.34.3", "trpc-bun-adapter": "^1.3.0", "typescript": "^5.9.2", - "zod": "^4.0.17", - "@onlineworkspace/workspace-instance": "workspace:*" + "zod": "^4.0.17" } } diff --git a/applications/uk.ewsgit.files/electron/src/main.ts b/applications/uk.ewsgit.files/electron/src/main.ts index 970330d..16409ae 100644 --- a/applications/uk.ewsgit.files/electron/src/main.ts +++ b/applications/uk.ewsgit.files/electron/src/main.ts @@ -1,5 +1,6 @@ import path from "node:path"; import { app, BrowserWindow, ipcMain, session } from "electron"; +import { promises as fs } from "node:fs"; async function createWindow() { const window = new BrowserWindow({ @@ -25,6 +26,45 @@ async function createWindow() { return window; } +function getFileType(path: string) { + const extension = path.split(".").pop(); + + switch (extension) { + case "avif": + case "webp": + case "jpg": + case "jpeg": + case "png": + case "gif": + return "image"; + case "txt": + case "json": + case "js": + case "ts": + case "tsx": + case "scss": + case "sass": + case "yml": + case "yaml": + case "xml": + case "py": + case "toml": + case "rs": + case "html": + case "htm": + case "css": + case "jsm": + case "tsm": + case "tsc": + case "jsc": + case "sql": + case "lock": + return "plaintext"; + } + + return undefined; +} + app.whenReady().then(async () => { const window = await createWindow(); @@ -32,6 +72,43 @@ app.whenReady().then(async () => { ipcMain.on("maximize_window", () => (window.isMaximized() ? window.unmaximize() : window.maximize())); ipcMain.on("close_window", () => window.close()); + ipcMain.handle("fs:readdir", async (_, [p, options]: [string, any]) => { + const resolvedPath = path.resolve(p); + + try { + return { status: "ok", items: await fs.readdir(resolvedPath, options) }; + } catch (_) { + return { status: "missing_permission" }; + } + }); + + ipcMain.handle("files:get_entry", async (_, [p, thumbnailSize]: [string, number]) => { + const resolvedPath = path.resolve(p); + + try { + const itemStats = await fs.lstat(resolvedPath); + + return { + status: "ok", + data: { + path: p, + type: itemStats.isSymbolicLink() ? "link" : itemStats.isDirectory() ? "directory" : itemStats.isFile() ? "file" : "file", + // TODO: implement sharing first + shared: false, + size: itemStats.size, + thumbnail: + getFileType(p) === "image" + ? `data:image/${path.extname(resolvedPath)};base64,${await fs.readFile(resolvedPath, { encoding: "base64" })}` + : undefined, + }, + }; + } catch (_) { + return { + status: "invalid_path", + }; + } + }); + app.on("activate", () => { if (BrowserWindow.getAllWindows().length === 0) { createWindow(); diff --git a/applications/uk.ewsgit.files/electron/src/preload.js b/applications/uk.ewsgit.files/electron/src/preload.js index edbb558..c9623f6 100644 --- a/applications/uk.ewsgit.files/electron/src/preload.js +++ b/applications/uk.ewsgit.files/electron/src/preload.js @@ -1,15 +1,21 @@ -const { contextBridge, ipcRenderer } = require('electron/renderer') +const { contextBridge, ipcRenderer } = require("electron/renderer"); -contextBridge.exposeInMainWorld('electronAPI', { +contextBridge.exposeInMainWorld("electronAPI", { minimize_window: () => ipcRenderer.send("minimize_window"), maximize_window: () => ipcRenderer.send("maximize_window"), - close_window: () => ipcRenderer.send("close_window") -}) + close_window: () => ipcRenderer.send("close_window"), + fs: { + readdir: (path, options) => ipcRenderer.invoke("fs:readdir", [path, options]), + }, + files: { + get_entry: (path, thumbnailSize) => ipcRenderer.invoke("files:get_entry", [path, thumbnailSize]), + }, +}); localStorage.setItem("onlineworkspace_workspace_no_app_navigation_rail", "true"); localStorage.setItem("onlineworkspace_workspace_desktop_app", "true"); -document.addEventListener('DOMContentLoaded', () => { +document.addEventListener("DOMContentLoaded", () => { const elem = document.createElement("style"); elem.innerHTML = `:root { background-color: transparent !important; }`; @@ -20,5 +26,3 @@ document.addEventListener('DOMContentLoaded', () => { document.body.style.overflow = "hidden"; document.body.style.border = "1px solid #646464"; }); - - diff --git a/applications/uk.ewsgit.files/web/App.tsx b/applications/uk.ewsgit.files/web/App.tsx index 1b1c4d3..609c844 100644 --- a/applications/uk.ewsgit.files/web/App.tsx +++ b/applications/uk.ewsgit.files/web/App.tsx @@ -1,5 +1,5 @@ import {Route} from "@solidjs/router"; -import {type Accessor, type Component, createSignal, lazy, onMount} from "solid-js"; +import {type Accessor, type Component, createSignal, lazy, onMount, type Setter} from "solid-js"; import {createStore, type SetStoreFunction, type Store} from "solid-js/store"; import {AppContext} from "./appContext.ts"; import ActionBar from "./layout/components/ActionBar/ActionBar.tsx"; @@ -24,6 +24,7 @@ interface ViewState { viewItems: ViewItem[]; selectedItems: string[]; lastSelectionTime: number; + viewId: number; } interface AppContextType { @@ -35,9 +36,23 @@ interface AppContextType { setViewState: SetStoreFunction; deletedItemCount: number; isDesktopApp: boolean; + tasks: Accessor; + setTasks: Setter; } -export type {AppContextType}; +interface Task { + parent: `view${number}` | string, + max: number, + current: number, + // replaces %m with max and %c with current + message: string, + id: string, + + // Internal use only + invalid?: boolean +} + +export type {AppContextType, Task}; const App: Component = () => { const [ hasLoaded, setHasLoaded ] = createSignal(false); @@ -46,13 +61,15 @@ const App: Component = () => { homePath: "remote:/users/1/fs/", pinnedDirectories: [ "~/home" ], viewType: "details", - showPreview: true, + showPreview: false, }); const [ viewState, setViewState ] = createStore({ viewItems: [], selectedItems: [], lastSelectionTime: -1, + viewId: 0 }); + const [ taskStatus, setTaskStatus ] = createSignal([]) onMount(async () => { const userServerPreferences = await trpc.userPreferences.get.query(); @@ -77,7 +94,9 @@ const App: Component = () => { viewState: viewState, setViewState: setViewState, deletedItemCount: 24, - isDesktopApp: localStorage.getItem("onlineworkspace_workspace_desktop_app") === "true" + isDesktopApp: localStorage.getItem("onlineworkspace_workspace_desktop_app") === "true", + tasks: taskStatus, + setTasks: setTaskStatus }} > {props.children} diff --git a/applications/uk.ewsgit.files/web/layout/Layout.tsx b/applications/uk.ewsgit.files/web/layout/Layout.tsx index d9b35b1..16e5e57 100644 --- a/applications/uk.ewsgit.files/web/layout/Layout.tsx +++ b/applications/uk.ewsgit.files/web/layout/Layout.tsx @@ -67,6 +67,16 @@ const Layout: Component = (props) => { label: string; onClick(): void; }[]), + {type: "divider"}, + {type: "label", label: "Local"}, + { + type: "button", + icon: {type: "icon", value: FOLDER_ICON}, + label: "Local Root", + onClick() { + navigate(`/app/uk.ewsgit.files/dir?path=local:/`); + }, + }, {type: "margin"}, { type: "button", diff --git a/applications/uk.ewsgit.files/web/layout/components/Quota/Quota.tsx b/applications/uk.ewsgit.files/web/layout/components/Quota/Quota.tsx index a93093c..6bc2823 100644 --- a/applications/uk.ewsgit.files/web/layout/components/Quota/Quota.tsx +++ b/applications/uk.ewsgit.files/web/layout/components/Quota/Quota.tsx @@ -1,32 +1,26 @@ import UKLinearProgressIndicator from "@onlineworkspace/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.tsx"; import UKText from "@onlineworkspace/uikit-solid/src/components/text/UKText.tsx"; -import { type Component, createSignal, onCleanup, onMount } from "solid-js"; +import {type Component, createSignal, onCleanup, onMount} from "solid-js"; import styles from "./Quota.module.scss"; +import filesystemInterface from "../../../lib/filesystemInterface"; +import humanReadableSize from "../../../lib/humanReadableSize"; const Quota: Component = () => { - const [used, setUsed] = createSignal(0); + const [ used, setUsed ] = createSignal(0); + const [ maxUsed, setMaxUsed ] = createSignal(0); - onMount(() => { - const interval = setInterval(() => { - setUsed((u) => { - if (u + 1 > 10) { - return 0; - } else { - return u + 1; - } - }); - }, 500); + onMount(async () => { + const quota = await filesystemInterface.getQuota("remote") - onCleanup(() => { - clearInterval(interval); - }); + setUsed(quota.currentUsage) + setMaxUsed(quota.maximum) }); return (
- + - {used()} of 10GB Used + {humanReadableSize(used())} of {humanReadableSize(maxUsed())} Used
); diff --git a/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.module.scss b/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.module.scss index 990d3aa..ce5691b 100644 --- a/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.module.scss +++ b/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.module.scss @@ -15,3 +15,19 @@ .margin { margin-right: auto; } + +.taskStatus { + display: flex; + flex-direction: row; + gap: 0.5rem; + align-items: center; + text-wrap: nowrap; + flex-shrink: 0; + width: max-content; + opacity: 0; + transition: opacity 200ms var(--uk-sys-motion-easing-standard-accelerate); + + &.visible { + opacity: 1; + } +} diff --git a/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.tsx b/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.tsx index d6933af..bea378e 100644 --- a/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.tsx +++ b/applications/uk.ewsgit.files/web/layout/components/StatusBar/StatusBar.tsx @@ -1,30 +1,14 @@ import UKLinearProgressIndicator from "@onlineworkspace/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.tsx"; import UKText from "@onlineworkspace/uikit-solid/src/components/text/UKText.tsx"; -import { type Component, createEffect, createSignal, onCleanup, onMount, useContext } from "solid-js"; -import { AppContext } from "../../../appContext.ts"; +import {type Component, createEffect, createSignal, onCleanup, onMount, useContext} from "solid-js"; +import {AppContext} from "../../../appContext.ts"; import humanReadableSize from "../../../lib/humanReadableSize.ts"; import styles from "./StatusBar.module.scss"; +import clsx from "clsx"; const StatusBar: Component = () => { const appContext = useContext(AppContext); - const [statusProgression, setStatusProgression] = createSignal(0); - const [dirContents, setDirContents] = createSignal(""); - - onMount(() => { - const interval = setInterval(() => { - setStatusProgression((u) => { - if (u + 10 > 100) { - return 0; - } else { - return u + 10; - } - }); - }, 500); - - onCleanup(() => { - clearInterval(interval); - }); - }); + const [ dirContents, setDirContents ] = createSignal(""); createEffect(() => { const viewItems = appContext?.viewState.viewItems; @@ -62,19 +46,48 @@ const StatusBar: Component = () => { outputString += ` - Selected ${selectedItems.length} item${selectedItems.length !== 1 ? "s" : ""}`; } + if (outputString === "") { + outputString = "Empty directory" + } + setDirContents(outputString); }); + createEffect(() => { + for (const task of appContext?.tasks() || []) { + if (task.current === task.max) { + setTimeout(() => { + appContext?.setTasks(tasks => tasks.map(t => { + if (t.id === task.id) { + return { + ...t, + invalid: true + } + } + + return t + })) + + setTimeout(() => { + appContext?.setTasks(tasks => tasks.filter(t => t.id !== task.id)) + }, 250) + }, 1000) + } + } + }) + return (
{dirContents()}
- - - Status Message - +
+ + + {appContext!.tasks()[ 0 ]?.message.replaceAll("%c", appContext!.tasks()[ 0 ].current.toString()).replaceAll("%m", appContext!.tasks()[ 0 ].max.toString())} + +
); }; diff --git a/applications/uk.ewsgit.files/web/lib/chunk.ts b/applications/uk.ewsgit.files/web/lib/chunk.ts new file mode 100644 index 0000000..13c3da2 --- /dev/null +++ b/applications/uk.ewsgit.files/web/lib/chunk.ts @@ -0,0 +1,16 @@ +export default function chunkArray(inputArray: T[], multipleOf: number) { + const output: T[][] = [[]]; + + let currentIndex = 0; + + for (let i = 0; i < inputArray.length; i++) { + if (output[currentIndex].length === multipleOf) { + currentIndex++; + output.push([]); + } + + output[currentIndex].push(inputArray[i]); + } + + return output; +} diff --git a/applications/uk.ewsgit.files/web/lib/filesystemInterface.ts b/applications/uk.ewsgit.files/web/lib/filesystemInterface.ts index db3a91f..a3986c8 100644 --- a/applications/uk.ewsgit.files/web/lib/filesystemInterface.ts +++ b/applications/uk.ewsgit.files/web/lib/filesystemInterface.ts @@ -44,10 +44,20 @@ class FilesystemInterface { } if (parsedUrl.type === "local") { - // Do some electron things - return { status: "missing_permission" as const }; + // @ts-ignore + const readDirectoryResponse = await window.electronAPI.fs.readdir(parsedUrl.path); + + if (readDirectoryResponse.status === "ok") { + return { + status: readDirectoryResponse.status, + items: readDirectoryResponse.items.map((newItem: string) => { + return `${parsedUrl.type}:${path.join(parsedUrl.path, newItem)}` as UniformResourceLocator; + }), + }; + } else { + return { status: readDirectoryResponse.status }; + } } else { - console.log(parsedUrl.path); const readDirectoryResponse = await trpc.readDirectory.query({ path: parsedUrl.path }); if (readDirectoryResponse.status === "ok") { @@ -71,8 +81,20 @@ class FilesystemInterface { } if (parsedUrl.type === "local") { - // Do some electron things - return { status: "missing_permission" as const }; + // @ts-ignore + const getEntryResponse = await window.electronAPI.files.get_entry(parsedUrl.path, viewType === "details" ? 32 : 128); + + if (getEntryResponse.status === "ok") { + return { + status: getEntryResponse.status, + data: { + ...getEntryResponse.data, + path: `${parsedUrl.type}:${getEntryResponse.data.path}`, + }, + }; + } else { + return { status: getEntryResponse.status }; + } } else { const getEntryResponse = await trpc.view.getEntry.query({ path: parsedUrl.path, thumbnailSize: viewType === "details" ? 32 : 128 }); @@ -90,6 +112,14 @@ class FilesystemInterface { } } + async getQuota(location: "local" | "remote") { + if (location === "local") return { maximum: -1, currentUsage: -1 }; + + const quota = await trpc.quota.query(); + + return quota; + } + joinUrls(...urls: UniformResourceLocator[]) { const firstUrlType = this.urlToPath(urls[0]).type; @@ -116,7 +146,7 @@ class FilesystemInterface { if (url.startsWith("local:")) { return { type: "local", - path: url.substring(7), + path: url.substring(6), }; } else if (url.startsWith("remote:")) { return { diff --git a/applications/uk.ewsgit.files/web/pages/dir/View.module.scss b/applications/uk.ewsgit.files/web/pages/dir/View.module.scss index 3099d4c..9543dfd 100644 --- a/applications/uk.ewsgit.files/web/pages/dir/View.module.scss +++ b/applications/uk.ewsgit.files/web/pages/dir/View.module.scss @@ -5,6 +5,7 @@ } .dragSelectRegion { + pointer-events: none; background: rgb(var(--uk-sys-color-primary), 0.5); border: 1px solid rgb(var(--uk-sys-color-primary), 0.25); position: fixed; diff --git a/applications/uk.ewsgit.files/web/pages/dir/View.tsx b/applications/uk.ewsgit.files/web/pages/dir/View.tsx index 48a2b4d..fbc1d8a 100644 --- a/applications/uk.ewsgit.files/web/pages/dir/View.tsx +++ b/applications/uk.ewsgit.files/web/pages/dir/View.tsx @@ -1,5 +1,5 @@ import {useSearchParams} from "@solidjs/router"; -import {type Component, createEffect, Match, Switch, useContext} from "solid-js"; +import {type Component, createEffect, createSignal, Match, Switch, useContext} from "solid-js"; import {createStore} from "solid-js/store"; import {AppContext} from "../../appContext.ts"; import trpc from "../../lib/trpc.ts"; @@ -7,6 +7,9 @@ import DetailsView from "./components/DetailsView/DetailsView.tsx"; import GridView from "./components/GridView/GridView.tsx"; import styles from "./View.module.scss"; import filesystemInterface, {type UniformResourceLocator} from "../../lib/filesystemInterface.ts"; +import chunkArray from "../../lib/chunk.ts"; +import type {ViewItem} from "./viewItem.ts"; +import type {Task} from "../../App.tsx"; const View: Component = () => { const [ searchParams, setSearchParams ] = useSearchParams<{path?: UniformResourceLocator}>(); @@ -19,6 +22,7 @@ const View: Component = () => { origin: undefined, size: undefined, }); + const [ errorMessage, setErrorMessage ] = createSignal(undefined) createEffect(async () => { if (!searchParams.path) { @@ -28,137 +32,180 @@ const View: Component = () => { const newItems = await filesystemInterface.readDirectory(searchParams.path || "remote:/") - console.log({path: searchParams.path, newItems}) - if (newItems.status === "ok") { + setErrorMessage(undefined) + appContext?.setViewState("selectedItems", []); appContext?.setViewState("lastSelectionTime", -1); appContext?.setViewState("viewItems", []); - for (const itemPath of newItems.items) { - filesystemInterface.getViewEntry(itemPath, appContext!.userPreferences.viewType === "details" ? "details" : "grid").then((viewEntry) => { - if (viewEntry.status === "ok") { - appContext?.setViewState("viewItems", [ ...appContext.viewState.viewItems, viewEntry.data ]); + const task: Task = { + parent: "view0", + id: crypto.randomUUID(), + max: newItems.items.length, + current: 0, + message: `Fetched %c of %m items` + } + + appContext?.setTasks(tasks => [ ...tasks, task ]) + + const CHUNK_SIZE = 10 + for (const itemPathGroup of chunkArray(newItems.items, CHUNK_SIZE)) { + console.log(itemPathGroup) + + await new Promise(async (resolve) => { + const itemGroupResponsePromises = []; + for (const itemPath of itemPathGroup) { + itemGroupResponsePromises.push(filesystemInterface.getViewEntry(itemPath, appContext!.userPreferences.viewType === "details" ? "details" : "grid").then((viewEntry) => { + if (viewEntry.status === "ok") { + return viewEntry + } + })) + } + appContext?.setViewState("viewItems", [ ...appContext.viewState.viewItems, ...(await Promise.all(itemGroupResponsePromises)).map(ig => ig?.data || undefined).filter(ig => ig !== undefined) ]); + if (!(task.current + CHUNK_SIZE > task.max)) { + task.current += CHUNK_SIZE + } else { + task.current = task.max } + appContext?.setTasks(tasks => { + return tasks.map(t => { + if (task.id === t.id) { + return task + } + + return t + }) + }) + + console.log(appContext?.tasks()) + + resolve(); }) } - + } else { + setErrorMessage(newItems.status) } }); return ( - /** biome-ignore lint/a11y/noStaticElementInteractions: button functionality not required */ -
{ - if (e.currentTarget === e.target) appContext!.setViewState("selectedItems", []); - }} - // onMouseDown={(downEvent) => { - // document.body.style.userSelect = "none"; - // setDragSelectRegion("origin", {x: downEvent.clientX, y: downEvent.clientY}); - // setDragSelectRegion("transOrigin", {x: downEvent.clientX, y: downEvent.clientY}); - - // const currentTarget = downEvent.currentTarget as HTMLDivElement; - // const currentTargetBounds = currentTarget.getBoundingClientRect(); - // const selectableItems = currentTarget.querySelectorAll("[data-fs-item-path]"); - - // function mouseUp() { - // document.body.style.userSelect = "unset"; - - // setDragSelectRegion("origin", undefined); - // setDragSelectRegion("size", undefined); - // setDragSelectRegion("transOrigin", undefined); - - // document.removeEventListener("mouseup", mouseUp); - // document.removeEventListener("mousemove", mouseMove); - // } - - // let itemInRegionCalculationTimeout: NodeJS.Timeout | undefined; - - // function itemInRegionCalculation() { - // appContext?.setViewState("selectedItems", []); - - // for (const item of selectableItems) { - // const boundingRect = item.getBoundingClientRect(); - - // const tl1 = {x: dragSelectRegion.transOrigin?.x || 0, y: dragSelectRegion.transOrigin?.y || 0}; - // const br1 = {x: tl1.x + (dragSelectRegion.size?.x || 0), y: tl1.y + (dragSelectRegion.size?.y || 0)}; - - // const tl2 = {x: boundingRect.left, y: boundingRect.top}; - // const br2 = {x: boundingRect.right, y: boundingRect.bottom}; - - // if (tl1.x > br2.x || tl2.x > br1.x) continue; - - // if (tl1.y > br2.y || tl2.y > br1.y) continue; - - // const itemPath = item.getAttribute("data-fs-item-path"); - // if (!itemPath) return; - // if (!appContext?.viewState.selectedItems.includes(itemPath)) { - // appContext?.setViewState("selectedItems", [ ...appContext.viewState.selectedItems, itemPath ]); - // } - // } - // } - - // function mouseMove(e: MouseEvent) { - // if (!dragSelectRegion.origin) return; - // if (!dragSelectRegion.transOrigin) return; - - // const mouseX = Math.min(Math.max(e.clientX, currentTargetBounds.left), currentTargetBounds.right); - // const mouseY = Math.min(Math.max(e.clientY, currentTargetBounds.top), currentTargetBounds.bottom); - - // let sizeX = 0; - // let sizeY = 0; - - // sizeX = mouseX - dragSelectRegion.origin.x; - // sizeY = mouseY - dragSelectRegion.origin.y; - - // if (mouseX < dragSelectRegion.origin.x) { - // setDragSelectRegion("transOrigin", {x: mouseX, y: dragSelectRegion.transOrigin.y}); - // sizeX = dragSelectRegion.origin.x - mouseX; - // } - - // if (mouseY < dragSelectRegion.origin.y) { - // setDragSelectRegion("transOrigin", {x: dragSelectRegion.transOrigin.x, y: mouseY}); - // sizeY = dragSelectRegion.origin.y - mouseY; - // } - - // setDragSelectRegion("size", { - // x: sizeX, - // y: sizeY, - // }); - - // if (itemInRegionCalculationTimeout) clearTimeout(itemInRegionCalculationTimeout); - - // itemInRegionCalculationTimeout = setTimeout(() => { - // itemInRegionCalculation(); - // }, 2); - // } - - // document.addEventListener("mouseup", mouseUp); - // document.addEventListener("mousemove", mouseMove); - // }} - > - - - - - - - - Gallery View - - {dragSelectRegion.transOrigin !== undefined ? ( + <> + {errorMessage() ?
{errorMessage()}
: <> + {/** biome-ignore lint/a11y/noStaticElementInteractions: button functionality not required */}
{ + if (e.currentTarget === e.target) appContext!.setViewState("selectedItems", []); }} - /> - ) : null} -
+ onMouseDown={(downEvent) => { + document.body.style.userSelect = "none"; + setDragSelectRegion("origin", {x: downEvent.clientX, y: downEvent.clientY}); + setDragSelectRegion("transOrigin", {x: downEvent.clientX, y: downEvent.clientY}); + + const currentTarget = downEvent.currentTarget as HTMLDivElement; + const currentTargetBounds = currentTarget.getBoundingClientRect(); + const selectableItems = currentTarget.querySelectorAll("[data-fs-item-path]"); + + function mouseUp() { + document.body.style.userSelect = "unset"; + + setDragSelectRegion("origin", undefined); + setDragSelectRegion("size", undefined); + setDragSelectRegion("transOrigin", undefined); + + document.removeEventListener("mouseup", mouseUp); + document.removeEventListener("mousemove", mouseMove); + } + + let itemInRegionCalculationTimeout: NodeJS.Timeout | undefined; + + function itemInRegionCalculation() { + appContext?.setViewState("selectedItems", []); + + for (const item of selectableItems) { + const boundingRect = item.getBoundingClientRect(); + + const tl1 = {x: dragSelectRegion.transOrigin?.x || 0, y: dragSelectRegion.transOrigin?.y || 0}; + const br1 = {x: tl1.x + (dragSelectRegion.size?.x || 0), y: tl1.y + (dragSelectRegion.size?.y || 0)}; + + const tl2 = {x: boundingRect.left, y: boundingRect.top}; + const br2 = {x: boundingRect.right, y: boundingRect.bottom}; + + if (tl1.x > br2.x || tl2.x > br1.x) continue; + + if (tl1.y > br2.y || tl2.y > br1.y) continue; + + const itemPath = item.getAttribute("data-fs-item-path"); + if (!itemPath) return; + if (!appContext?.viewState.selectedItems.includes(itemPath)) { + appContext?.setViewState("selectedItems", [ ...appContext.viewState.selectedItems, itemPath ]); + } + } + } + + function mouseMove(e: MouseEvent) { + if (!dragSelectRegion.origin) return; + if (!dragSelectRegion.transOrigin) return; + + const mouseX = Math.min(Math.max(e.clientX, currentTargetBounds.left), currentTargetBounds.right); + const mouseY = Math.min(Math.max(e.clientY, currentTargetBounds.top), currentTargetBounds.bottom); + + let sizeX = 0; + let sizeY = 0; + + sizeX = mouseX - dragSelectRegion.origin.x; + sizeY = mouseY - dragSelectRegion.origin.y; + + if (mouseX < dragSelectRegion.origin.x) { + setDragSelectRegion("transOrigin", {x: mouseX, y: dragSelectRegion.transOrigin.y}); + sizeX = dragSelectRegion.origin.x - mouseX; + } + + if (mouseY < dragSelectRegion.origin.y) { + setDragSelectRegion("transOrigin", {x: dragSelectRegion.transOrigin.x, y: mouseY}); + sizeY = dragSelectRegion.origin.y - mouseY; + } + + setDragSelectRegion("size", { + x: sizeX, + y: sizeY, + }); + + if (itemInRegionCalculationTimeout) clearTimeout(itemInRegionCalculationTimeout); + + itemInRegionCalculationTimeout = setTimeout(() => { + itemInRegionCalculation(); + }, 2); + } + + document.addEventListener("mouseup", mouseUp); + document.addEventListener("mousemove", mouseMove); + }} + > + + + + + + + + Gallery View + + {dragSelectRegion.transOrigin !== undefined ? ( +
+ ) : null} +
+ + } + ); }; diff --git a/bun.lock b/bun.lock index 6befecd..15640bf 100644 --- a/bun.lock +++ b/bun.lock @@ -117,6 +117,7 @@ "dockerode": "^4.0.7", "dotenv": "^17.2.1", "express": "^5.1.0", + "fast-folder-size": "^2.4.0", "minimist": "^1.2.8", "sharp": "^0.34.3", "trpc-bun-adapter": "^1.3.0", @@ -1193,7 +1194,7 @@ "adm-zip": ["adm-zip@0.5.17", "", {}, "sha512-+Ut8d9LLqwEvHHJl1+PIHqoyDxFgVN847JTVM3Izi3xHDWPE4UtzzXysMZQs64DMcrJfBeS/uoEP4AD3HQHnQQ=="], - "agent-base": ["agent-base@4.3.0", "", { "dependencies": { "es6-promisify": "^5.0.0" } }, "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg=="], + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], "agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="], @@ -1695,7 +1696,7 @@ "http2-wrapper": ["http2-wrapper@1.0.3", "", { "dependencies": { "quick-lru": "^5.1.1", "resolve-alpn": "^1.0.0" } }, "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg=="], - "https-proxy-agent": ["https-proxy-agent@2.2.4", "", { "dependencies": { "agent-base": "^4.3.0", "debug": "^3.1.0" } }, "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg=="], + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], "humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="], @@ -1863,7 +1864,7 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + "make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="], "make-fetch-happen": ["make-fetch-happen@10.2.1", "", { "dependencies": { "agentkeepalive": "^4.2.1", "cacache": "^16.1.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^5.0.0", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^7.7.1", "minipass": "^3.1.6", "minipass-collect": "^1.0.2", "minipass-fetch": "^2.0.3", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.3", "promise-retry": "^2.0.1", "socks-proxy-agent": "^7.0.0", "ssri": "^9.0.0" } }, "sha512-NgOPbRiaQM10DYXvN3/hhGVI2M5MtITFryzBGxHM5p4wnFxsVCbxkrBrDsk+EZ5OB4jEOT7AjDxtdF+KVEFT7w=="], @@ -2631,6 +2632,8 @@ "@mapbox/node-pre-gyp/https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], + "@mapbox/node-pre-gyp/make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + "@mapbox/node-pre-gyp/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "@npmcli/move-file/rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], @@ -2691,6 +2694,8 @@ "@tensorflow/tfjs-layers/@tensorflow/tfjs-core": ["@tensorflow/tfjs-core@4.22.0", "", { "dependencies": { "@types/long": "^4.0.1", "@types/offscreencanvas": "~2019.7.0", "@types/seedrandom": "^2.4.28", "@webgpu/types": "0.1.38", "long": "4.0.0", "node-fetch": "~2.6.1", "seedrandom": "^3.0.5" } }, "sha512-LEkOyzbknKFoWUwfkr59vSB68DMJ4cjwwHgicXN0DUi3a0Vh1Er3JQqCI1Hl86GGZQvY8ezVrtDIvqR1ZFW55A=="], + "@tensorflow/tfjs-node/https-proxy-agent": ["https-proxy-agent@2.2.4", "", { "dependencies": { "agent-base": "^4.3.0", "debug": "^3.1.0" } }, "sha512-OmvfoQ53WLjtA9HeYP9RNrWMJzzAz1JGaSFr1nijg0PVR1JaD/xbJq1mdEIIlxGpXp9eSe/O2LgU9DJmTPd0Eg=="], + "@tybys/wasm-util/tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], "@types/jsdom/undici-types": ["undici-types@7.24.7", "", {}, "sha512-XA+gOBkzYD3C74sZowtCLTpgtaCdqZhqCvR6y9LXvrKTt/IVU6bz49T4D+BPi475scshCCkb0IklJRw6T1ZlgQ=="], @@ -2729,8 +2734,6 @@ "clone-response/mimic-response": ["mimic-response@1.0.1", "", {}, "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ=="], - "decompress/make-dir": ["make-dir@1.3.0", "", { "dependencies": { "pify": "^3.0.0" } }, "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ=="], - "decompress-tar/tar-stream": ["tar-stream@1.6.2", "", { "dependencies": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", "end-of-stream": "^1.0.0", "fs-constants": "^1.0.0", "readable-stream": "^2.3.0", "to-buffer": "^1.1.1", "xtend": "^4.0.0" } }, "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A=="], "decompress-tarbz2/file-type": ["file-type@6.2.0", "", {}, "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg=="], @@ -2775,8 +2778,6 @@ "external-editor/iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], - "fast-folder-size/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -2787,10 +2788,6 @@ "get-package-info/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], - "http-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - - "https-proxy-agent/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], - "inquirer/ansi-escapes": ["ansi-escapes@1.4.0", "", {}, "sha512-wiXutNjDUlNEDWHcYH3jtZUhd3c4/VojassD8zHdHCY13xbZy2XbW+NKQwA0tWGBVzDA9qEzYwfoSsWmviidhw=="], "inquirer/chalk": ["chalk@1.1.3", "", { "dependencies": { "ansi-styles": "^2.2.1", "escape-string-regexp": "^1.0.2", "has-ansi": "^2.0.0", "strip-ansi": "^3.0.0", "supports-color": "^2.0.0" } }, "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A=="], @@ -2801,8 +2798,6 @@ "inquirer/string-width": ["string-width@1.0.2", "", { "dependencies": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", "strip-ansi": "^3.0.0" } }, "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw=="], - "jsdom/https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], - "jsdom/parse5": ["parse5@8.0.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-9m4m5GSgXjL4AjumKzq1Fgfp3Z8rsvjRNbnkVwfu2ImRqE5D0LnY2QfDen18FSY9C573YU5XxSapdHZTZ2WolA=="], "log-symbols/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], @@ -2811,7 +2806,7 @@ "log-update/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], - "make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], "make-fetch-happen/http-proxy-agent": ["http-proxy-agent@5.0.0", "", { "dependencies": { "@tootallnate/once": "2", "agent-base": "6", "debug": "4" } }, "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w=="], @@ -3007,6 +3002,8 @@ "@mapbox/node-pre-gyp/https-proxy-agent/agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="], + "@mapbox/node-pre-gyp/make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "@tensorflow/tfjs-backend-cpu/@tensorflow/tfjs-core/@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], "@tensorflow/tfjs-backend-cpu/@tensorflow/tfjs-core/long": ["long@4.0.0", "", {}, "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="], @@ -3047,6 +3044,10 @@ "@tensorflow/tfjs-layers/@tensorflow/tfjs-core/seedrandom": ["seedrandom@3.0.5", "", {}, "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="], + "@tensorflow/tfjs-node/https-proxy-agent/agent-base": ["agent-base@4.3.0", "", { "dependencies": { "es6-promisify": "^5.0.0" } }, "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg=="], + + "@tensorflow/tfjs-node/https-proxy-agent/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="], + "@tensorflow/tfjs/@tensorflow/tfjs-core/@types/offscreencanvas": ["@types/offscreencanvas@2019.7.3", "", {}, "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="], "@tensorflow/tfjs/@tensorflow/tfjs-core/@types/seedrandom": ["@types/seedrandom@2.4.34", "", {}, "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A=="], @@ -3091,8 +3092,6 @@ "decompress-tar/tar-stream/readable-stream": ["readable-stream@2.3.8", "", { "dependencies": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", "isarray": "~1.0.0", "process-nextick-args": "~2.0.0", "safe-buffer": "~5.1.1", "string_decoder": "~1.1.1", "util-deprecate": "~1.0.1" } }, "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA=="], - "decompress/make-dir/pify": ["pify@3.0.0", "", {}, "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg=="], - "electron-installer-debian/yargs/cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], "electron-installer-debian/yargs/y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], @@ -3123,8 +3122,6 @@ "execa/cross-spawn/which": ["which@1.3.1", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "which": "./bin/which" } }, "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ=="], - "fast-folder-size/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], "fs-promise/fs-extra/jsonfile": ["jsonfile@2.4.0", "", { "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw=="], @@ -3141,8 +3138,6 @@ "inquirer/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@1.0.0", "", { "dependencies": { "number-is-nan": "^1.0.0" } }, "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw=="], - "jsdom/https-proxy-agent/agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], - "log-symbols/chalk/supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], "log-update/ansi-escapes/type-fest": ["type-fest@1.4.0", "", {}, "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA=="], diff --git a/instance/src/systems/filesystem.ts b/instance/src/systems/filesystem.ts index f971e18..7b48b28 100644 --- a/instance/src/systems/filesystem.ts +++ b/instance/src/systems/filesystem.ts @@ -3,7 +3,7 @@ import fs from "fs"; import path from "path"; import type { Instance } from "../index.js"; import System from "../system.js"; -import {WorkspacesEvent} from "./events.js"; +import { WorkspacesEvent } from "./events.js"; export default class FilesystemSystem extends System { readonly SRC_ROOT = path.resolve("./instance/src/"); @@ -80,6 +80,7 @@ export default class FilesystemSystem extends System { case "jpg": case "jpeg": case "png": + case "gif": return "image"; case "txt": case "json": @@ -195,9 +196,9 @@ export default class FilesystemSystem extends System { async startup(): Promise { this.instance.sys.event.on(WorkspacesEvent.BeforeStartupComplete, async () => { - const db = this.instance.sys.database.postgres(); + const db = this.instance.sys.database.postgres(); - await db`CREATE TABLE IF NOT EXISTS filesystem_permission_overrides ( + await db`CREATE TABLE IF NOT EXISTS filesystem_permission_overrides ( file_path TEXT, recursive BOOL, read_permission_groups TEXT[], @@ -206,7 +207,7 @@ export default class FilesystemSystem extends System { write_permission_users TEXT[], owner TEXT[] )`; - }) + }); return true; } diff --git a/instance/src/systems/users.ts b/instance/src/systems/users.ts index 8bff235..b50dd35 100644 --- a/instance/src/systems/users.ts +++ b/instance/src/systems/users.ts @@ -139,7 +139,7 @@ export class WorkspacesUser { } /** - Sets the user's quota to the provided quota + Sets the user's quota to the provided quota in bits @returns `false` - failed to set the quota @returns `true` - successfully set the quota */ @@ -156,7 +156,7 @@ export class WorkspacesUser { } /** - Get the user's quota + Get the user's quota in bits @returns `number` - the users quota @returns `undefined` - could not get the user's quota */ @@ -488,7 +488,7 @@ export default class UsersSystem extends System { surname TEXT DEFAULT '', gender TEXT DEFAULT 'other', bio TEXT, - storage_quota BIGINT DEFAULT 10, + storage_quota BIGINT DEFAULT 8000000000, email TEXT, is_administrator BOOL DEFAULT FALSE, is_email_verified BOOL DEFAULT FALSE, diff --git a/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.module.scss b/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.module.scss index c3f3e46..c3cb3e5 100644 --- a/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.module.scss +++ b/uikit-solid/src/components/linearProgressIndicator/UKLinearProgressIndicator.module.scss @@ -4,7 +4,8 @@ display: flex; flex-direction: row; - width: 100%; + max-width: 100%; + min-width: 10rem; border-radius: 100rem; height: var(--track-height); } diff --git a/uikit-solid/src/components/sideBar/UKSideBar.module.scss b/uikit-solid/src/components/sideBar/UKSideBar.module.scss index a546b8c..67a0708 100644 --- a/uikit-solid/src/components/sideBar/UKSideBar.module.scss +++ b/uikit-solid/src/components/sideBar/UKSideBar.module.scss @@ -20,6 +20,7 @@ .mobileToggleButton { width: max-content; + -webkit-app-region: no-drag; > div { rotate: 0deg;