mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-05 16:37:09 +00:00
424 lines
14 KiB
Svelte
Executable File
424 lines
14 KiB
Svelte
Executable File
<script lang="ts">
|
|
import {
|
|
ClipboardPaste,
|
|
Download,
|
|
FolderPlus,
|
|
Pen,
|
|
RefreshCcw,
|
|
Scissors,
|
|
Trash2,
|
|
Upload,
|
|
ZoomIn,
|
|
ZoomOut
|
|
} from 'lucide-svelte';
|
|
import { change_height, current_height, next_height_step_size } from '$lib/ts/stores/ui_behavior';
|
|
import Button from '$lib/components/Button.svelte';
|
|
import PathBar from './PathBar.svelte';
|
|
import { select, selected_display_ids, selected_file_ids } from '$lib/ts/stores/select';
|
|
import {
|
|
current_file_path,
|
|
get_folder_elements,
|
|
get_file_by_id,
|
|
run_for_selected_files_on_selected_displays,
|
|
update_current_folder_on_selected_displays,
|
|
get_displays_where_path_not_exists,
|
|
create_path_on_all_selected_displays
|
|
} from '$lib/ts/stores/files';
|
|
import { slide } from 'svelte/transition';
|
|
import InodeElement from '../lib/components/InodeElement.svelte';
|
|
import PopUp from '$lib/components/PopUp.svelte';
|
|
import { get_file_primary_key, is_folder, type Inode, type PopupContent } from '$lib/ts/types';
|
|
import TextInput from '$lib/components/TextInput.svelte';
|
|
import {
|
|
first_letter_is_valid,
|
|
get_accepted_file_type_string,
|
|
is_valid_name
|
|
} from '$lib/ts/utils';
|
|
import { delete_files, rename_file } from '$lib/ts/api_handler';
|
|
import HighlightedText from '$lib/components/HighlightedText.svelte';
|
|
import { liveQuery, type Observable } from 'dexie';
|
|
import { download_file, add_upload, add_sync_recursively } from '$lib/ts/file_transfer_handler';
|
|
import { selected_online_display_ids } from '$lib/ts/stores/displays';
|
|
|
|
let current_name: string = $state('');
|
|
let current_valid: boolean = $state(false);
|
|
|
|
let display_names_where_path_does_not_exist: string[] = $state([]);
|
|
let selected_files: Observable<Inode[]> | undefined = $state();
|
|
$effect(() => {
|
|
const s = $selected_file_ids;
|
|
selected_files = liveQuery(() => get_selected_files(s));
|
|
});
|
|
let current_folder_elements: Observable<Inode[]> | undefined = $state();
|
|
$effect(() => {
|
|
const path = $current_file_path,
|
|
display_ids = $selected_online_display_ids;
|
|
current_folder_elements = liveQuery(() => get_folder_elements(path, display_ids));
|
|
});
|
|
let one_file_selected: Observable<boolean> | undefined = $state();
|
|
$effect(() => {
|
|
const s = $selected_file_ids;
|
|
one_file_selected = liveQuery(async () => {
|
|
const inode = await get_file_by_id(s[0]);
|
|
if (!inode) return false;
|
|
return s.length === 1 && !is_folder(inode);
|
|
});
|
|
});
|
|
|
|
let popup_content: PopupContent = $state({
|
|
open: false,
|
|
snippet: null,
|
|
title: ''
|
|
});
|
|
|
|
let file_input: HTMLInputElement;
|
|
|
|
async function get_selected_files(selected_file_ids: string[]): Promise<Inode[]> {
|
|
try {
|
|
const results = await Promise.all(selected_file_ids.map((id) => get_file_by_id(id)));
|
|
return results.filter((element) => element !== null);
|
|
} catch (e: unknown) {
|
|
console.error('Error on generating selected_files', e);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function popup_close_function() {
|
|
popup_content.open = false;
|
|
}
|
|
|
|
async function create_new_folder() {
|
|
popup_close_function();
|
|
const path_with_folder_name = ($current_file_path += current_name.trim() + '/');
|
|
await create_path_on_all_selected_displays(path_with_folder_name, $selected_online_display_ids);
|
|
await update_current_folder_on_selected_displays();
|
|
}
|
|
|
|
async function edit_file_name(new_file_name: string) {
|
|
popup_close_function();
|
|
await run_for_selected_files_on_selected_displays(async (ip: string, file_names: string[]) => {
|
|
if (file_names.length !== 1) {
|
|
console.error(file_names);
|
|
return; // Error
|
|
}
|
|
await rename_file(ip, $current_file_path, file_names[0], new_file_name);
|
|
});
|
|
await update_current_folder_on_selected_displays();
|
|
}
|
|
|
|
const show_edit_file_popup = async () => {
|
|
const file = await get_file_by_id($selected_file_ids[0]);
|
|
if (!file) return;
|
|
const file_is_folder = is_folder(file);
|
|
const extension = file_is_folder ? '' : '.' + file.name.split('.').at(-1) || '';
|
|
current_name = file.name.slice(0, file.name.length - extension.length);
|
|
current_valid = true;
|
|
popup_content = {
|
|
open: true,
|
|
snippet: edit_file_name_popup,
|
|
title: `${file_is_folder ? 'Ordner' : 'Datei'} umbenennen`,
|
|
title_icon: FolderPlus,
|
|
snippet_arg: extension
|
|
};
|
|
};
|
|
|
|
const show_new_folder_popup = async () => {
|
|
current_name = '';
|
|
current_valid = false;
|
|
display_names_where_path_does_not_exist = (
|
|
await get_displays_where_path_not_exists($current_file_path, $selected_online_display_ids)
|
|
).map((display) => display.name);
|
|
popup_content = {
|
|
open: true,
|
|
snippet: new_folder_popup,
|
|
title: 'Neuen Ordner erstellen',
|
|
title_icon: FolderPlus
|
|
};
|
|
};
|
|
|
|
const delete_folder_element_popup = () => {
|
|
popup_content = {
|
|
open: true,
|
|
snippet: delete_request_popup,
|
|
title: `${$selected_file_ids.length} ${$selected_file_ids.length === 1 ? 'Objekt' : 'Objekte'} wirklich löschen?`,
|
|
title_icon: Trash2
|
|
};
|
|
};
|
|
|
|
async function sync_selected_files(
|
|
current_selected_file_ids: string[],
|
|
selected_display_ids: string[],
|
|
current_folder_elements: Inode[]
|
|
) {
|
|
if (current_selected_file_ids.length === 0 && current_folder_elements.length > 0) {
|
|
current_selected_file_ids = current_folder_elements.map((inode) =>
|
|
get_file_primary_key(inode)
|
|
);
|
|
}
|
|
if (current_selected_file_ids.length === 0) return;
|
|
// Mit For-Schleife über ausgewählte Elemente gehen
|
|
for (const file_id of current_selected_file_ids) {
|
|
await select(selected_file_ids, file_id, 'deselect');
|
|
await add_sync_recursively(file_id, selected_display_ids);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
{#snippet new_folder_popup()}
|
|
{#if display_names_where_path_does_not_exist.length > 0}
|
|
<span class="leading-relaxed"
|
|
>Der aktuelle Pfad <HighlightedText
|
|
>{$current_file_path.slice(0, $current_file_path.length - 1)}</HighlightedText
|
|
> existiert nicht auf {display_names_where_path_does_not_exist.length === 1
|
|
? 'dem Bildschirm'
|
|
: 'den Bildschirmen'}
|
|
{#each display_names_where_path_does_not_exist as display_name, i (i)}
|
|
{#if i !== 0}
|
|
,
|
|
{/if}
|
|
<HighlightedText>{display_name}</HighlightedText>
|
|
{/each}. Mit der Erstellung dieses Ordners wird der Pfad automatisch mit leeren Ordnern bis
|
|
zum aktuellen Pfad aufgefüllt.
|
|
</span>
|
|
{/if}
|
|
<TextInput
|
|
focused_on_start={true}
|
|
bind:current_value={current_name}
|
|
bind:current_valid
|
|
title="Ordnername"
|
|
is_valid_function={async (input: string) => {
|
|
const trimmed_input = input.trim();
|
|
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
|
return [false, 'Ungültige Länge'];
|
|
if (!first_letter_is_valid(trimmed_input))
|
|
return [false, `Name darf nicht mit ${trimmed_input[0]} beginnen`];
|
|
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
|
if (($current_folder_elements ?? []).some((e) => e.name === trimmed_input))
|
|
return [false, 'Name bereits verwendet'];
|
|
return [true, 'Gültiger Name'];
|
|
}}
|
|
enter_mode="submit"
|
|
enter_function={create_new_folder}
|
|
/>
|
|
<div class="flex flex-row justify-end gap-2">
|
|
<Button className="px-4 font-bold" click_function={create_new_folder} disabled={!current_valid}
|
|
>Neuen Ordner erstellen</Button
|
|
>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet edit_file_name_popup(extension: string)}
|
|
<TextInput
|
|
focused_on_start={true}
|
|
bind:current_value={current_name}
|
|
bind:current_valid
|
|
title="Neuer {extension === '' ? 'Ordner' : 'Datei'}name"
|
|
is_valid_function={async (input: string) => {
|
|
const trimmed_input = input.trim() + extension;
|
|
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
|
return [false, 'Ungültige Länge'];
|
|
if (!first_letter_is_valid(trimmed_input))
|
|
return [false, `Name darf nicht mit ${trimmed_input[0]} beginnen`];
|
|
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
|
if (
|
|
($current_folder_elements ?? []).some(
|
|
(e) => e.name === trimmed_input && get_file_primary_key(e) !== $selected_file_ids[0]
|
|
)
|
|
)
|
|
return [false, 'Name bereits verwendet'];
|
|
return [true, 'Gültiger Name'];
|
|
}}
|
|
enter_mode="submit"
|
|
enter_function={async () => await edit_file_name(current_name.trim() + extension)}
|
|
{extension}
|
|
/>
|
|
<div class="flex flex-row justify-end gap-2">
|
|
<Button
|
|
className="px-4 font-bold"
|
|
click_function={async () => await edit_file_name(current_name.trim() + extension)}
|
|
disabled={!current_valid}>{extension === '' ? 'Ordner' : 'Datei'} umbenennen</Button
|
|
>
|
|
</div>
|
|
{/snippet}
|
|
|
|
{#snippet delete_request_popup()}
|
|
<div class="flex flex-col gap-1 h-full min-h-0 grow-0">
|
|
<span class="text-stone-400 px-1"
|
|
>{`${$selected_file_ids.length === 1 ? 'Folgendes Objekt' : `Folgende ${$selected_file_ids.length} Objekte`} löschen? (Wiederherstellung nicht möglich)`}</span
|
|
>
|
|
<div class="flex flex-col gap-2 overflow-y-auto h-full min-h-0 grow-0">
|
|
{#each $selected_files || [] as file, i (i)}
|
|
<InodeElement {file} not_interactable />
|
|
{/each}
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-row justify-end gap-2">
|
|
<Button className="button space font-bold" click_function={popup_close_function}>
|
|
Abbrechen
|
|
</Button>
|
|
<Button
|
|
className="button error space"
|
|
click_function={async () => {
|
|
popup_close_function();
|
|
await run_for_selected_files_on_selected_displays(
|
|
async (ip: string, file_names: string[]) => {
|
|
await delete_files(ip, $current_file_path, file_names);
|
|
}
|
|
);
|
|
await update_current_folder_on_selected_displays();
|
|
}}>Löschen</Button
|
|
>
|
|
</div>
|
|
{/snippet}
|
|
|
|
<input
|
|
class="hidden"
|
|
type="file"
|
|
bind:this={file_input}
|
|
multiple
|
|
accept={get_accepted_file_type_string()}
|
|
onchange={async (e) => {
|
|
const target = e.target as HTMLInputElement;
|
|
await add_upload(target.files!, $selected_online_display_ids, $current_file_path);
|
|
target.value = '';
|
|
}}
|
|
/>
|
|
|
|
<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
|
|
</span>
|
|
<div class="flex flex-row">
|
|
<Button
|
|
title="Dateien größer darstellen"
|
|
className="aspect-square p-1.5! pr-1! rounded-r-none"
|
|
bg="bg-stone-600"
|
|
disabled={!next_height_step_size('file', $current_height, 1)}
|
|
click_function={() => {
|
|
change_height('file', 1);
|
|
}}
|
|
>
|
|
<ZoomIn class="size-full" />
|
|
</Button>
|
|
<Button
|
|
title="Dateien kleiner darstellen"
|
|
className="aspect-square p-1.5! pl-1! rounded-l-none"
|
|
bg="bg-stone-600"
|
|
disabled={!next_height_step_size('file', $current_height, -1)}
|
|
click_function={() => {
|
|
change_height('file', -1);
|
|
}}
|
|
>
|
|
<ZoomOut class="size-full" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div class="flex flex-col gap-2 p-2 overflow-hidden relative rounded-b-2xl">
|
|
<div class="flex flex-col gap-2 p-2 bg-stone-750 rounded-xl">
|
|
<PathBar />
|
|
<div class="flex flex-row justify-between gap-6">
|
|
<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"
|
|
click_function={show_new_folder_popup}
|
|
disabled={$selected_online_display_ids.length === 0}><FolderPlus /></Button
|
|
>
|
|
<div class="border border-stone-700 my-1"></div>
|
|
<Button
|
|
title="Datei(en) hochladen"
|
|
className="px-3 flex"
|
|
click_function={() => {
|
|
if (file_input) file_input.click();
|
|
}}
|
|
disabled={$selected_online_display_ids.length === 0}><Upload /></Button
|
|
>
|
|
<Button
|
|
title="Ausgewählte Datei herunterladen"
|
|
className="px-3 flex"
|
|
click_function={async () =>
|
|
await download_file($selected_file_ids[0], $selected_online_display_ids)}
|
|
disabled={!$one_file_selected}><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"
|
|
click_function={async () =>
|
|
await sync_selected_files(
|
|
$selected_file_ids,
|
|
$selected_online_display_ids,
|
|
$current_folder_elements ?? []
|
|
)}
|
|
disabled={$selected_online_display_ids.length === 0}
|
|
><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"
|
|
disabled={$selected_online_display_ids.length === 0}
|
|
>
|
|
<ClipboardPaste />
|
|
</Button>
|
|
<div class="border border-stone-700 my-1"></div>
|
|
<Button
|
|
title="Ausgewählte Datei umbenennen"
|
|
className="px-3 flex"
|
|
click_function={show_edit_file_popup}
|
|
disabled={$selected_file_ids.length !== 1}><Pen /></Button
|
|
>
|
|
<Button
|
|
title="Ausgewählte Datei(en) löschen"
|
|
hover_bg="bg-red-400"
|
|
active_bg="bg-red-500"
|
|
className="px-3 flex"
|
|
disabled={$selected_file_ids.length === 0}
|
|
click_function={delete_folder_element_popup}><Trash2 /></Button
|
|
>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="min-h-0 h-full overflow-y-auto overflow-x-hidden bg-stone-750 rounded-xl">
|
|
<div class="flex flex-col gap-2 p-2 min-h-0 max-w-full">
|
|
{#if $selected_online_display_ids.length === 0}
|
|
<span class="text-stone-450 px-10 py-6 leading-relaxed text-center">
|
|
Es sind keine Bildschirme ausgewählt.
|
|
</span>
|
|
{:else}
|
|
{#each $current_folder_elements ?? [] as folder_element (get_file_primary_key(folder_element))}
|
|
<section in:slide={{ duration: 100 }} class="outline-none">
|
|
<InodeElement file={folder_element} />
|
|
</section>
|
|
{/each}
|
|
{#if ($current_folder_elements ?? []).length === 0}
|
|
<span class="text-stone-450 px-10 py-6 leading-relaxed text-center max-w-full">
|
|
Es existieren keine Dateien auf {$selected_display_ids.length === 1
|
|
? 'dem ausgewähltem Bildchirm'
|
|
: 'den ausgewählten Bildschirmen'} im aktuellen Ordner. Klicke auf <HighlightedText
|
|
bg="bg-stone-700"
|
|
fg="text-stone-400"
|
|
className="p-1!"><Upload class="inline pb-1" /></HighlightedText
|
|
> um Datei(en) hochzuladen.
|
|
</span>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
</div>
|
|
<PopUp
|
|
content={popup_content}
|
|
close_function={popup_close_function}
|
|
className="rounded-b-2xl"
|
|
snippet_container_class="overflow-hidden min-w-90"
|
|
/>
|
|
</div>
|
|
</div>
|