mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-05 16:37:09 +00:00
improve folder view - test with display-api
This commit is contained in:
Generated
+6
-6
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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',
|
||||
],
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
@@ -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"
|
||||
// },], '/');
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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 } = {},
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user