mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
refactor(frontend): use db for files & displays
Co-Authored-By: E44 <129310925+programmer-44@users.noreply.github.com>
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
menu_class = 'right-0',
|
||||
div_class = '',
|
||||
children
|
||||
} = $props<{
|
||||
}: {
|
||||
className?: string;
|
||||
bg?: string;
|
||||
hover_bg?: string;
|
||||
@@ -28,7 +28,7 @@
|
||||
menu_class?: string;
|
||||
div_class?: string;
|
||||
children?: any;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let menu_shown = $state(false);
|
||||
let button_element: HTMLButtonElement;
|
||||
@@ -166,8 +166,8 @@
|
||||
? 'text-stone-500 cursor-not-allowed'
|
||||
: 'hover:bg-white/35 active:bg-white/60 cursor-pointer ' +
|
||||
option.class} rounded-lg p-2 transition-colors duration-200 select-none flex flex-row gap-2 items-center"
|
||||
onclick={(e) => {
|
||||
if (option.on_select) option.on_select();
|
||||
onclick={async (e) => {
|
||||
if (option.on_select) await option.on_select();
|
||||
close_menu();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
import PopUp from './PopUp.svelte';
|
||||
import type { PopupContent } from '../ts/types';
|
||||
import KeyInput from './KeyInput.svelte';
|
||||
import { send_keyboard_input, show_blackscreen, show_html } from '../ts/api_handler';
|
||||
import { send_keyboard_input, show_blackscreen } from '../ts/api_handler';
|
||||
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||
import { selected_display_ids } from '../ts/stores/select';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -81,16 +81,16 @@
|
||||
title="Vorherige Folie (Pfeil nach Links)"
|
||||
className="px-9"
|
||||
disabled={$selected_display_ids.length === 0}
|
||||
click_function={() => {
|
||||
run_on_all_selected_displays(send_keyboard_input, true, 'VK_LEFT');
|
||||
click_function={async () => {
|
||||
await run_on_all_selected_displays(send_keyboard_input, true, 'VK_LEFT');
|
||||
}}><ArrowBigLeft /></Button
|
||||
>
|
||||
<Button
|
||||
title="Nächste Folie (Pfeil nach Rechts)"
|
||||
className="px-9"
|
||||
disabled={$selected_display_ids.length === 0}
|
||||
click_function={() => {
|
||||
run_on_all_selected_displays(send_keyboard_input, true, 'VK_RIGHT');
|
||||
click_function={async () => {
|
||||
await run_on_all_selected_displays(send_keyboard_input, true, 'VK_RIGHT');
|
||||
}}><ArrowBigRight /></Button
|
||||
>
|
||||
</div>
|
||||
@@ -102,8 +102,8 @@
|
||||
<Button
|
||||
className="px-3 flex gap-3 w-75 justify-normal"
|
||||
disabled={$selected_display_ids.length === 0}
|
||||
click_function={() => {
|
||||
run_on_all_selected_displays(show_blackscreen, true);
|
||||
click_function={async () => {
|
||||
await run_on_all_selected_displays(show_blackscreen, true);
|
||||
}}><Presentation />Blackout</Button
|
||||
>
|
||||
<div class="flex flex-row justify-normal">
|
||||
|
||||
@@ -2,10 +2,10 @@
|
||||
import { GripHorizontal } from 'lucide-svelte';
|
||||
import { dragHandle } from 'svelte-dnd-action';
|
||||
|
||||
let { bg, className = "" } = $props<{
|
||||
let { bg, className = "" }: {
|
||||
bg: string;
|
||||
className?: string;
|
||||
}>();
|
||||
className?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div
|
||||
|
||||
@@ -10,51 +10,61 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import DisplayObject from './DisplayObject.svelte';
|
||||
import {
|
||||
add_empty_display_group,
|
||||
all_displays_of_group_selected,
|
||||
remove_empty_display_groups,
|
||||
get_display_ids_in_group,
|
||||
select_all_of_group,
|
||||
set_new_display_group_data
|
||||
set_new_display_order
|
||||
} from '../ts/stores/displays';
|
||||
import DNDGrip from './DNDGrip.svelte';
|
||||
import { fade } from 'svelte/transition';
|
||||
import type { DisplayGroup, MenuOption } from '../ts/types';
|
||||
import { selected_display_ids } from '../ts/stores/select';
|
||||
import { liveQuery } from 'dexie';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let { display_group, get_display_menu_options, close_pinned_display } = $props<{
|
||||
display_group: DisplayGroup;
|
||||
let { display_group_id, get_display_menu_options, close_pinned_display }: {
|
||||
|
||||
display_group_id: string;
|
||||
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||
close_pinned_display: () => void;
|
||||
}>();
|
||||
} = $props
|
||||
();
|
||||
|
||||
let all_selected = liveQuery(() =>
|
||||
all_displays_of_group_selected(display_group_id, $selected_display_ids)
|
||||
);
|
||||
let display_ids_in_group = liveQuery(() => get_display_ids_in_group(display_group_id));
|
||||
let hovering_selectable = $state(false);
|
||||
|
||||
function select_all_of_this_group() {
|
||||
const new_value = !all_displays_of_group_selected(display_group, $selected_display_ids);
|
||||
select_all_of_group(display_group, new_value);
|
||||
|
||||
async function select_all_of_this_group() {
|
||||
const new_value = !(await all_displays_of_group_selected(
|
||||
display_group_id,
|
||||
$selected_display_ids
|
||||
));
|
||||
await select_all_of_group(display_group_id, new_value);
|
||||
}
|
||||
|
||||
function handle_consider(e: CustomEvent) {
|
||||
async function handle_consider(e: CustomEvent) {
|
||||
const { items, info } = e.detail;
|
||||
|
||||
if (items.length !== 1 && info.trigger === TRIGGERS.DRAG_STARTED) {
|
||||
$is_display_drag = true;
|
||||
add_empty_display_group();
|
||||
// add_empty_display_group();
|
||||
}
|
||||
set_new_display_group_data(display_group.id, items);
|
||||
await set_new_display_order(items);
|
||||
}
|
||||
|
||||
function handle_finalize(e: CustomEvent) {
|
||||
remove_empty_display_groups();
|
||||
async function handle_finalize(e: CustomEvent) {
|
||||
$is_display_drag = false;
|
||||
set_new_display_group_data(display_group.id, e.detail.items);
|
||||
await set_new_display_order(e.detail.items);
|
||||
}
|
||||
</script>
|
||||
|
||||
<div
|
||||
transition:fade={{ duration: 100 }}
|
||||
class="{get_selectable_color_classes(
|
||||
all_displays_of_group_selected(display_group, $selected_display_ids),
|
||||
$all_selected || false,
|
||||
{
|
||||
bg: true,
|
||||
hover: hovering_selectable,
|
||||
@@ -68,7 +78,7 @@
|
||||
<div
|
||||
class="flex flex-col min-w-0 pl-2 py-2 gap-2 w-full"
|
||||
use:dragHandleZone={{
|
||||
items: display_group.data,
|
||||
items: $display_ids_in_group || [],
|
||||
type: 'item',
|
||||
flipDurationMs: dnd_flip_duration_ms,
|
||||
dropTargetStyle: { outline: 'none' }
|
||||
@@ -76,7 +86,7 @@
|
||||
onconsider={handle_consider}
|
||||
onfinalize={handle_finalize}
|
||||
>
|
||||
{#each display_group.data as display (display.id)}
|
||||
{#each $display_ids_in_group || [] as display (display.id)}
|
||||
<!-- Each Group -->
|
||||
<section
|
||||
animate:flip={{ duration: $is_group_drag ? 0 : dnd_flip_duration_ms, easing: cubicOut }}
|
||||
@@ -87,7 +97,7 @@
|
||||
</section>
|
||||
{/each}
|
||||
|
||||
{#if display_group.data.length === 0}
|
||||
{#if ($display_ids_in_group || []).length === 0}
|
||||
<div class="min-h-10 h-full w-full pl-2 py-2 flex justify-center items-center">
|
||||
Hier in leere neue Gruppe ablegen
|
||||
</div>
|
||||
@@ -95,18 +105,18 @@
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center justify-center px-2"
|
||||
onclick={(e) => select_all_of_this_group()}
|
||||
onclick={select_all_of_this_group}
|
||||
role="button"
|
||||
tabindex="0"
|
||||
onkeydown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') select_all_of_this_group();
|
||||
onkeydown={async (e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') await select_all_of_this_group();
|
||||
}}
|
||||
onmouseenter={() => (hovering_selectable = true)}
|
||||
onmouseleave={() => (hovering_selectable = false)}
|
||||
>
|
||||
<DNDGrip
|
||||
bg={get_selectable_color_classes(
|
||||
all_displays_of_group_selected(display_group, $selected_display_ids),
|
||||
$all_selected || false,
|
||||
{
|
||||
bg: true
|
||||
},
|
||||
|
||||
Regular → Executable
+17
-11
@@ -10,30 +10,36 @@
|
||||
import OnlineState from './OnlineState.svelte';
|
||||
import type { Display, MenuOption } from '../ts/types';
|
||||
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
||||
import { update_screenshot } from '../ts/stores/displays';
|
||||
import { filter_file_selection_for_current_selected_displays } from '../ts/stores/files';
|
||||
import { screenshot_loop } from '../ts/stores/displays';
|
||||
import { change_file_path, current_file_path } from '../ts/stores/files';
|
||||
|
||||
let { display, get_display_menu_options, close_pinned_display } = $props<{
|
||||
let {
|
||||
display,
|
||||
get_display_menu_options,
|
||||
close_pinned_display
|
||||
}: {
|
||||
display: Display;
|
||||
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||
close_pinned_display: () => void;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let hovering_unselectable = $state(false);
|
||||
|
||||
function onclick(e: Event) {
|
||||
select(selected_display_ids, display.id);
|
||||
filter_file_selection_for_current_selected_displays();
|
||||
async function onclick(e: Event) {
|
||||
e.stopPropagation();
|
||||
select(selected_display_ids, display.id, 'toggle');
|
||||
|
||||
// force file view update
|
||||
await change_file_path($current_file_path);
|
||||
}
|
||||
|
||||
function on_preview_click(e: MouseEvent) {
|
||||
async function on_preview_click(e: MouseEvent) {
|
||||
if ($pinned_display_id === display.id) {
|
||||
close_pinned_display();
|
||||
} else {
|
||||
$pinned_display_id = display.id;
|
||||
}
|
||||
update_screenshot(display.id);
|
||||
await screenshot_loop(display.id);
|
||||
e.stopPropagation();
|
||||
}
|
||||
</script>
|
||||
@@ -65,8 +71,8 @@
|
||||
<div class="size-[50%]">
|
||||
<Pin class="size-full" />
|
||||
</div>
|
||||
{:else if display.preview_url}
|
||||
<img src={display.preview_url} alt="preview" class="w-full object-cover bg-black" />
|
||||
{:else if display.preview.url}
|
||||
<img src={display.preview.url} alt="preview" class="w-full object-cover bg-black" />
|
||||
{:else}
|
||||
<!-- No Signal -->
|
||||
<div class="size-full bg-black flex justify-center items-center">
|
||||
|
||||
Regular → Executable
+45
-32
@@ -2,9 +2,10 @@
|
||||
import { fade, scale } from 'svelte/transition';
|
||||
import {
|
||||
all_displays_of_group_selected,
|
||||
displays,
|
||||
get_display_by_id,
|
||||
select_all_of_group
|
||||
get_display_groups,
|
||||
select_all_of_group,
|
||||
set_new_display_group_order
|
||||
} from '../ts/stores/displays';
|
||||
import {
|
||||
change_height,
|
||||
@@ -27,15 +28,23 @@
|
||||
import DisplayGroupObject from './DisplayGroupObject.svelte';
|
||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||
import HighlightedText from './HighlightedText.svelte';
|
||||
import { liveQuery, type Observable } from 'dexie';
|
||||
|
||||
let { handle_display_deletion, handle_display_editing } = $props<{
|
||||
let {
|
||||
handle_display_deletion,
|
||||
handle_display_editing
|
||||
}: {
|
||||
handle_display_deletion: (display_id: string) => void;
|
||||
handle_display_editing: (display_id: string) => void;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let displays_scroll_box: HTMLElement;
|
||||
let pinned_display: Display | null = $derived(
|
||||
get_display_by_id($pinned_display_id || '', $displays)
|
||||
let pinned_display: Observable<Display | null> = liveQuery(() =>
|
||||
get_display_by_id($pinned_display_id || '')
|
||||
);
|
||||
let display_groups = liveQuery(() => get_display_groups());
|
||||
let all_groups_selected = liveQuery(() =>
|
||||
all_selected($display_groups || [], $selected_display_ids)
|
||||
);
|
||||
|
||||
let last_pinned_pane_size: number = 45;
|
||||
@@ -66,16 +75,22 @@
|
||||
];
|
||||
}
|
||||
|
||||
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
||||
const new_value = !all_selected(current_displays, current_selected_display_ids);
|
||||
async function select_all(
|
||||
current_displays: DisplayGroup[],
|
||||
current_selected_display_ids: string[]
|
||||
) {
|
||||
const new_value = !(await all_selected(current_displays, current_selected_display_ids));
|
||||
for (const display_group of current_displays) {
|
||||
select_all_of_group(display_group, new_value);
|
||||
await select_all_of_group(display_group.id, new_value);
|
||||
}
|
||||
}
|
||||
|
||||
function all_selected(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
||||
async function all_selected(
|
||||
current_displays: DisplayGroup[],
|
||||
current_selected_display_ids: string[]
|
||||
) {
|
||||
for (const display_group of current_displays) {
|
||||
if (!all_displays_of_group_selected(display_group, current_selected_display_ids)) {
|
||||
if (!(await all_displays_of_group_selected(display_group.id, current_selected_display_ids))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -121,16 +136,16 @@
|
||||
>
|
||||
<span
|
||||
class="text-xl font-bold pl-2 content-center truncate min-w-0"
|
||||
title={pinned_display?.name}
|
||||
title={$pinned_display?.name || '...'}
|
||||
>
|
||||
{pinned_display?.name}
|
||||
{$pinned_display?.name || '...'}
|
||||
</span>
|
||||
<div class="flex flex-row gap-1">
|
||||
<div class="flex flex-row items-center mr-1">
|
||||
<span class="text-stone-400"> Aktueller Status: </span>
|
||||
<OnlineState
|
||||
selected={false}
|
||||
status={pinned_display?.status ?? null}
|
||||
status={$pinned_display?.status ?? null}
|
||||
className="flex items-center px-2"
|
||||
/>
|
||||
</div>
|
||||
@@ -159,9 +174,9 @@
|
||||
<div
|
||||
class="h-full bg-stone-800 rounded-b-2xl overflow-hidden flex justify-center items-center"
|
||||
>
|
||||
{#if pinned_display?.preview_url}
|
||||
{#if $pinned_display?.preview.url}
|
||||
<img
|
||||
src={pinned_display.preview_url}
|
||||
src={$pinned_display.preview.url}
|
||||
alt="preview"
|
||||
class="max-h-full max-w-full object-cover bg-black"
|
||||
/>
|
||||
@@ -188,7 +203,7 @@
|
||||
<div class="flex flex-row gap-1">
|
||||
<button
|
||||
class="min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
|
||||
all_selected($displays, $selected_display_ids),
|
||||
$all_groups_selected || false,
|
||||
{
|
||||
bg: true,
|
||||
hover: true,
|
||||
@@ -196,13 +211,9 @@
|
||||
text: true
|
||||
}
|
||||
)}"
|
||||
onclick={() => select_all($displays, $selected_display_ids)}
|
||||
onclick={async () => await select_all($display_groups || [], $selected_display_ids)}
|
||||
>
|
||||
<span
|
||||
>{all_selected($displays, $selected_display_ids)
|
||||
? 'Alle abwählen'
|
||||
: 'Alle auswählen'}</span
|
||||
>
|
||||
<span>{$all_groups_selected || false ? 'Alle abwählen' : 'Alle auswählen'}</span>
|
||||
</button>
|
||||
<div class="flex flex-row">
|
||||
<Button
|
||||
@@ -234,30 +245,32 @@
|
||||
<div
|
||||
class="min-h-full p-2 flex flex-col gap-4"
|
||||
use:dragHandleZone={{
|
||||
items: $displays,
|
||||
items: $display_groups || [],
|
||||
type: 'group',
|
||||
flipDurationMs: dnd_flip_duration_ms,
|
||||
dropFromOthersDisabled: true,
|
||||
dropTargetStyle: { outline: 'none' }
|
||||
}}
|
||||
onconsider={(e: CustomEvent) => {
|
||||
onconsider={async (e: CustomEvent) => {
|
||||
$is_group_drag = true;
|
||||
$displays = e.detail.items;
|
||||
await set_new_display_group_order(e.detail.items);
|
||||
}}
|
||||
onfinalize={(e: CustomEvent) => {
|
||||
$displays = e.detail.items;
|
||||
onfinalize={async (e: CustomEvent) => {
|
||||
await set_new_display_group_order(e.detail.items);
|
||||
$is_group_drag = false;
|
||||
}}
|
||||
>
|
||||
{#if $displays.length === 1 && $displays[0].data.length === 0}
|
||||
{#if ($display_groups || []).length === 0}
|
||||
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
|
||||
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf
|
||||
<HighlightedText fg="text-stone-450" className="!p-1"><Settings class="inline pb-1" /></HighlightedText>
|
||||
<HighlightedText fg="text-stone-450" className="!p-1"
|
||||
><Settings class="inline pb-1" /></HighlightedText
|
||||
>
|
||||
und
|
||||
<HighlightedText fg="text-stone-450">Neuen Bildschirm hinzufügen</HighlightedText>.
|
||||
</div>
|
||||
{:else}
|
||||
{#each $displays as display_group (display_group.id)}
|
||||
{#each $display_groups || [] as display_group (display_group.id)}
|
||||
<!-- Each Group -->
|
||||
<section
|
||||
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
||||
@@ -265,7 +278,7 @@
|
||||
class="outline-none"
|
||||
>
|
||||
<DisplayGroupObject
|
||||
{display_group}
|
||||
display_group_id={display_group.id}
|
||||
{get_display_menu_options}
|
||||
{close_pinned_display}
|
||||
/>
|
||||
|
||||
Regular → Executable
+52
-55
@@ -3,6 +3,7 @@
|
||||
ClipboardPaste,
|
||||
Download,
|
||||
FolderPlus,
|
||||
Minus,
|
||||
Info,
|
||||
Pen,
|
||||
RefreshCcw,
|
||||
@@ -17,30 +18,33 @@
|
||||
import PathBar from './PathBar.svelte';
|
||||
import { selected_display_ids, selected_file_ids } from '../ts/stores/select';
|
||||
import {
|
||||
all_files,
|
||||
current_file_path,
|
||||
get_current_folder_elements,
|
||||
get_display_ids_where_file_is_missing,
|
||||
get_display_ids_where_path_does_not_exist,
|
||||
get_file_by_id,
|
||||
get_longest_existing_path_and_needed_parts,
|
||||
run_for_selected_files_on_selected_displays,
|
||||
update_current_folder_on_selected_displays
|
||||
update_current_folder_on_selected_displays,
|
||||
get_displays_where_path_exists,
|
||||
create_folder_on_all_selected_displays
|
||||
} from '../ts/stores/files';
|
||||
import { slide } from 'svelte/transition';
|
||||
import FolderElementObject from './FolderElementObject.svelte';
|
||||
import InodeElement from './InodeElement.svelte';
|
||||
import PopUp from './PopUp.svelte';
|
||||
import type { PopupContent } from '../ts/types';
|
||||
import { get_file_primary_key, type Inode, type PopupContent } from '../ts/types';
|
||||
import TextInput from './TextInput.svelte';
|
||||
import { is_valid_name } from '../ts/utils';
|
||||
import { displays, get_display_by_id, run_on_all_selected_displays } from '../ts/stores/displays';
|
||||
import { create_folders, delete_files, rename_file } from '../ts/api_handler';
|
||||
import { get } from 'svelte/store';
|
||||
import { delete_files, rename_file } from '../ts/api_handler';
|
||||
import HighlightedText from './HighlightedText.svelte';
|
||||
import { liveQuery } from 'dexie';
|
||||
|
||||
let current_name: string = $state('');
|
||||
let current_valid: boolean = $state(false);
|
||||
|
||||
let display_names_where_path_does_not_exist: string[] = $state([]);
|
||||
let selected_files = liveQuery(() => get_selected_files($selected_display_ids));
|
||||
let current_folder_elements = liveQuery(() =>
|
||||
get_current_folder_elements($current_file_path, $selected_display_ids)
|
||||
);
|
||||
|
||||
let popup_content: PopupContent = $state({
|
||||
open: false,
|
||||
snippet: null,
|
||||
@@ -48,24 +52,26 @@
|
||||
closable: true
|
||||
});
|
||||
|
||||
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');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function popup_close_function() {
|
||||
popup_content.open = false;
|
||||
}
|
||||
|
||||
async function create_new_folder() {
|
||||
for (const display_id of $selected_display_ids) {
|
||||
const display = get_display_by_id(display_id, $displays);
|
||||
if (!display) continue;
|
||||
const path_data = get_longest_existing_path_and_needed_parts(
|
||||
$current_file_path,
|
||||
display_id,
|
||||
$all_files
|
||||
);
|
||||
await create_folders(display.ip, path_data.existing, [
|
||||
...path_data.needed,
|
||||
current_name.trim()
|
||||
]);
|
||||
}
|
||||
await create_folder_on_all_selected_displays(
|
||||
current_name.trim(),
|
||||
$current_file_path,
|
||||
$selected_display_ids
|
||||
);
|
||||
await update_current_folder_on_selected_displays();
|
||||
popup_close_function();
|
||||
}
|
||||
@@ -82,8 +88,8 @@
|
||||
popup_close_function();
|
||||
}
|
||||
|
||||
const show_edit_file_popup = () => {
|
||||
const file = get_file_by_id($selected_file_ids[0], $all_files, $current_file_path);
|
||||
const show_edit_file_popup = async () => {
|
||||
const file = await get_file_by_id($selected_file_ids[0]);
|
||||
if (!file) return;
|
||||
const is_folder = file.type === 'inode/directory';
|
||||
const extension = is_folder ? '' : '.' + file.name.split('.').at(-1) || '';
|
||||
@@ -99,9 +105,12 @@
|
||||
};
|
||||
};
|
||||
|
||||
const show_new_folder_popup = () => {
|
||||
const show_new_folder_popup = async () => {
|
||||
current_name = '';
|
||||
current_valid = false;
|
||||
display_names_where_path_does_not_exist = (
|
||||
await get_displays_where_path_exists($current_file_path, $selected_display_ids, true)
|
||||
).map((display) => display.name);
|
||||
popup_content = {
|
||||
open: true,
|
||||
snippet: new_folder_popup,
|
||||
@@ -123,22 +132,18 @@
|
||||
</script>
|
||||
|
||||
{#snippet new_folder_popup()}
|
||||
{#if get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files).length > 0}
|
||||
{#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 {get_display_ids_where_path_does_not_exist(
|
||||
$current_file_path,
|
||||
$selected_display_ids,
|
||||
$all_files
|
||||
).length === 1
|
||||
> existiert nicht auf {display_names_where_path_does_not_exist.length === 1
|
||||
? 'dem Bildschirm'
|
||||
: 'den Bildschirmen'}
|
||||
{#each get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files) as display_id, i}
|
||||
{#each display_names_where_path_does_not_exist as display_name, i}
|
||||
{#if i !== 0}
|
||||
,
|
||||
{/if}
|
||||
<HighlightedText>{get_display_by_id(display_id, $displays)?.name}</HighlightedText>
|
||||
<HighlightedText>{display_name}</HighlightedText>
|
||||
{/each}. Mit der Erstellung dieses Ordners wird der Pfad automatisch mit leeren Ordnern bis
|
||||
zum aktuellen Pfad aufgefüllt.
|
||||
</span>
|
||||
@@ -148,14 +153,14 @@
|
||||
bind:current_value={current_name}
|
||||
bind:current_valid
|
||||
title="Ordnername"
|
||||
is_valid_function={(input: string) => {
|
||||
is_valid_function={async (input: string) => {
|
||||
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
||||
const trimmed_input = input.trim();
|
||||
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
||||
return [false, 'Ungültige Länge'];
|
||||
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
||||
if (
|
||||
get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some(
|
||||
(await get_current_folder_elements($current_file_path, $selected_display_ids)).some(
|
||||
(e) => e.name === trimmed_input
|
||||
)
|
||||
)
|
||||
@@ -178,30 +183,28 @@
|
||||
bind:current_value={current_name}
|
||||
bind:current_valid
|
||||
title="Neuer {extension === '' ? 'Ordner' : 'Datei'}name"
|
||||
is_valid_function={(input: string) => {
|
||||
is_valid_function={async (input: string) => {
|
||||
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
||||
const trimmed_input = input.trim() + extension;
|
||||
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
||||
return [false, 'Ungültige Länge'];
|
||||
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
||||
if (
|
||||
get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some(
|
||||
(e) =>
|
||||
e.name === trimmed_input &&
|
||||
e.id !== get_file_by_id($selected_file_ids[0], $all_files, $current_file_path)?.id
|
||||
(await get_current_folder_elements($current_file_path, $selected_display_ids)).some(
|
||||
async (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={() => edit_file_name(current_name.trim() + extension)}
|
||||
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={() => edit_file_name(current_name.trim() + extension)}
|
||||
click_function={async () => await edit_file_name(current_name.trim() + extension)}
|
||||
disabled={!current_valid}>{extension === '' ? 'Ordner' : 'Datei'} umbenennen</Button
|
||||
>
|
||||
</div>
|
||||
@@ -213,10 +216,8 @@
|
||||
>{`${$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-auto h-full min-h-0 grow-0">
|
||||
{#each $selected_file_ids
|
||||
.map((file_id) => get_file_by_id(file_id, $all_files, $current_file_path))
|
||||
.filter((element) => element !== null) as file}
|
||||
<FolderElementObject {file} not_interactable />
|
||||
{#each $selected_files || [] as file}
|
||||
<InodeElement {file} not_interactable />
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
@@ -239,10 +240,6 @@
|
||||
</div>
|
||||
{/snippet}
|
||||
|
||||
{#snippet clipboard_hover_snippet()}
|
||||
<div></div>
|
||||
{/snippet}
|
||||
|
||||
<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">
|
||||
@@ -312,7 +309,7 @@
|
||||
>
|
||||
<Button
|
||||
title="Ausgewählte Datei(en) einfügen"
|
||||
className="!p-0 flex relative"
|
||||
className="px-3 flex"
|
||||
disabled={$selected_display_ids.length === 0}
|
||||
>
|
||||
<ClipboardPaste />
|
||||
@@ -342,12 +339,12 @@
|
||||
Es sind keine Bildschirme ausgewählt.
|
||||
</span>
|
||||
{:else}
|
||||
{#each get_current_folder_elements($all_files, $current_file_path, $selected_display_ids) as folder_element (folder_element.id)}
|
||||
{#each $current_folder_elements || [] as folder_element (get_file_primary_key(folder_element))}
|
||||
<section in:slide={{ duration: 100 }} class="outline-none">
|
||||
<FolderElementObject file={folder_element} />
|
||||
<InodeElement file={folder_element} />
|
||||
</section>
|
||||
{/each}
|
||||
{#if get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).length === 0}
|
||||
{#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'
|
||||
|
||||
@@ -4,9 +4,16 @@
|
||||
bg = 'bg-stone-750',
|
||||
fg = 'text-stone-200',
|
||||
className = ''
|
||||
} = $props<{ children: any; bg?: string; fg?: string; className?: string }>();
|
||||
}: {
|
||||
children: any;
|
||||
bg?: string;
|
||||
fg?: string;
|
||||
className?: string;
|
||||
} = $props();
|
||||
</script>
|
||||
|
||||
<div class="{bg} {fg} {className} rounded-lg py-0.5 px-1 inline-block truncate min-w-0 max-w-full align-middle my-[1.5px]">
|
||||
<div
|
||||
class="{bg} {fg} {className} rounded-lg py-0.5 px-1 inline-block truncate min-w-0 max-w-full align-middle my-[1.5px]"
|
||||
>
|
||||
{@render children()}
|
||||
</div>
|
||||
|
||||
Regular → Executable
+18
-46
@@ -1,24 +1,12 @@
|
||||
<script lang="ts">
|
||||
import {
|
||||
ArrowRight,
|
||||
Ban,
|
||||
FileIcon,
|
||||
Folder,
|
||||
Play,
|
||||
RefreshCcwDot,
|
||||
TriangleAlert
|
||||
} from 'lucide-svelte';
|
||||
import { ArrowRight, Ban, FileIcon, Folder, Play } 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_type_icon,
|
||||
type FolderElement,
|
||||
type SupportedFileType
|
||||
} from '../ts/types';
|
||||
import { supported_file_type_icon, type Inode, get_file_primary_key } from '../ts/types';
|
||||
|
||||
import {
|
||||
is_selected,
|
||||
@@ -27,39 +15,23 @@
|
||||
selected_file_ids
|
||||
} from '../ts/stores/select';
|
||||
import {
|
||||
all_files,
|
||||
change_file_path,
|
||||
current_file_path,
|
||||
get_display_ids_where_file_is_missing
|
||||
get_missing_colliding_display_ids
|
||||
} from '../ts/stores/files';
|
||||
import RefreshPlay from './RefreshPlay.svelte';
|
||||
import { get_file_size_display_string, get_file_type } from '../ts/utils';
|
||||
import { open_file } from '../ts/api_handler';
|
||||
import {
|
||||
displays,
|
||||
get_display_by_id,
|
||||
run_on_all_selected_displays,
|
||||
update_screenshot
|
||||
} from '../ts/stores/displays';
|
||||
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||
import { get_thumbnail_url } from '../ts/stores/thumbnails';
|
||||
import { db } from '../ts/indexdb/file_thumbnails.db';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { liveQuery } from 'dexie';
|
||||
|
||||
let { file, not_interactable = false } = $props<{
|
||||
file: FolderElement;
|
||||
not_interactable?: boolean;
|
||||
}>();
|
||||
let { file, not_interactable = false }: { file: Inode; not_interactable?: boolean } = $props();
|
||||
|
||||
let thumbnail_url: string | null = $state(null);
|
||||
// Update thumbnail_url automatically if data is available
|
||||
const subscription = liveQuery(() => db.thumbnail_blobs.get(file.hash)).subscribe({
|
||||
next: async () => {
|
||||
thumbnail_url = await get_thumbnail_url(file.hash);
|
||||
},
|
||||
error: (err) => console.error('Dexie subscription error:', err)
|
||||
});
|
||||
onDestroy(() => subscription.unsubscribe());
|
||||
let missing_colliding_displays_ids = liveQuery(() =>
|
||||
get_missing_colliding_display_ids(file, $selected_display_ids)
|
||||
);
|
||||
let thumbnail_url = liveQuery(() => get_thumbnail_url(file));
|
||||
|
||||
const is_folder = file.type === 'inode/directory';
|
||||
|
||||
@@ -106,13 +78,13 @@
|
||||
|
||||
function onclick(e: Event) {
|
||||
if (not_interactable) return;
|
||||
select(selected_file_ids, file.id);
|
||||
select(selected_file_ids, get_file_primary_key(file), 'toggle');
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
async function open() {
|
||||
if (is_folder) {
|
||||
change_file_path($current_file_path + file.name + '/');
|
||||
await change_file_path($current_file_path + file.name + '/');
|
||||
} else {
|
||||
const path_to_file = $current_file_path + file.name;
|
||||
await run_on_all_selected_displays(open_file, true, path_to_file);
|
||||
@@ -158,7 +130,7 @@
|
||||
>
|
||||
{#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}
|
||||
{:else if $missing_colliding_displays_ids && $missing_colliding_displays_ids.missing.length !== 0}
|
||||
<RefreshPlay className="size-full" />
|
||||
{:else if get_file_type(file) !== null}
|
||||
<Play class="size-full" strokeWidth="3" />
|
||||
@@ -176,7 +148,7 @@
|
||||
}}
|
||||
{onclick}
|
||||
class="{get_selectable_color_classes(
|
||||
!not_interactable && is_selected(file.id, $selected_file_ids),
|
||||
!not_interactable && is_selected(get_file_primary_key(file), $selected_file_ids),
|
||||
{
|
||||
bg: true,
|
||||
hover: !not_interactable,
|
||||
@@ -191,9 +163,9 @@
|
||||
<div class="aspect-square rounded-md flex justify-center items-center">
|
||||
{#if is_folder}
|
||||
<Folder class="size-full p-2" />
|
||||
{:else if thumbnail_url}
|
||||
{:else if $thumbnail_url || null}
|
||||
<img
|
||||
src={thumbnail_url}
|
||||
src={$thumbnail_url || null}
|
||||
alt="file_thumbnail"
|
||||
class="object-contain size-full select-none block p-1 rounded-lg"
|
||||
draggable="false"
|
||||
@@ -213,7 +185,7 @@
|
||||
</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)
|
||||
is_selected(get_file_primary_key(file), $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}
|
||||
@@ -251,7 +223,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||
is_selected(file.id, $selected_file_ids)
|
||||
is_selected(get_file_primary_key(file), $selected_file_ids)
|
||||
)} duration-200 transition-colors my-1"
|
||||
></div>
|
||||
<div
|
||||
@@ -262,7 +234,7 @@
|
||||
</div>
|
||||
<div
|
||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||
is_selected(file.id, $selected_file_ids)
|
||||
is_selected(get_file_primary_key(file), $selected_file_ids)
|
||||
)} duration-200 transition-colors"
|
||||
></div>
|
||||
<div
|
||||
@@ -3,8 +3,7 @@
|
||||
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
|
||||
import key_map_json from './../../../../shared/keys.json';
|
||||
import { fade } from 'svelte/transition';
|
||||
import { selected_display_ids } from '../ts/stores/select';
|
||||
import { displays, get_display_by_id, run_on_all_selected_displays } from '../ts/stores/displays';
|
||||
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||
import { send_keyboard_input } from '../ts/api_handler';
|
||||
|
||||
const bg = 'bg-stone-700';
|
||||
@@ -64,7 +63,11 @@
|
||||
{active ? 'Erfassung aktiv' : 'Hier für Erfassung klicken'}
|
||||
<div class="absolute top-full left-0 ml-1 mt-0.5 flex flex-col-reverse text-sm text-stone-400">
|
||||
{#each last_keys as key (key.id)}
|
||||
<span animate:flip={{ duration: 200 }} in:fade={{ duration: 200 }} out:fade={{ duration: 500 }} >{key.key}</span>
|
||||
<span
|
||||
animate:flip={{ duration: 200 }}
|
||||
in:fade={{ duration: 200 }}
|
||||
out:fade={{ duration: 500 }}>{key.key}</span
|
||||
>
|
||||
{/each}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
<script lang="ts">
|
||||
import { type DisplayStatus } from "../ts/types";
|
||||
import { display_status_to_info } from "../ts/utils";
|
||||
import { type DisplayStatus } from '../ts/types';
|
||||
import { display_status_to_info } from '../ts/utils';
|
||||
|
||||
let { selected, status, className = "" } = $props<{
|
||||
let {
|
||||
selected,
|
||||
status,
|
||||
className = ''
|
||||
}: {
|
||||
selected: boolean;
|
||||
status: DisplayStatus;
|
||||
className?: string;
|
||||
}>();
|
||||
className?: string;
|
||||
} = $props();
|
||||
|
||||
function get_text_color(selected: boolean, status: DisplayStatus) {
|
||||
switch (status) {
|
||||
|
||||
@@ -8,9 +8,11 @@
|
||||
import { flip } from 'svelte/animate';
|
||||
import { cubicOut } from 'svelte/easing';
|
||||
|
||||
let { bg = 'bg-stone-700' } = $props<{
|
||||
let {
|
||||
bg = 'bg-stone-700'
|
||||
}: {
|
||||
bg?: string;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let outside_container: HTMLDivElement;
|
||||
let inside_container: HTMLDivElement;
|
||||
@@ -79,21 +81,21 @@
|
||||
out.push({
|
||||
name: ' '.repeat(i) + hidden_folders[i],
|
||||
class: 'truncate max-w-80',
|
||||
on_select: () => {
|
||||
open_path(i + 1, path);
|
||||
on_select: async () => {
|
||||
await open_path(i + 1, path);
|
||||
}
|
||||
});
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function open_path(index_of_all_folders: number, path: string) {
|
||||
async 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);
|
||||
await change_file_path(new_path);
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
@@ -113,11 +115,14 @@
|
||||
<Button
|
||||
className="py-1 shrink-0 grow-0 w-10"
|
||||
{bg}
|
||||
click_function={(e) => {
|
||||
open_path(0, $current_file_path);
|
||||
click_function={async () => {
|
||||
await open_path(0, $current_file_path);
|
||||
}}
|
||||
>
|
||||
<House class="size-full transition-all duration-100" strokeWidth={$current_file_path === '/' ? 2.7 : 2}/>
|
||||
<House
|
||||
class="size-full transition-all duration-100"
|
||||
strokeWidth={$current_file_path === '/' ? 2.7 : 2}
|
||||
/>
|
||||
</Button>
|
||||
</div>
|
||||
{#if cut_folders !== 0}
|
||||
@@ -144,8 +149,8 @@
|
||||
? 'max-w-80 font-bold'
|
||||
: 'max-w-30'}"
|
||||
{bg}
|
||||
click_function={(e) => {
|
||||
open_path(cut_folders + i + 1, $current_file_path);
|
||||
click_function={async () => {
|
||||
await open_path(cut_folders + i + 1, $current_file_path);
|
||||
}}
|
||||
>
|
||||
<ChevronRight class="shrink-0 text-stone-500 h-full" />
|
||||
|
||||
@@ -10,12 +10,12 @@
|
||||
close_function,
|
||||
className = '',
|
||||
snippet_container_class = ''
|
||||
} = $props<{
|
||||
}: {
|
||||
content: PopupContent;
|
||||
close_function: () => void;
|
||||
className?: string;
|
||||
snippet_container_class?: string;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
function try_to_close() {
|
||||
if (!content.closable || !content.open) return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script lang="ts">
|
||||
let {className = ''} = $props<{className?: string}>();
|
||||
let { className = '' }: { className?: string } = $props();
|
||||
</script>
|
||||
|
||||
<svg
|
||||
|
||||
@@ -16,28 +16,28 @@
|
||||
extension = null,
|
||||
enter_mode = 'none',
|
||||
enter_function = null
|
||||
} = $props<{
|
||||
}: {
|
||||
current_value: string;
|
||||
current_valid: boolean;
|
||||
className?: string;
|
||||
bg?: string;
|
||||
title: string;
|
||||
placeholder?: string;
|
||||
is_valid_function?: ((input: string) => [boolean, string]) | null;
|
||||
is_valid_function?: ((input: string) => [boolean, string] | Promise<[boolean, string]>) | null;
|
||||
focused_on_start?: boolean;
|
||||
extension?: string | null;
|
||||
enter_mode?: 'none' | 'focus_next' | 'submit';
|
||||
enter_function?: (() => void) | null;
|
||||
}>();
|
||||
} = $props();
|
||||
|
||||
let focus_bg = get_shifted_color(bg, 100);
|
||||
let focused: boolean = $state(false);
|
||||
let current_info = $state('');
|
||||
let input_element: HTMLInputElement;
|
||||
|
||||
function validate_input() {
|
||||
async function validate_input() {
|
||||
if (!is_valid_function) return;
|
||||
[current_valid, current_info] = is_valid_function(current_value.trim());
|
||||
[current_valid, current_info] = await is_valid_function(current_value.trim());
|
||||
}
|
||||
|
||||
function get_highlighting_string(): string {
|
||||
@@ -72,12 +72,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
validate_input();
|
||||
onMount(async () => {
|
||||
await validate_input();
|
||||
if (focused_on_start && input_element) input_element.focus();
|
||||
|
||||
selected_display_ids.subscribe(() => {
|
||||
validate_input();
|
||||
selected_display_ids.subscribe(async () => {
|
||||
await validate_input();
|
||||
});
|
||||
});
|
||||
</script>
|
||||
|
||||
@@ -114,11 +114,11 @@
|
||||
};
|
||||
}
|
||||
|
||||
function show_text() {
|
||||
async function show_text() {
|
||||
const html =
|
||||
editor_state.editor?.getHTML() +
|
||||
`<style>:root {--background-color: ${color_states.bg.value} !important;}
|
||||
</style>`;
|
||||
</style>`;
|
||||
await run_on_all_selected_displays(show_html, true, html);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user