Rewrote the server in cpp with the frontend in svelte

This commit is contained in:
2023-10-20 13:02:21 +02:00
commit 03b22ebb61
4168 changed files with 831370 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<script lang="ts">
import {rpc, show_working, state, token, type UploadFile} from '../store';
import {Button, ButtonGroup, Modal} from 'flowbite-svelte';
import {afterUpdate} from 'svelte';
let show_confirm = false;
let show_modal = false;
let pre_element: HTMLElement|null = null;
let text = '';
let nodes: number[] = [];
async function real_delete() {
show_confirm = false;
show_modal = true;
text = '';
show_working.set(true);
await new Promise<void>((resolve) => {
rpc.FS_delete_nodes($token ?? '', nodes, (v) => {
if (v == null)
resolve();
else {
text += v;
}
});
});
show_working.set(false);
show_modal = false;
state.update(v => v);
}
export const del = async (n: number[]) => {
nodes = n;
show_confirm = true;
};
afterUpdate(() => {
if (pre_element)
pre_element.scroll({ top: pre_element.scrollHeight, behavior: 'instant' });
});
</script>
<Modal bind:open={show_confirm} dismissable={false} title="Do you really want to delete these files?">
<ButtonGroup class="w-full flex flex-nowrap">
<Button class="flex-1" color="green" on:click={() => show_confirm = false}>No</Button>
<Button class="flex-1" color="red" on:click={real_delete}>Yes</Button>
</ButtonGroup>
</Modal>
<Modal bind:open={show_modal} dismissable={false} size="xl" title="Deleting" bodyClass="h-full p-0 space-y-0" class="h-screen-90">
<pre bind:this={pre_element} class="bg-gray-200 text-gray-600 px-4 py-2 h-full overflow-y-auto overscroll-contain rounded-b">{text}</pre>
</Modal>

View File

@@ -0,0 +1,146 @@
<script context="module" lang="ts">
import {writable} from 'svelte/store';
const show_preview = writable<boolean>(false);
</script>
<script lang="ts">
import {Checkbox, Dropdown, DropdownItem, Spinner, Table, TableBody, TableBodyCell, TableBodyRow, TableHead, TableHeadCell, Tooltip} from 'flowbite-svelte';
import {Folder, FolderParent, DocumentBlank, ChevronSortDown} from 'carbon-icons-svelte';
import {filesize} from 'filesize';
import {api, changeStateFunction, download, StateE, token, rpc} from '../store';
import LinkButton from './LinkButton.svelte';
import DeleteModal from './DeleteModal.svelte';
export let node: api.Node;
let selected: number[] = [];
let nodes: api.Node[], dirs: api.Node[], files: api.Node[], previews: {[key: number]: string|null} = {};
let total_size: number;
$: { nodes = node.children!; selectNone(); }
$: dirs = nodes.filter(v => !v.file).sort((a, b) => a.name.localeCompare(b.name));
$: files = nodes.filter(v => v.file).sort((a, b) => a.name.localeCompare(b.name));
$: total_size = files.map(v => v.size).reduce<number>((a, b) => (a + b!), 0);
$: {
if ($show_preview) {
for (const file of files) {
if (!file.preview) continue;
getPreview(file.id);
}
}
}
async function getPreview(node: number) {
const resp = await rpc.FS_download_preview($token ?? '', node);
if (resp.o == null)
return;
previews[node] = 'data:image/png;base64,' + resp.o;
previews = previews;
}
let ctx_node: api.Node;
let ctx_hidden = true;
let ctx_x = 0, ctx_y = 0;
let ctx_style: string;
$: ctx_style = `top: ${ctx_y}px; left: ${ctx_x}px; position: fixed;`;
function onCtxMenu(node: api.Node, e: MouseEvent) {
e.preventDefault();
if (!ctx_hidden)
return ctx_hidden = true;
ctx_x = e.pageX;
ctx_y = e.pageY;
ctx_node = node;
ctx_hidden = false;
}
const selectAll = () => selected = nodes.map(v => v.id);
const selectFolders = () => selected = dirs.map(v => v.id);
const selectFiles = () => selected = files.map(v => v.id);
const selectNone = () => selected = [];
const downloadSelected = () => download($token ?? '', nodes.filter(v => selected.includes(v.id)));
const deleteSelected = () => del(selected);
const onCtxDownload = () => download($token ?? '', [ctx_node]);
let del: (nodes: number[]) => Promise<void>;
const onCtxDelete = () => del([ctx_node.id]);
const onShowPreview = (e: Event) => { show_preview.set((e.target as HTMLInputElement).checked); }
</script>
<svelte:body on:click={() => (ctx_hidden = true)} />
<DeleteModal bind:del={del} />
<Table hoverable>
<TableHead theadClass="text-xs">
<TableHeadCell class="p-2 pl-4 w-0 h-0">
<ChevronSortDown id="dropdown-button" />
</TableHeadCell>
<TableHeadCell class="p-2 w-0"><Checkbox checked={$show_preview} on:change={onShowPreview} /><Tooltip>Show image previews</Tooltip></TableHeadCell>
<TableHeadCell>Name</TableHeadCell>
<TableHeadCell>Size</TableHeadCell>
</TableHead>
<TableBody>
{#if node.parent !== null}
<TableBodyRow>
<TableBodyCell class="!p-4"></TableBodyCell>
<TableBodyCell class="px-2 w-0"><FolderParent /></TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.parent ?? 0)}>..</LinkButton></TableBodyCell>
<TableBodyCell></TableBodyCell>
</TableBodyRow>
{/if}
{#each dirs as node}
<TableBodyRow on:contextmenu={onCtxMenu.bind(null, node)}>
<TableBodyCell class="p-2 pl-4 w-0 h-0"><Checkbox bind:group={selected} value={node.id}/></TableBodyCell>
<TableBodyCell class="px-2 w-0"><Folder /></TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.id)}>{node.name}</LinkButton></TableBodyCell>
<TableBodyCell></TableBodyCell>
</TableBodyRow>
{/each}
{#each files as node}
<TableBodyRow on:contextmenu={onCtxMenu.bind(null, node)}>
<TableBodyCell class="p-2 pl-4 w-0 h-0"><Checkbox bind:group={selected} value={node.id}/></TableBodyCell>
<TableBodyCell class="px-2 min-w-0">
{#if $show_preview && node.preview}
{#if previews[node.id] !== undefined}
<img class="w-screen max-w-xs" alt="preview" src={previews[node.id]} />
{:else}
<Spinner size="4"/>
{/if}
{:else}
<DocumentBlank />
{/if}
</TableBodyCell>
<TableBodyCell class="pl-0"><LinkButton on:click={changeStateFunction(StateE.VIEW, node.id)}>{node.name}</LinkButton></TableBodyCell>
<TableBodyCell>{filesize(node.size ?? 0, {base: 2, standard: 'jedec'})}</TableBodyCell>
</TableBodyRow>
{/each}
</TableBody>
<tfoot class="text-gray-700 bg-gray-50">
<tr>
<td class="px-6 py-3" colspan="3">
{#if selected.length > 0}
<LinkButton on:click={downloadSelected}>Download</LinkButton>
<LinkButton color="red" on:click={deleteSelected}>Delete</LinkButton>
{/if}
</td>
<td class="px-6 py-3">{filesize(total_size, {base: 2, standard: 'jedec'})}</td>
</tr>
</tfoot>
</Table>
<Dropdown triggeredBy="#dropdown-button" trigger="hover" placement="left">
<DropdownItem on:click={selectAll}>Select all</DropdownItem>
<DropdownItem on:click={selectFolders}>Select folders</DropdownItem>
<DropdownItem on:click={selectFiles}>Select files</DropdownItem>
<DropdownItem on:click={selectNone}>Select none</DropdownItem>
</Dropdown>
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div style={ctx_style} hidden={ctx_hidden} class="z-50 shadow-md rounded-lg border-gray-100 bg-white" on:contextmenu={() => (ctx_hidden = true)}>
<ul class="py-1">
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 w-full text-left" on:click={onCtxDownload}>Download</button></li>
<li><button class="font-medium py-2 px-4 text-sm hover:bg-gray-100 text-red-400 w-full text-left" on:click={onCtxDelete}>Delete</button></li>
</ul>
</div>

View File

@@ -0,0 +1,57 @@
<script lang="ts">
import {Button, Spinner} from 'flowbite-svelte';
import {Download} from 'carbon-icons-svelte';
import {api, rpc, token, workingWrapperR} from '../store';
import {onDestroy} from 'svelte';
export let node: api.Node;
let src = '';
let loading = false;
let mime: string|null;
let image: boolean, video: boolean, audio: boolean, pdf: boolean, can_display: boolean;
$: workingWrapperR<string>(() => rpc.FS_get_mime($token ?? '', node.id)).then(v => mime = v);
$: image = mime?.startsWith('image/') ?? false;
$: video = mime?.startsWith('video/') ?? false;
$: audio = mime?.startsWith('audio/') ?? false;
$: pdf = mime === 'application/pdf';
$: can_display = image || video || audio || pdf;
async function load() {
loading = true;
if (src.startsWith('blob'))
URL.revokeObjectURL(src);
const resp = await fetch('/download', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: `token=${$token ?? ''}&node=${node.id}`
});
if (resp.status != 200)
return;
src = URL.createObjectURL(await resp.blob());
loading = false;
}
$: if (image || pdf) load();
onDestroy(() => { if (src.startsWith('blob')) URL.revokeObjectURL(src); });
</script>
<Button class="w-full mb-6"><Download />Download</Button>
{#if can_display && !loading && src === ''}
<Button class="w-full" outline on:click={load}>Load</Button>
{:else if loading}
<Spinner class="w-full" />
{:else if can_display && src !== ''}
{#if image}
<img class="w-full" alt="img" src={src} />
{:else if video}
<!-- svelte-ignore a11y-media-has-caption -->
<video class="w-full" src={src} controls autoplay />
{:else if audio}
<audio class="w-full" src={src} controls autoplay />
{:else if pdf}
<embed class="w-full" style="height: 75vh" src={src} type="application/pdf" />
{/if}
{/if}

View File

@@ -0,0 +1,22 @@
<script>
import {twMerge} from "tailwind-merge";
export let color = 'primary';
</script>
<button class={twMerge('link-button transition-colors', `text-${color}-600`, `hover:text-${color}-400`, $$props.class)} on:click>
<slot>
<span class="text-primary-600 hover:text-primary-400"></span>
<span class="text-amber-600 hover:text-amber-400"></span>
<span class="text-red-600 hover:text-red-400"></span>
</slot>
</button>
<style>
.link-button {
background: none;
border: none;
padding: 0 0.25em;
cursor: pointer;
}
</style>

View File

@@ -0,0 +1,145 @@
<script lang="ts">
import {state, token, type UploadFile} from '../store';
import {Button, ButtonGroup, Modal, Progressbar} from 'flowbite-svelte';
import {filesize} from 'filesize';
interface MyFile extends UploadFile {
waiting: boolean,
done: boolean,
current: number,
total: number,
progress: number
}
let aborting = false;
let done = false;
let show_modal = false;
let files: MyFile[] = [];
let current = 0, total = 0;
let progress: number;
$: progress = total == 0 ? 0 : (current / total * 100);
let not_done_files: MyFile[], done_files: MyFile[];
$: not_done_files = files.filter(f => !f.done);
$: done_files = files.filter(f => f.done);
function close() {
show_modal = false;
state.update(v => v);
}
async function realUpload(file: MyFile) {
if (file.done)
return;
file.waiting = false;
let load_progress = 0;
await new Promise((resolve) => {
const xhr = new XMLHttpRequest();
xhr.onloadend = resolve;
xhr.onerror = resolve;
xhr.upload.onprogress = ev => {
current += ev.loaded - load_progress;
load_progress = ev.loaded;
file.current = ev.loaded;
file.progress = file.current / file.total * 100;
files = files;
if (file.current == file.total)
resolve(null);
};
xhr.open('POST', '/upload', true);
xhr.setRequestHeader('X-Node', file.id.toString());
xhr.setRequestHeader('X-Token', $token ?? '');
xhr.send(file.file);
});
current += file.total - load_progress;
file.done = true;
files = files;
}
export const upload = async (fs: UploadFile[]) => {
aborting = false;
done = false;
current = 0;
total = 0;
show_modal = true;
fs.forEach(f => (total += f.file.size));
files = fs.map(f => {
return {
...f,
waiting: f.file.size != 0,
done: f.file.size == 0,
current: 0,
total: f.file.size,
progress: 0
};
});
const in_flight = new Set();
for (const file of files) {
if (in_flight.size >= 15) {
await Promise.race(in_flight);
}
if (aborting)
break;
const p = realUpload(file);
in_flight.add(p);
const _ = (async () => { await p; in_flight.delete(p); })();
}
await Promise.all(in_flight);
done = true;
if (!aborting && !files.some(v => v.overwrite))
close();
};
</script>
<Modal bind:open={show_modal} dismissable={false} size="xl" title="Uploading">
<div>
<div class="mb-1 flex justify-between">
<span>{filesize(current, {base: 2, standard: 'jedec'})} / {filesize(total, {base: 2, standard: 'jedec'})}</span>
<span>{progress.toFixed(2)}%</span>
</div>
<Progressbar class="mt-0" size="h-4" bind:progress={progress} />
</div>
<table class="w-full">
<thead>
<tr>
<th>File</th>
<th>Status</th>
</tr>
</thead>
<tbody>
{#each not_done_files as file (file.id)}
<tr>
<td>{file.full_name}</td>
<td>
{#if file.waiting}
Waiting
{:else}
<div class="flex flex-nowrap flex-row">
<span class="mr-4">{filesize(file.current, {base: 2, standard: 'jedec'})} / {filesize(file.total, {base: 2, standard: 'jedec'})}</span>
<Progressbar class="flex-1" size="h-4" bind:progress={file.progress} labelInside />
</div>
{/if}
</td>
</tr>
{/each}
{#each done_files as file (file.id)}
<tr>
<td>{file.full_name}</td>
<td>Done {#if file.overwrite}(Overwrote old file){/if}</td>
</tr>
{/each}
</tbody>
</table>
<span slot="footer">
{#if done}
<Button class="w-full" on:click={close}>Close</Button>
{:else if !aborting}
<Button class="w-full" color="red" on:click={() => (aborting = true)}>Abort</Button>
{/if}
</span>
</Modal>