improve folder view - test with display-api

This commit is contained in:
E44
2025-10-30 08:59:22 +01:00
parent d92d8abc77
commit 9df03b6ccb
20 changed files with 1067 additions and 197 deletions
+6 -6
View File
@@ -1155,9 +1155,9 @@
}
},
"node_modules/@sveltejs/kit": {
"version": "2.46.4",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.46.4.tgz",
"integrity": "sha512-J1fd80WokLzIm6EAV7z7C2+/C02qVAX645LZomARARTRJkbbJSY1Jln3wtBZYibUB8c9/5Z6xqLAV39VdbtWCQ==",
"version": "2.47.3",
"resolved": "https://registry.npmjs.org/@sveltejs/kit/-/kit-2.47.3.tgz",
"integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==",
"dev": true,
"license": "MIT",
"peer": true,
@@ -4018,9 +4018,9 @@
"license": "MIT"
},
"node_modules/vite": {
"version": "7.1.9",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.9.tgz",
"integrity": "sha512-4nVGliEpxmhCL8DslSAUdxlB6+SMrhB0a1v5ijlh1xB1nEPuy1mxaHxysVucLHuWryAxLWg6a5ei+U4TLn/rFg==",
"version": "7.1.11",
"resolved": "https://registry.npmjs.org/vite/-/vite-7.1.11.tgz",
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
"dev": true,
"license": "MIT",
"peer": true,
+1 -1
View File
@@ -6,7 +6,7 @@
@theme {
--color-stone-150: oklch(0.9465 0.002 77.571);
--color-stone-250: oklch(0.896 0.004 52.542);
--color-stone-350: oklch(0.711 0.009 57.218);
--color-stone-350: oklch(0.789 0.0075 56.313);
--color-stone-450: oklch(0.631 0.0115 57.165);
--color-stone-550: oklch(0.4985 0.012 65.855);
--color-stone-650: oklch(0.409 0.0105 70.599);
@@ -13,6 +13,8 @@
title = '',
click_function = (e: MouseEvent) => {},
menu_options = null,
menu_class = 'right-0',
div_class = '',
children
} = $props<{
className?: string;
@@ -23,6 +25,8 @@
title?: string;
click_function?: (e: MouseEvent) => void;
menu_options?: MenuOption[] | null;
menu_class?: string;
div_class?: string,
children?: any;
}>();
@@ -126,7 +130,7 @@
}
</script>
<div class="relative min-w-0 flex" {title}>
<div class="relative min-w-0 flex {div_class}" {title}>
<button
bind:this={button_element}
class="{className} {menu_shown ? hover_bg : bg} {disabled
@@ -150,7 +154,7 @@
transition:fade={{ duration: 50 }}
class="absolute {position_bottom
? 'top-full'
: 'bottom-full'} right-0 z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
: 'bottom-full'} {menu_class} z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
onclick={(e) => {
e.stopPropagation();
}}
@@ -175,7 +179,7 @@
{/if}
</div>
{/if}
<div class="pr-2">
<div class="truncate min-w-0" title={option.name}>
{option.name}
</div>
</button>
@@ -7,16 +7,14 @@
import {
add_empty_display_group,
all_displays_of_group_selected,
is_selected,
remove_empty_display_groups,
select,
select_all_of_group,
selected_display_ids,
set_new_display_group_data
} from '../ts/stores/displays';
import DNDGrip from './DNDGrip.svelte';
import { fade } from 'svelte/transition';
import type { DisplayGroup } from '../ts/types';
import { selected_display_ids } from '../ts/stores/select';
let { display_group } = $props<{
display_group: DisplayGroup;
@@ -1,16 +1,15 @@
<script lang="ts">
import Button from './Button.svelte';
import { is_selected, select, selected_display_ids } from '../ts/stores/displays';
import {
display_screen_height,
current_height,
get_selectable_color_classes,
pinned_display_id
} from '../ts/stores/ui_behavior';
import DNDGrip from './DNDGrip.svelte';
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
import { fade } from 'svelte/transition';
import OnlineState from './OnlineState.svelte';
import type { Display } from '../ts/types';
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
let { display } = $props<{
display: Display;
@@ -19,7 +18,7 @@
let hovering_unselectable = $state(false);
function onclick(e: Event) {
select(display.id);
select(selected_display_ids, display.id);
e.stopPropagation();
}
@@ -45,7 +44,7 @@
hover: true,
active: !hovering_unselectable,
text: true
})} rounded-xl flex flex-row justify-between h-{$display_screen_height} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
})} rounded-xl flex flex-row justify-between h-{$current_height.display} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
>
<div class="flex flex-row gap-4 min-w-0 flex-1">
<!-- Left Preview Screen -->
@@ -1,48 +0,0 @@
<script lang="ts">
import { CloudAlert, Folder, RefreshCcwDot, Shield } from 'lucide-svelte';
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
import Button from './Button.svelte';
let {} = $props<{}>();
</script>
<div
class="p-1 {get_selectable_color_classes(false, {
bg: true,
hover: true,
active: true,
text: true
})} h-10 rounded-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group"
>
<div class="flex flex-row gap-2 min-w-0">
<div class="aspect-square rounded-md flex justify-center items-center">
<Folder />
</div>
<div class="content-center truncate select-none">
Tolle
Dateifjisodfhsuidfhdufighdfiughdfuighdfuihguifdghiudfhguidfgdfijghdfjkghdfkjghdfkjghdfkughdfjghdfkjghdfkjghfdkghdfkjghdfkghdfkghdfkjghdfkghdfkjghdfgkhdfkghfdkjghdfkghfdkghdfkjghdfjkghdk.zip
</div>
</div>
<div class="flex flex-row gap-1 pr-1 text-stone-400 group-hover:text-stone-300 group-active:text-stone-250 duration-200 transition-colors">
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
bg="bg-transparent"
hover_bg={get_selectable_color_classes(false, {
bg: true
})}
active_bg={get_selectable_color_classes(false, {
bg: true
})}
click_function={(e) => {
e.stopPropagation();
}}
>
<RefreshCcwDot class="size-full" />
</Button>
<div class="content-center select-none text-xs whitespace-nowrap">25.09.25</div>
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
<div class="content-center select-none text-xs whitespace-nowrap">MPEG</div>
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
<div class="content-center select-none text-xs whitespace-nowrap">523 MB</div>
</div>
</div>
@@ -0,0 +1,223 @@
<script lang="ts">
import { ArrowRight, Folder, Play, RefreshCcwDot, TriangleAlert } from 'lucide-svelte';
import {
current_height,
get_selectable_color_classes,
get_shifted_color
} from '../ts/stores/ui_behavior';
import Button from './Button.svelte';
import { supported_file_types, type FolderElement, type SupportedFileType } from '../ts/types';
import {
is_selected,
select,
selected_display_ids,
selected_file_ids
} from '../ts/stores/select';
import {
all_files,
change_file_path,
current_file_path,
get_display_ids_where_file_is_missing
} from '../ts/stores/files';
import RefreshPlay from './RefreshPlay.svelte';
let { file } = $props<{ file: FolderElement }>();
const is_folder = file.type === 'inode/directory';
function get_file_type(file: FolderElement): SupportedFileType | null {
for (const key of Object.keys(supported_file_types)) {
if (file.type === supported_file_types[key].mime_type) {
return supported_file_types[key];
}
}
// Fallback:
const extension = file.name.split('.').pop();
if (extension) {
if (Object.keys(supported_file_types).includes('.' + extension)) {
return supported_file_types['.' + extension];
}
}
return null;
}
function get_created_string(date_object: Date, full_string = false) {
if (full_string) {
return (
get_formated_date_string(date_object, true) + ' ' + get_formated_time_string(date_object)
);
} else if (date_object.toDateString() === new Date().toDateString()) {
return get_formated_time_string(date_object);
} else {
return get_formated_date_string(date_object);
}
}
function get_formated_time_string(date_object: Date) {
return `${date_object.getHours().toString().padStart(2, '0')}:${date_object.getMinutes().toString().padStart(2, '0')}`;
}
function get_formated_date_string(date_object: Date, full_year = false) {
return `${date_object.getDate().toString().padStart(2, '0')}.${(date_object.getMonth() + 1).toString().padStart(2, '0')}.${date_object
.getFullYear()
.toString()
.slice(full_year ? 0 : 2)}`;
}
function get_grayed_out_text_color_strings(is_selected: boolean): string {
const color = is_selected ? 'text-stone-600' : 'text-stone-400';
const factor = is_selected ? -1 : 1;
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
}
function get_grayed_out_border_color_strings(is_selected: boolean): string {
const color = is_selected ? 'border-stone-450' : 'border-stone-550';
const factor = is_selected ? 1 : 1;
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
}
function onclick(e: Event) {
select(selected_file_ids, file.id);
e.stopPropagation();
}
function open() {
if (is_folder) {
change_file_path($current_file_path + file.name + '/');
} else {
// TODO
}
}
</script>
<div class="flex flex-row h-{$current_height.file} w-full">
<div class="h-{$current_height.file} aspect-square max-w-15 flex">
<Button
className="flex rounded-l-lg rounded-r-none {is_folder
? 'text-stone-450'
: 'text-stone-800'} w-full"
div_class="w-full"
bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
-50
)}
hover_bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
50
)}
active_bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
100
)}
click_function={(e) => {
open();
e.stopPropagation();
}}
>
{#if is_folder}
<ArrowRight class="size-full" strokeWidth="3" />
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
<RefreshPlay className="size-full" />
{:else}
<Play class="size-full" strokeWidth="3" />
{/if}
</Button>
</div>
<div
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onclick(e);
}}
{onclick}
class="{get_selectable_color_classes(is_selected(file.id, $selected_file_ids), {
bg: true,
hover: true,
active: true,
text: true
})} rounded-r-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group w-full h-full min-w-0"
>
<div class="flex flex-row gap-2 min-w-0 w-full">
<div class="aspect-square rounded-md flex justify-center items-center p-2">
{#if is_folder}
<Folder class="size-full" />
{:else if file.thumbnail}
<div></div>
{:else if get_file_type(file)?.icon}
{@const Icon = get_file_type(file)?.icon}
<Icon class="size-full" />
{/if}
</div>
<div class="content-center truncate select-none w-full" title={file.name}>
{file.name.includes('.') ? file.name.slice(0, file.name.lastIndexOf('.')) : file.name}
</div>
</div>
<div
class=" p-1 flex flex-row items-center gap-1 pr-1 {get_grayed_out_text_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors"
>
{#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0}
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5 text-stone-100"
bg="bg-red-500"
click_function={(e) => {
e.stopPropagation();
}}
>
<TriangleAlert class="size-full" />
</Button>
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
bg="bg-transparent"
hover_bg={get_selectable_color_classes(false, {
bg: true
})}
active_bg={get_selectable_color_classes(false, {
bg: true
})}
click_function={(e) => {
e.stopPropagation();
}}
>
<RefreshCcwDot class="size-full" />
</Button>
{/if}
<div
class="w-14 content-center text-center select-none text-xs whitespace-nowrap"
title={get_created_string(file.date_created, true)}
>
{get_created_string(file.date_created)}
</div>
<div
class="h-[70%] border {get_grayed_out_border_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors my-1"
></div>
<div
class="w-12 content-center text-center select-none text-xs whitespace-nowrap truncate"
title={file.type}
>
{is_folder ? 'Ordner' : (get_file_type(file)?.display_name ?? '?')}
</div>
<div
class="h-[70%] border {get_grayed_out_border_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors"
></div>
<div class="w-12 content-center text-center select-none text-xs whitespace-nowrap">
{file.size}
</div>
</div>
</div>
</div>
@@ -0,0 +1,160 @@
<script lang="ts">
import { ChevronRight, House } from 'lucide-svelte';
import Button from './Button.svelte';
import { onDestroy, onMount } from 'svelte';
import type { MenuOption } from '../ts/types';
import { change_file_path, current_file_path } from '../ts/stores/files';
import { fade } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { cubicOut } from 'svelte/easing';
let { bg = 'bg-stone-700' } = $props<{
bg?: string;
}>();
let outside_container: HTMLDivElement;
let inside_container: HTMLDivElement;
let resize_observer: ResizeObserver;
let w10_div: HTMLDivElement;
let cut_folders: number = $state(0);
// let folders_length: number = $state(get_folders($current_file_path).length);
// let all_folders_length: number = $state(get_folders($current_file_path).length);
function get_folders(path: string): string[] {
path = path.slice(1); // Cut first '/'
if (path === '') return [];
if (path.endsWith('/')) {
path = path.slice(0, path.length - 1);
}
return path.split('/');
}
function get_sliced_folders(path: string, cut_folders: number): string[] {
const folders = get_folders(path);
let sliced_folders: string[] = [];
if (cut_folders !== get_folders($current_file_path).length)
sliced_folders = folders.slice(cut_folders);
return sliced_folders;
}
function get_hidden_folders(path: string, cut_folders: number): string[] {
const folders = get_folders(path);
let hidden_folders: string[] = [];
if (cut_folders !== get_folders($current_file_path).length)
hidden_folders = folders.slice(0, cut_folders);
return hidden_folders;
}
function recalc() {
if (!outside_container || !inside_container || !w10_div) return;
const first_shrink = cut_folders === 0;
const second_last_grow = cut_folders === 2;
const difference = outside_container.offsetWidth - inside_container.offsetWidth;
const w10_px = parseFloat(getComputedStyle(w10_div).width);
if ((!first_shrink && difference < 2 * w10_px) || (first_shrink && difference < w10_px)) {
if (cut_folders < get_folders($current_file_path).length) {
cut_folders += first_shrink ? 2 : 1;
}
} else if (
(!second_last_grow && difference > 6 * w10_px) ||
(second_last_grow && difference > 7 * w10_px)
) {
if (cut_folders >= 1) {
if (second_last_grow) {
cut_folders -= 2;
} else {
cut_folders -= 1;
}
}
}
}
function get_hidden_menu_options(hidden_folders: string[], path: string): MenuOption[] {
const out: MenuOption[] = [];
for (let i = 0; i < hidden_folders.length; i++) {
out.push({
name: '  '.repeat(i) + hidden_folders[i],
class: 'truncate max-w-80',
on_select: () => {
open_path(i + 1, path);
}
});
}
return out;
}
function open_path(index_of_all_folders: number, path: string) {
let new_path = '/';
const all_folders = get_folders(path);
for (let i = 0; i < index_of_all_folders; i++) {
new_path += all_folders[i] + '/';
}
change_file_path(new_path);
}
onMount(() => {
resize_observer = new ResizeObserver(() => recalc());
resize_observer.observe(outside_container);
resize_observer.observe(inside_container);
// initial
setTimeout(recalc, 0);
});
onDestroy(() => resize_observer?.disconnect());
</script>
<div class="{bg} rounded-xl flex" bind:this={outside_container}>
<div class="flex flex-row">
<div class="flex flex-row">
<div bind:this={w10_div} class="flex">
<Button
className="py-1 shrink-0 grow-0 w-10"
{bg}
click_function={(e) => {
open_path(0, $current_file_path);
}}
>
<House class="size-full" />
</Button>
</div>
{#if cut_folders !== 0}
<Button
className="pl-0 py-1 grow"
{bg}
menu_options={get_hidden_menu_options(
get_hidden_folders($current_file_path, cut_folders),
$current_file_path
)}
menu_class="left-0"
>
<ChevronRight class="shrink-0 text-stone-500 h-full" />
<span class="font-bold" title="Weiteren Pfad zeigen"> ... </span>
</Button>
{/if}
</div>
<div class="flex flex-row" bind:this={inside_container}>
{#each get_sliced_folders($current_file_path, cut_folders) as folder, i (i)}
<div animate:flip={{ duration: 100, easing: cubicOut }}>
<Button
className="shrink-0 py-1 pl-0 {i ===
get_sliced_folders($current_file_path, cut_folders).length - 1
? 'max-w-80 font-bold'
: 'max-w-30'}"
{bg}
click_function={(e) => {
open_path(cut_folders + i + 1, $current_file_path);
}}
>
<ChevronRight class="shrink-0 text-stone-500 h-full" />
<span class="truncate" title={folder}>
{folder}
</span>
</Button>
</div>
{/each}
</div>
</div>
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
let {className = ''} = $props<{className?: string}>();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.7"
stroke-linecap="round"
stroke-linejoin="round"
class="{className} lucide lucide-refresh-ccw-dot-icon lucide-refresh-ccw-dot"
>
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
<path d="M16 16h5v5" />
<path
d="M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"
transform="translate(0,2.0)"
/>
</svg>
+77 -46
View File
@@ -7,10 +7,12 @@
ClipboardPaste,
Download,
FolderOutput,
FolderPlus,
Info,
Keyboard,
Menu,
Minus,
Pen,
Pencil,
PinOff,
Plus,
@@ -33,13 +35,13 @@
import Button from '../components/Button.svelte';
import SplashScreen from './../../../../shared/splash_screen.html?raw';
import {
change_display_screen_height,
display_screen_height,
change_height,
current_height,
dnd_flip_duration_ms,
get_selectable_color_classes,
is_display_drag,
is_group_drag,
next_step_possible,
next_height_step_size,
pinned_display_id
} from '../ts/stores/ui_behavior';
import { dragHandleZone } from 'svelte-dnd-action';
@@ -47,9 +49,7 @@
all_displays_of_group_selected,
displays,
get_display_by_id,
is_selected,
select_all_of_group,
selected_display_ids
select_all_of_group
} from '../ts/stores/displays';
import { cubicOut } from 'svelte/easing';
import { flip } from 'svelte/animate';
@@ -58,7 +58,11 @@
import OnlineState from '../components/OnlineState.svelte';
import type { DisplayGroup } from '../ts/types';
import PopUp from '../components/PopUp.svelte';
import FileObject from '../components/FileObject.svelte';
import FileObject from '../components/FolderElementObject.svelte';
import FolderElementObject from '../components/FolderElementObject.svelte';
import PathBar from '../components/PathBar.svelte';
import { all_files, current_file_path, get_current_folder_elements } from '../ts/stores/files';
import { selected_display_ids, selected_file_ids } from '../ts/stores/select';
let displays_scroll_box: HTMLElement;
@@ -93,17 +97,17 @@
<svelte:window on:wheel={on_wheel} />
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 p-4 gap-4 grid grid-rows-[3rem_auto]">
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 px-4 py-2 gap-2 grid grid-rows-[3rem_auto]">
<!-- {@html SplashScreen} -->
<div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between">
<span class="text-4xl font-bold content-center h-full"> PLG MuDiCS </span>
<Button className="aspect-square" bg="bg-stone-800">
<span class="text-4xl font-bold content-center pl-1"> PLG MuDiCS </span>
<Button className="aspect-square" bg="bg-stone-800" div_class="aspect-square">
<Settings></Settings>
</Button>
</div>
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
<div class="h-[calc(100dvh-3rem-(12*var(--spacing)))] flex flex-col gap-2">
<div class="h-[calc(100dvh-3rem-(6*var(--spacing)))] flex flex-col gap-2">
{#if $pinned_display_id}
<!-- Pinned Item -->
<div in:fade={{ duration: 140 }} out:fade={{ duration: 120 }}>
@@ -201,9 +205,9 @@
title="Bildschirme größer darstellen"
className="aspect-square !p-1 rounded-r-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, 1)}
disabled={!Boolean(next_height_step_size('display', $current_height, 1))}
click_function={() => {
change_display_screen_height(1);
change_height('display', 1);
}}
>
<Plus />
@@ -212,9 +216,9 @@
title="Bildschirme kleiner darstellen"
className="aspect-square !p-1 rounded-l-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, -1)}
disabled={!Boolean(next_height_step_size('display', $current_height, -1))}
click_function={() => {
change_display_screen_height(-1);
change_height('display', -1);
}}
>
<Minus />
@@ -256,7 +260,7 @@
</div>
</div>
<div
class="col-start-2 h-[calc(100dvh-3rem-(12*var(--spacing)))] rounded-2xl flex flex-col gap-2"
class="col-start-2 h-[calc(100dvh-3rem-(6*var(--spacing)))] rounded-2xl flex flex-col gap-2"
>
<div class="grid grid-rows-[2.5rem_auto] bg-stone-800 rounded-2xl min-w-0">
<div
@@ -307,7 +311,7 @@
</div>
</div>
</div>
<div class="bg-stone-800 h-full rounded-2xl grid grid-rows-[2.5rem_auto]">
<div class="bg-stone-800 h-full rounded-2xl grid grid-rows-[2.5rem_1fr] min-h-0">
<div class="bg-stone-700 flex justify-between w-full p-1 rounded-t-2xl min-w-0 gap-2">
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
Dateien anzeigen und verwalten
@@ -317,9 +321,9 @@
title="Dateien größer darstellen"
className="aspect-square !p-1 rounded-r-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, 1)}
disabled={!Boolean(next_height_step_size('file', $current_height, 1))}
click_function={() => {
change_display_screen_height(1);
change_height('file', 1);
}}
>
<Plus />
@@ -328,9 +332,9 @@
title="Dateien kleiner darstellen"
className="aspect-square !p-1 rounded-l-none"
bg="bg-stone-600"
disabled={next_step_possible($display_screen_height, -1)}
disabled={!Boolean(next_height_step_size('file', $current_height, -1))}
click_function={() => {
change_display_screen_height(-1);
change_height('file', -1);
}}
>
<Minus />
@@ -338,35 +342,62 @@
</div>
</div>
<div class="flex flex-col gap-2 p-2 overflow-auto">
<div class="flex flex-row justify-between gap-6 overflow-x-auto">
<div class="flex flex-row gap-2">
<Button title="Eine Verzeichnis-Ebene zurück" className="px-3 flex"><FolderOutput /></Button>
<Button title="Datei anzeigen" className="px-3 flex gap-3">
<TvMinimalPlay class="shrink-0 flex"/>
<span class="min-w-0 hidden xl:flex">Anzeigen</span>
</Button>
<Button
title="Dateien zwischen Bildschirmen synchronisieren"
className="px-3 flex gap-3"
><RefreshCcw />
<span class="hidden 2xl:flex">Synchronisieren</span>
</Button>
</div>
<div class="flex flex-row gap-2">
<Button title="Datei(en) hochladen" className="px-3 flex"><Upload /></Button>
<Button title="Datei(en) herunterladen" className="px-3 flex"><Download /></Button>
<div class="border border-stone-700 my-1"></div>
<Button title="Datei(en) ausschneiden" className="px-3 flex"><Scissors /></Button>
<Button title="Datei(en) einfügen" className="px-3 flex"><ClipboardPaste /></Button>
<div class="border border-stone-700 my-1"></div>
<Button title="Datei(en) löschen" className="hover:!bg-red-400 px-3 flex"><Trash2 /></Button>
<div class="flex flex-col gap-2 p-2 bg-stone-750 rounded-xl">
<PathBar />
<div class="flex flex-row justify-between gap-6 overflow-x-auto">
<div class="flex flex-row gap-2 shrink-0">
<Button
title="Neuen Ordner erstellen (Neuen Ordner mit ausgewählten Objekten erstellen)"
className="px-3 flex"><FolderPlus /></Button
>
<div class="border border-stone-700 my-1"></div>
<Button title="Datei(en) hochladen" className="px-3 flex"><Upload /></Button>
<Button
title="Ausgewählte Datei(en) herunterladen"
className="px-3 flex"
disabled={$selected_file_ids.length === 0}><Download /></Button
>
<div class="border border-stone-700 my-1"></div>
<Button
title="Aktuellen Ordner / Ausgewählte Datei(en) zwischen Bildschirmen synchronisieren"
className="px-3 flex gap-3"
><RefreshCcw />
<span class="hidden 2xl:flex">Synchronisieren</span>
</Button>
</div>
<div class="flex flex-row gap-2">
<Button
title="Ausgewählte Datei(en) ausschneiden"
className="px-3 flex"
disabled={$selected_file_ids.length === 0}><Scissors /></Button
>
<Button title="Ausgewählte Datei(en) einfügen" className="px-3 flex"
><ClipboardPaste /></Button
>
<div class="border border-stone-700 my-1"></div>
<Button
title="Ausgewählte Datei umbenennen"
className="px-3 flex"
disabled={$selected_file_ids.length !== 1}><Pen /></Button
>
<Button
title="Ausgewählte Datei(en) löschen"
className="hover:!bg-red-400 px-3 flex"
disabled={$selected_file_ids.length === 0}><Trash2 /></Button
>
</div>
</div>
</div>
<div class="flex flex-col gap-2 p-2 bg-stone-750 h-full rounded-xl">
<FileObject />
<div class="min-h-0 h-full overflow-y-auto bg-stone-750 rounded-xl">
<div class="flex flex-col gap-2 p-2 min-h-0">
{#each get_current_folder_elements($all_files, $current_file_path, $selected_display_ids) as folder_element (folder_element.id)}
<section in:slide={{ duration: 100 }} class="outline-none">
<FolderElementObject file={folder_element} />
</section>
{/each}
</div>
</div>
</div>
</div>
</div>
</div>
+44 -32
View File
@@ -1,35 +1,47 @@
<script lang="ts">
let isHoveringChild = false;
<script>
import { Minus, Settings, TvMinimalPlay } from 'lucide-svelte';
function handleClickParent() {
if (!isHoveringChild) {
console.log('Parent clicked');
}
}
function handleClickChild(event: MouseEvent) {
console.log('Child clicked');
// Verhindert, dass das Event nach oben propagiert
event.stopPropagation();
}
import Button from '../../components/Button.svelte';
import { pinned_display_id } from '../../ts/stores/ui_behavior';
import { fade, slide } from 'svelte/transition';
</script>
<div
class={`relative rounded-lg p-6 transition-colors duration-200
${isHoveringChild ? 'bg-gray-200' : 'bg-gray-300 hover:bg-gray-400 active:bg-gray-500'}
`}
on:click={handleClickParent}
>
<p>Ich bin das Haupt-Div</p>
<div
class="no-display-selectable mt-4 p-4 bg-white border rounded shadow cursor-pointer"
on:mouseenter={() => (isHoveringChild = true)}
on:mouseleave={() => (isHoveringChild = false)}
on:click={handleClickChild}
>
Ich bin das Kind-Div (no-display-selectable)
</div>
<p class="mt-4">Noch mehr Inhalt...</p>
</div>
<main class="bg-stone-900 h-dvh w-dvw text-stone-200 p-4 gap-4 grid grid-rows-[3rem_auto]">
<div class="w-[calc(100dvw-(8*var(--spacing)))] flex justify-between">
<span class="text-4xl font-bold content-center h-full"> PLG MuDiCS </span>
<Button className="aspect-square" bg="bg-stone-800">
<Settings></Settings>
</Button>
</div>
<div class="w-[calc(100dvw-(8*var(--spacing)))] grid grid-cols-2 gap-2">
<div class="h-[calc(100dvh-3rem-(12*var(--spacing)))] flex flex-col gap-2"></div>
<div
class="col-start-2 h-[calc(100dvh-3rem-(12*var(--spacing)))] rounded-2xl flex flex-col gap-2"
>
<div class="bg-stone-800 h-full rounded-2xl grid grid-rows-[2.5rem_auto]">
<div class="bg-stone-700 flex justify-between w-full p-1 rounded-t-2xl min-w-0 gap-2">
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
Dateien anzeigen und verwalten
</span>
<div class="flex flex-ro"></div>
</div>
<div class="flex flex-col gap-2 p-2 max-w-full overflow-hidden min-w-0">
<div class="flex flex-row justify-between gap-4 min-w-0">
<div class="flex flex-row gap-2 min-w-0 flex-1">
<Button className="px-3 flex items-center gap-3 min-w-0 flex-1 justify-start">
<TvMinimalPlay />
<!-- Text: nimmt Restbreite, darf schrumpfen & ellipsieren -->
<span class="truncate min-w-0 flex-1">Sehr lange Beschriftung für den Button</span>
</Button>
<div class="relative min-w-0 flex">
<button class="text-xl font-bold pl-2 content-center truncate min-w-0">
Dateien anzeigen und verwalten
</button>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</main>
+123 -2
View File
@@ -61,13 +61,134 @@ module.exports = {
'active:bg-stone-900',
'active:bg-stone-950',
'h-5',
'h-10',
'text-stone-50',
'text-stone-150',
'text-stone-200',
'text-stone-250',
'text-stone-300',
'text-stone-350',
'text-stone-100',
'text-stone-400',
'text-stone-450',
'text-stone-500',
'text-stone-550',
'text-stone-600',
'text-stone-650',
'text-stone-700',
'text-stone-750',
'text-stone-800',
'text-stone-850',
'text-stone-900',
'text-stone-950',
'group-hover:text-stone-100',
'group-hover:text-stone-150',
'group-hover:text-stone-200',
'group-hover:text-stone-50',
'group-hover:text-stone-250',
'group-hover:text-stone-300',
'group-hover:text-stone-350',
'group-hover:text-stone-400',
'group-hover:text-stone-450',
'group-hover:text-stone-500',
'group-hover:text-stone-550',
'group-hover:text-stone-600',
'group-hover:text-stone-650',
'group-hover:text-stone-700',
'group-hover:text-stone-750',
'group-hover:text-stone-800',
'group-hover:text-stone-850',
'group-hover:text-stone-900',
'group-hover:text-stone-950',
'group-active:text-stone-50',
'group-active:text-stone-100',
'group-active:text-stone-150',
'group-active:text-stone-200',
'group-active:text-stone-250',
'group-active:text-stone-300',
'group-active:text-stone-350',
'group-active:text-stone-400',
'group-active:text-stone-450',
'group-active:text-stone-500',
'group-active:text-stone-550',
'group-active:text-stone-600',
'group-active:text-stone-650',
'group-active:text-stone-700',
'group-active:text-stone-750',
'group-active:text-stone-800',
'group-active:text-stone-850',
'group-active:text-stone-950',
'group-active:text-stone-900',
'border-stone-50',
'border-stone-100',
'border-stone-150',
'border-stone-200',
'border-stone-250',
'border-stone-300',
'border-stone-350',
'border-stone-400',
'border-stone-450',
'border-stone-500',
'border-stone-550',
'border-stone-600',
'border-stone-650',
'border-stone-700',
'border-stone-750',
'border-stone-800',
'border-stone-850',
'border-stone-900',
'border-stone-950',
'group-hover:border-stone-50',
'group-hover:border-stone-100',
'group-hover:border-stone-150',
'group-hover:border-stone-200',
'group-hover:border-stone-250',
'group-hover:border-stone-300',
'group-hover:border-stone-350',
'group-hover:border-stone-400',
'group-hover:border-stone-450',
'group-hover:border-stone-500',
'group-hover:border-stone-550',
'group-hover:border-stone-600',
'group-hover:border-stone-650',
'group-hover:border-stone-700',
'group-hover:border-stone-750',
'group-hover:border-stone-800',
'group-hover:border-stone-850',
'group-hover:border-stone-950',
'group-hover:border-stone-900',
'group-active:border-stone-50',
'group-active:border-stone-100',
'group-active:border-stone-150',
'group-active:border-stone-200',
'group-active:border-stone-250',
'group-active:border-stone-300',
'group-active:border-stone-350',
'group-active:border-stone-400',
'group-active:border-stone-450',
'group-active:border-stone-500',
'group-active:border-stone-550',
'group-active:border-stone-600',
'group-active:border-stone-650',
'group-active:border-stone-700',
'group-active:border-stone-750',
'group-active:border-stone-800',
'group-active:border-stone-850',
'group-active:border-stone-900',
'group-active:border-stone-950',
'h-15',
'h-20',
'h-25',
'h-30',
'h-35',
'h-40',
'right-0',
'left-0',
],
}
+25
View File
@@ -0,0 +1,25 @@
export async function get_file_data(ip: string, path: string = './') {
const options = {
method: 'PATCH',
headers: { 'content-type': 'application/json' },
body: `{"command":"cd ${path} && find . -maxdepth 1 -mindepth 1 -print0 | while IFS= read -r -d \'\' f; do\n typ=$(file -b --mime-type -- \"$f\")\n size=$(stat -c \'%s\' -- \"$f\")\n created=$(stat -c \'%w\' -- \"$f\")\n [ \"$created\" = \"-\" ] && created=$(stat -c \'%y\' -- \"$f\")\n jq -n --arg name \"$f\" --arg type \"$typ\" --arg size \"$size\" --arg created \"$created\" \'{name:$name, type:$type, size:($size|tonumber), created:$created}\' | tr -d \'\\n\'\n echo\n done\n"}`
};
console.log(request(ip, '/shellCommand', options));
}
async function request(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, body?: string }) {
try {
const url = `http://${ip}:1323/api${api_route}`;
const response = await fetch(url, options);
const data = await response.json();
return data;
} catch (error) {
console.error(error);
}
}
+10 -28
View File
@@ -1,14 +1,12 @@
import { get, writable, type Writable } from "svelte/store";
import type { Display, DisplayGroup } from "../types";
import { is_selected, select, selected_display_ids } from "./select";
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
id: crypto.randomUUID(),
data: []
}]);
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
add_testing_displays();
function add_display(ip: string, mac: string, name: string, status: string) {
displays.update((displays: DisplayGroup[]) => {
@@ -17,24 +15,6 @@ function add_display(ip: string, mac: string, name: string, status: string) {
});
}
export function select(display_id: string, new_value: boolean | null = null) {
selected_display_ids.update((all_ids: string[]) => {
if (all_ids.includes(display_id)) {
const index = all_ids.indexOf(display_id);
if (index > -1 && new_value !== true) {
all_ids.splice(index, 1);
}
} else if (new_value !== false) {
all_ids.push(display_id);
}
return all_ids;
});
}
export function is_selected(display_id: string, current_selected_display_ids: string[]): boolean {
return current_selected_display_ids.includes(display_id);
}
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
if (display_group.data.length === 0) return false;
for (const display of display_group.data) {
@@ -47,7 +27,7 @@ export function all_displays_of_group_selected(display_group: DisplayGroup, curr
export function select_all_of_group(display_group: DisplayGroup, new_value: boolean | null = null) {
for (const display of display_group.data) {
select(display.id, new_value);
select(selected_display_ids, display.id, new_value);
}
}
@@ -99,10 +79,12 @@ export function remove_empty_display_groups() {
add_testing_displays();
function add_testing_displays() {
const names = ["Vorne Rechts", "Vorne Links", "Vorne Mitte", "Fernseher Rechts", "Fernseher Bühne", "UIUIUIUIUIUIUISEHRLANGERTEXT DER IST WIRKLICH LANG, DER TEXT, so lang, dass er wirklich nirgendswo hinpasst, nichtmal da oben /\\"];
for (const name of names) {
add_display("192.168.1.42", "00:1A:2B:3C:4D:5E", name, "Offline");
}
// const names = ["Vorne Rechts", "Vorne Links", "Vorne Mitte", "Fernseher Rechts", "Fernseher Bühne", "UIUIUIUIUIUIUISEHRLANGERTEXT DER IST WIRKLICH LANG, DER TEXT, so lang, dass er wirklich nirgendswo hinpasst, nichtmal da oben /\\"];
// for (const name of names) {
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
// }
add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "Test", "Offline")
}
+239
View File
@@ -0,0 +1,239 @@
import { get, writable, type Writable } from "svelte/store";
import type { FolderElement } from "../types";
import { displays } from "./displays";
import { selected_display_ids, selected_file_ids } from "./select";
import { get_file_data } from "../api_handler";
export const all_files: Writable<Record<string, Record<string, FolderElement[]>>> = writable<Record<string, Record<string, FolderElement[]>>>({});
// {
// path: {
// display_id: FolderElement[]
// ...
// },
// path2: {
// display_id: FolderElement[]
// ...
// },
// ...
// }
export const current_file_path: Writable<string> = writable<string>('/');
export function change_file_path(new_path: string) {
current_file_path.update(() => {
return new_path;
});
selected_file_ids.update(() => {
return [];
})
}
export function get_display_ids_where_file_is_missing(path: string, file: FolderElement, selected_display_ids: string[], all_files: Record<string, Record<string, FolderElement[]>>): string[][] {
if (!all_files.hasOwnProperty(path)) return [];
const missing: string[] = [];
const colliding: string[] = [];
Display:
for (const selected_display_id of selected_display_ids) {
if (!all_files[path].hasOwnProperty(selected_display_id)) {
missing.push(selected_display_id);
continue;
}
for (const folder_element of all_files[path][selected_display_id]) {
if (folder_element.name === file.name) {
if (folder_element.hash !== file.hash) {
colliding.push(selected_display_id);
}
continue Display;
}
}
missing.push(selected_display_id);
}
return [missing, colliding];
}
export function updates_files_on_display(display_id: string, new_folder_elements: FolderElement[], file_path: string) {
all_files.update((files) => {
if (!files.hasOwnProperty(file_path)) {
files[file_path] = {};
}
for (const new_folder_element of new_folder_elements) {
new_folder_element.id = crypto.randomUUID();
}
files[file_path][display_id] = new_folder_elements;
return files;
});
}
function get_files_on_all_displays() {
for (const display_group of get(displays)) {
for (const display of display_group.data) {
get_file_data(display.ip)
}
}
}
export function get_current_folder_elements(all_files: Record<string, Record<string, FolderElement[]>>, current_file_path: string, selected_display_ids: string[]) {
if (!all_files.hasOwnProperty(current_file_path)) {
get_files_on_all_displays();
return [];
}
const files_on_display_array = all_files[current_file_path];
const files: FolderElement[] = [];
for (const key of Object.keys(files_on_display_array)) {
if (selected_display_ids.includes(key)) {
FileOnDisplay:
for (const file_on_display of files_on_display_array[key]) {
for (const existing_file of files) {
if (file_on_display.name === existing_file.name) {
if (file_on_display.hash === existing_file.hash) {
continue FileOnDisplay;
}
}
}
files.push(file_on_display);
}
}
}
return sort_files(files);
}
function sort_files(files: FolderElement[]) {
files.sort((a, b) => {
const isDirA = a.type === 'inode/directory';
const isDirB = b.type === 'inode/directory';
// Ordner zuerst
if (isDirA && !isDirB) return -1;
if (!isDirA && isDirB) return 1;
// Danach alphabetisch nach name (case-insensitive)
const nameCompare = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
if (nameCompare !== 0) return nameCompare;
// Wenn name gleich, absteigend nach date_created
return b.date_created.getTime() - a.date_created.getTime();
});
return files;
}
add_test_files();
export function add_test_files() {
// updates_files_on_display(get(displays)[0].data[0].id, [
// {
// hash: "ok",
// thumbnail: null,
// name: "ok.png",
// type: "image/png",
// date_created: new Date("2022-09-10"),
// size: "32 KB"
// },
// {
// hash: "schön",
// thumbnail: null,
// name: "schön.png",
// type: "image/png",
// date_created: new Date("2023-09-10"),
// size: "2 GB"
// },
// {
// hash: "lang",
// thumbnail: null,
// name: "langer Bildname der wirklich gar nicht mehr aufhört, sodass man mal gut testen kann, wie die UI darauf reagiert.odp",
// type: "application/vnd.oasis.opendocument.presentation",
// date_created: new Date("2028-09-10"),
// size: "324 KB"
// },
// {
// hash: "Schön hier aber waren Sie mal in Baden Würtemberg?",
// thumbnail: null,
// name: "Schön hier aber waren Sie mal in Baden Würtemberg?.mp4",
// type: "video/mp4",
// date_created: new Date("2024-09-10"),
// size: "32 KB"
// },
// {
// hash: "lan4234g",
// thumbnail: null,
// name: "Ein schöner Ordner",
// type: 'inode/directory',
// date_created: new Date("2025-01-02"),
// size: "324 TB"
// },
// {
// hash: "Schön hi23424er aber waren Sie mal in Baden Würtemberg?",
// thumbnail: null,
// name: "Ein hässlicher Ordner",
// type: 'inode/directory',
// date_created: new Date("2025-10-22"),
// size: "1 B"
// },
// ], '/');
// updates_files_on_display(get(displays)[0].data[0].id, [
// {
// hash: "ok",
// thumbnail: null,
// name: "ok.png",
// type: "image/png",
// date_created: new Date("2022-09-10"),
// size: "32 KB"
// },
// {
// hash: "schön",
// thumbnail: null,
// name: "schön.png",
// type: "image/png",
// date_created: new Date("2023-09-10"),
// size: "2 GB"
// },
// ], '/Ein hässlicher Ordner/');
// updates_files_on_display(get(displays)[0].data[0].id, [
// {
// hash: "nö",
// thumbnail: null,
// name: "nö.png",
// type: "image/png",
// date_created: new Date("2022-09-10"),
// size: "32 KB"
// },
// {
// hash: "na gut",
// thumbnail: null,
// name: "na gut.png",
// type: "image/png",
// date_created: new Date("2023-09-10"),
// size: "2 GB"
// },
// ], '/Ein schöner Ordner/');
// updates_files_on_display(get(displays)[0].data[1].id, [
// {
// hash: "okk",
// thumbnail: null,
// name: "ok.png",
// type: "image/png",
// date_created: new Date("2022-09-10"),
// size: "32 KB"
// },
// {
// hash: "schön",
// thumbnail: null,
// name: "schön.png",
// type: "image/png",
// date_created: new Date("2023-09-10"),
// size: "2 GB"
// },], '/');
}
+23
View File
@@ -0,0 +1,23 @@
import { writable, type Writable } from "svelte/store";
export const selected_file_ids: Writable<string[]> = writable<string[]>([]);
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
export function select(selected_ids: Writable<string[]>, id: string, new_value: boolean | null = null) {
selected_ids.update((all_ids: string[]) => {
if (all_ids.includes(id)) {
const index = all_ids.indexOf(id);
if (index > -1 && new_value !== true) {
all_ids.splice(index, 1);
}
} else if (new_value !== false) {
all_ids.push(id);
}
return all_ids;
});
}
export function is_selected(id: string, selected_ids: string[]): boolean {
return selected_ids.includes(id);
}
+33 -18
View File
@@ -1,36 +1,51 @@
import type { NumericRange } from "@sveltejs/kit";
import { get, writable, type Writable } from "svelte/store";
import { writable, type Writable } from "svelte/store";
const screen_height_step_size = 5;
export const dnd_flip_duration_ms = 300;
const min_display_screen_height = 15;
const max_display_screen_height = 40;
export const display_screen_height: Writable<number> = writable<number>(25);
const heights_options: Record<string, { min: number; max: number; step: number }> = {
display: {
min: 15,
max: 40,
step: 5,
},
file: {
min: 10,
max: 20,
step: 5,
}
}
export const current_height: Writable<Record<string, number>> = writable<Record<string, number>>({
display: 25,
file: 10,
});
export const is_group_drag: Writable<boolean> = writable<boolean>(false);
export const is_display_drag: Writable<boolean> = writable<boolean>(false);
export const pinned_display_id: Writable<string | null> = writable<string | null>(null);
export function change_display_screen_height(factor: number) {
display_screen_height.update((current_height) => {
const new_size = current_height + (factor * screen_height_step_size);
if (new_size > max_display_screen_height || new_size < min_display_screen_height) {
return current_height;
} else {
return new_size;
}
export function change_height(key: 'display' | 'file', factor: number) {
current_height.update((height) => {
height[key] = next_height_step_size(key, height, factor) || height[key];
return height;
});
}
export function next_step_possible(current_height: number, factor: number) {
const new_size = current_height + (factor * screen_height_step_size);
return new_size > max_display_screen_height || new_size < min_display_screen_height;
export function next_height_step_size(key: 'display' | 'file', current_height_array: Record<string, number>, factor: number): number {
const new_size = current_height_array[key] + (factor * heights_options[key].step);
if (new_size > heights_options[key].max || new_size < heights_options[key].min) {
return 0;
} else {
return new_size;
}
}
export function get_selectable_color_classes(
selected: boolean,
returning_classes: { bg?: boolean; hover?: boolean; active?: boolean; text?: boolean } = {},
+57 -3
View File
@@ -1,4 +1,59 @@
import type { X } from "lucide-svelte";
import { FileBox, FileImage, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
export type SupportedFileType = {
display_name:
string; mime_type:
string; icon: typeof X;
};
export const supported_file_types: Record<string, SupportedFileType> = {
'.mp4': {
display_name: 'MP4',
mime_type: 'video/mp4',
icon: FileVideoCamera,
},
'.jpg': {
display_name: 'JPG',
mime_type: 'image/jpg',
icon: FileImage,
},
'.jpeg': {
display_name: 'JPG',
mime_type: 'image/jpeg',
icon: FileImage,
},
'.png': {
display_name: 'PNG',
mime_type: 'image/png',
icon: FileImage,
},
'.gif': {
display_name: 'GIF',
mime_type: 'image/gif',
icon: ImagePlay,
},
'.pptx': {
display_name: 'PPTX',
mime_type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
icon: FileBox,
},
'.odp': {
display_name: 'ODP',
mime_type: 'application/vnd.oasis.opendocument.presentation',
icon: FileBox,
}
}
export type FolderElement = {
id?: string;
hash: string;
thumbnail: Blob | null;
name: string;
type: string;
date_created: Date;
size: string;
}
export type Display = {
id: string;
@@ -20,5 +75,4 @@ export type MenuOption = {
class?: string;
on_select?: () => void;
disabled?: boolean;
}
}
+3 -1
View File
@@ -12,10 +12,12 @@ patch {
body:json {
{
"command": "ls"
"command": "find . -maxdepth 1 -mindepth 1 -print0 | while IFS= read -r -d '' f; do\n typ=$(file -b --mime-type -- \"$f\")\n size=$(stat -c '%s' -- \"$f\")\n created=$(stat -c '%w' -- \"$f\")\n [ \"$created\" = \"-\" ] && created=$(stat -c '%y' -- \"$f\")\n jq -n --arg name \"$f\" --arg type \"$typ\" --arg size \"$size\" --arg created \"$created\" '{name:$name, type:$type, size:($size|tonumber), created:$created}' | tr -d '\\n'\n echo\n done\n"
}
}
settings {
encodeUrl: true
timeout: 0
}
+6 -1
View File
@@ -5,11 +5,16 @@ meta {
}
post {
url: 127.0.0.1:1323/api/file/titel.jpg
url: 127.0.0.1:1323/api/file/test.mp4
body: file
auth: inherit
}
body:file {
file: @file(/home/fedorra-44/Downloads/10 Revenge Party und Playoff.mov) @contentType(video/quicktime)
}
settings {
encodeUrl: true
timeout: 0
}