mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-05 16:37: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:
Generated
-18
@@ -1506,7 +1506,6 @@
|
||||
"integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@standard-schema/spec": "^1.0.0",
|
||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||
@@ -1546,7 +1545,6 @@
|
||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||
"debug": "^4.4.1",
|
||||
@@ -1903,7 +1901,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.11.0.tgz",
|
||||
"integrity": "sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -2105,7 +2102,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.11.0.tgz",
|
||||
"integrity": "sha512-4Ane7VCVZ+GFOQNuy2nMP+SoWH7EemC3geTTqvgHm1H0tbSosxLJAVaZ9dF06F35RJmYCm+jLJUhRVd156eCRQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -2238,7 +2234,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.11.0.tgz",
|
||||
"integrity": "sha512-g43beA73ZMLezez1st9LEwYrRHZ0FLzlsSlOZKk7sdmtHLmuqWHf4oyb0XAHol1HZIdGv104rYaGNgmQXr1ecQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/ueberdosis"
|
||||
@@ -2253,7 +2248,6 @@
|
||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.11.0.tgz",
|
||||
"integrity": "sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-changeset": "^2.3.0",
|
||||
"prosemirror-collab": "^1.3.1",
|
||||
@@ -2403,7 +2397,6 @@
|
||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "8.46.0",
|
||||
"@typescript-eslint/types": "8.46.0",
|
||||
@@ -2621,7 +2614,6 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -3018,7 +3010,6 @@
|
||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.8.0",
|
||||
"@eslint-community/regexpp": "^4.12.1",
|
||||
@@ -4198,7 +4189,6 @@
|
||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -4226,7 +4216,6 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.11",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -4346,7 +4335,6 @@
|
||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"prettier": "bin/prettier.cjs"
|
||||
},
|
||||
@@ -4480,7 +4468,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"orderedmap": "^2.0.0"
|
||||
}
|
||||
@@ -4510,7 +4497,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.0.0",
|
||||
"prosemirror-transform": "^1.0.0",
|
||||
@@ -4559,7 +4545,6 @@
|
||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz",
|
||||
"integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"prosemirror-model": "^1.20.0",
|
||||
"prosemirror-state": "^1.0.0",
|
||||
@@ -5222,7 +5207,6 @@
|
||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz",
|
||||
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jridgewell/remapping": "^2.3.4",
|
||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||
@@ -5472,7 +5456,6 @@
|
||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"tsc": "bin/tsc",
|
||||
"tsserver": "bin/tsserver"
|
||||
@@ -5541,7 +5524,6 @@
|
||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"esbuild": "^0.25.0",
|
||||
"fdir": "^6.5.0",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -1 +1,2 @@
|
||||
export const prerender = true;
|
||||
export const ssr = false;
|
||||
@@ -10,7 +10,6 @@
|
||||
import TextInput from '../components/TextInput.svelte';
|
||||
import {
|
||||
add_display,
|
||||
displays,
|
||||
edit_display_data,
|
||||
get_display_by_id,
|
||||
is_display_name_taken,
|
||||
@@ -35,6 +34,7 @@
|
||||
title_class: '!text-xl',
|
||||
closable: true
|
||||
});
|
||||
let remove_display_name = $state('');
|
||||
|
||||
const text_inputs_valid_null_values = {
|
||||
name: { valid: false, value: '' },
|
||||
@@ -81,7 +81,8 @@
|
||||
};
|
||||
};
|
||||
|
||||
const show_remove_display_popup = (display_id: string) => {
|
||||
const show_remove_display_popup = async (display_id: string) => {
|
||||
remove_display_name = (await get_display_by_id(display_id))?.name || '?';
|
||||
popup_content = {
|
||||
open: true,
|
||||
snippet: remove_display_popup,
|
||||
@@ -93,8 +94,8 @@
|
||||
};
|
||||
};
|
||||
|
||||
const show_edit_display_popup = (display_id: string) => {
|
||||
const display = get_display_by_id(display_id, $displays);
|
||||
const show_edit_display_popup = async (display_id: string) => {
|
||||
const display = await get_display_by_id(display_id);
|
||||
if (!display) return;
|
||||
// insert existing values in text_inputs_valid
|
||||
for (const key of Object.keys(text_inputs_valid) as (keyof typeof text_inputs_valid)[]) {
|
||||
@@ -112,17 +113,14 @@
|
||||
};
|
||||
};
|
||||
|
||||
onMount(() => {
|
||||
on_start();
|
||||
});
|
||||
onMount(on_start);
|
||||
</script>
|
||||
|
||||
{#snippet remove_display_popup(display_id: string)}
|
||||
<div class="max-w-prose px-2">
|
||||
Soll der Bildschirm <HighlightedText
|
||||
>{get_display_by_id(display_id, $displays)?.name || '?'}</HighlightedText
|
||||
> wirklich gelöscht werden? Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation
|
||||
auf dem Gerät bleibt bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
|
||||
Soll der Bildschirm <HighlightedText>{remove_display_name}</HighlightedText> wirklich gelöscht werden?
|
||||
Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation auf dem Gerät bleibt
|
||||
bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
|
||||
</div>
|
||||
<div class="flex flex-row justify-end gap-2">
|
||||
<Button className="px-4 font-bold" click_function={popup_close_function}>Abbrechen</Button>
|
||||
@@ -130,8 +128,8 @@
|
||||
hover_bg="bg-red-400"
|
||||
active_bg="bg-red-500"
|
||||
className="px-4 flex text-red-400 hover:text-stone-100"
|
||||
click_function={() => {
|
||||
remove_display(display_id);
|
||||
click_function={async () => {
|
||||
await remove_display(display_id);
|
||||
popup_close_function();
|
||||
}}>Löschen</Button
|
||||
>
|
||||
@@ -145,13 +143,13 @@
|
||||
bind:current_valid={text_inputs_valid.name.valid}
|
||||
title="Anzeigename"
|
||||
placeholder="z.B. Beamer vorne links"
|
||||
is_valid_function={(input: string) => {
|
||||
is_valid_function={async (input: string) => {
|
||||
if (!!existing_display_id) {
|
||||
if (input === get_display_by_id(existing_display_id, $displays)?.name)
|
||||
if (input === (await get_display_by_id(existing_display_id))?.name)
|
||||
return [true, 'Gültiger Name'];
|
||||
}
|
||||
if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
|
||||
if (is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
||||
if (await is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
||||
return [true, 'Gültiger Name'];
|
||||
}}
|
||||
enter_mode="focus_next"
|
||||
@@ -199,8 +197,8 @@
|
||||
: [false, 'Ungültige MAC-Adresse'];
|
||||
}}
|
||||
enter_mode="submit"
|
||||
enter_function={() => {
|
||||
finalize_add_edit_display(existing_display_id);
|
||||
enter_function={async () => {
|
||||
await finalize_add_edit_display(existing_display_id);
|
||||
}}
|
||||
/>
|
||||
<div class="flex flex-row gap-2 justify-end pt-2">
|
||||
@@ -212,8 +210,8 @@
|
||||
disabled={!all_text_inputs_valid()}
|
||||
className="{!!existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
|
||||
bg="bg-stone-650"
|
||||
click_function={() => {
|
||||
finalize_add_edit_display(existing_display_id);
|
||||
click_function={async () => {
|
||||
await finalize_add_edit_display(existing_display_id);
|
||||
}}
|
||||
>{#if !!existing_display_id}
|
||||
Speichern
|
||||
|
||||
Regular → Executable
+244
-187
@@ -1,49 +1,58 @@
|
||||
import { notifications } from "./stores/notification";
|
||||
import { to_display_status, type DisplayStatus, type FolderElement, type RequestResponse, type ShellCommandResponse, type TreeElement } from "./types";
|
||||
import { get_uuid } from "./utils";
|
||||
import { notifications } from './stores/notification';
|
||||
import {
|
||||
to_display_status,
|
||||
type DisplayStatus,
|
||||
type Inode,
|
||||
type RequestResponse,
|
||||
type ShellCommandResponse,
|
||||
type TreeElement
|
||||
} from './types';
|
||||
|
||||
export async function get_screenshot(ip: string): Promise<Blob | null> {
|
||||
const options = { method: 'PATCH' };
|
||||
const response = await request_display(ip, '/takeScreenshot', options);
|
||||
if (!response.ok || !response.blob) return null;
|
||||
return response.blob;
|
||||
const options = { method: 'PATCH' };
|
||||
const response = await request_display(ip, '/takeScreenshot', options);
|
||||
if (!response.ok || !response.blob) return null;
|
||||
return response.blob;
|
||||
}
|
||||
|
||||
export async function open_file(ip: string, path_to_file: string): Promise<void> {
|
||||
const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } };
|
||||
await request_display(ip, `/file${path_to_file}`, options);
|
||||
const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } };
|
||||
await request_display(ip, `/file${path_to_file}`, options);
|
||||
}
|
||||
|
||||
export async function send_keyboard_input(ip: string, key: string): Promise<void> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: key,
|
||||
}),
|
||||
};
|
||||
await request_display(ip, '/keyboardInput', options);
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: key
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/keyboardInput', options);
|
||||
}
|
||||
|
||||
export async function show_html(ip: string, html: string) {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: html
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/showHTML', options);
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: html
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/showHTML', options);
|
||||
}
|
||||
|
||||
export async function get_file_data(ip: string, path: string): Promise<FolderElement[] | null> {
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
size: string;
|
||||
created: string;
|
||||
}
|
||||
const command = `cd ".${path}" && find . -maxdepth 1 -mindepth 1 -print0 | while IFS= read -r -d '' f; do
|
||||
export async function get_file_data(
|
||||
ip: string,
|
||||
path: string
|
||||
): Promise<{ folder_element: Inode; date_created: Date }[] | null> {
|
||||
interface FileInfo {
|
||||
name: string;
|
||||
type: string;
|
||||
size: string;
|
||||
created: string;
|
||||
}
|
||||
const command = `cd ".${path}" && find . -maxdepth 1 -mindepth 1 -print0 | while IFS= read -r -d '' f; do
|
||||
typ=$(file -b --mime-type -- "$f")
|
||||
size=$(stat -c '%s' -- "$f")
|
||||
created=$(stat -c '%w' -- "$f")
|
||||
@@ -52,194 +61,242 @@ export async function get_file_data(ip: string, path: string): Promise<FolderEle
|
||||
'{name:$name, type:$type, size:($size|tostring), created:$created}' | tr -d '\n'
|
||||
echo
|
||||
done
|
||||
`
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
if (json_response.exitCode === 0 && json_response.stdout.trim() === '') return [];
|
||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||
`;
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
if (json_response.exitCode === 0 && json_response.stdout.trim() === '') return [];
|
||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||
|
||||
const response: FileInfo[] = json_response.stdout.trim()
|
||||
.split("\n")
|
||||
.filter(Boolean)
|
||||
.map((line: string) => JSON.parse(line) as FileInfo);
|
||||
const response: FileInfo[] = json_response.stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line: string) => JSON.parse(line) as FileInfo);
|
||||
|
||||
const folder_element_list: FolderElement[] = [];
|
||||
const folder_element_list: { folder_element: Inode; date_created: Date }[] = [];
|
||||
|
||||
for (const response_element of response) {
|
||||
// filter hidden files (start with '.' -> './.config')
|
||||
if (response_element.name.charAt(2) !== '.') {
|
||||
const truncated = {
|
||||
...response_element,
|
||||
created: response_element.created.slice(0, 16) // truncated to YYYY-MM-DD hh-mm -> no (milli)seconds
|
||||
};
|
||||
|
||||
const folder_element: FolderElement = {
|
||||
id: get_uuid(),
|
||||
hash: JSON.stringify(truncated),
|
||||
name: response_element.name.slice(2), // remove "./"
|
||||
type: response_element.type,
|
||||
date_created: new Date(response_element.created),
|
||||
size: Number(response_element.size),
|
||||
};
|
||||
folder_element_list.push(folder_element);
|
||||
}
|
||||
}
|
||||
return folder_element_list;
|
||||
for (const response_element of response) {
|
||||
// filter hidden files (start with '.' -> './.config')
|
||||
if (response_element.name.charAt(2) === '.') continue;
|
||||
const folder_element: Inode = {
|
||||
path: path,
|
||||
name: response_element.name.slice(2), // remove "./"
|
||||
type: response_element.type,
|
||||
size: Number(response_element.size),
|
||||
thumbnail: null
|
||||
};
|
||||
folder_element_list.push({ folder_element, date_created: new Date(response_element.created) });
|
||||
}
|
||||
return folder_element_list;
|
||||
}
|
||||
|
||||
export async function get_file_tree_data(ip: string, path: string): Promise<TreeElement[] | null> {
|
||||
const command = `cd ".${path}" && tree -Js`
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
const command = `cd ".${path}" && tree -Js`;
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||
|
||||
return (JSON.parse(json_response.stdout.trim()) as [TreeElement, any])[0].contents || null;
|
||||
const tree_element: TreeElement | null = JSON.parse(json_response.stdout.trim())[0] || null;
|
||||
|
||||
return tree_element?.contents || null;
|
||||
}
|
||||
|
||||
export async function create_folders(ip: string, path: string, folder_names: string[]): Promise<void> {
|
||||
let command = `cd ".${path}"`;
|
||||
export async function create_folders(
|
||||
ip: string,
|
||||
path: string,
|
||||
folder_names: string[]
|
||||
): Promise<void> {
|
||||
let command = `cd ".${path}"`;
|
||||
|
||||
for (const part of folder_names) {
|
||||
command += ` && mkdir "${part}" && cd "${part}/"`;
|
||||
}
|
||||
for (const part of folder_names) {
|
||||
command += ` && mkdir "${part}" && cd "${part}/"`;
|
||||
}
|
||||
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
}
|
||||
|
||||
export async function rename_file(ip: string, path: string, old_file_name: string, new_file_name: string): Promise<void> {
|
||||
const command: string = `cd ".${path}" && mv "${old_file_name}" "${new_file_name}"`;
|
||||
export async function rename_file(
|
||||
ip: string,
|
||||
path: string,
|
||||
old_file_name: string,
|
||||
new_file_name: string
|
||||
): Promise<void> {
|
||||
const command: string = `cd ".${path}" && mv "${old_file_name}" "${new_file_name}"`;
|
||||
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
}
|
||||
|
||||
export async function delete_files(ip: string, current_path: string, file_names: string[]): Promise<void> {
|
||||
let command: string = `cd ".${current_path}"`;
|
||||
for (const file_name of file_names) {
|
||||
command += ` && rm -r "${file_name}"`;
|
||||
}
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
export async function delete_files(
|
||||
ip: string,
|
||||
current_path: string,
|
||||
file_names: string[]
|
||||
): Promise<void> {
|
||||
let command: string = `cd ".${current_path}"`;
|
||||
for (const file_name of file_names) {
|
||||
command += ` && rm -r "${file_name}"`;
|
||||
}
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
}
|
||||
|
||||
|
||||
export async function show_blackscreen(ip: string): Promise<void> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: ``
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/showHTML', options);
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: ``
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/showHTML', options);
|
||||
}
|
||||
|
||||
export async function get_thumbnail_blob(ip: string, path_to_file: string): Promise<Blob | null> {
|
||||
const raw_response = await request_display(ip, `/file/preview${path_to_file}`, { method: 'GET' }, true, [415]);
|
||||
if (!raw_response.ok || !raw_response.blob) return null
|
||||
return raw_response.blob;
|
||||
const raw_response = await request_display(
|
||||
ip,
|
||||
`/file/preview${path_to_file}`,
|
||||
{ method: 'GET' },
|
||||
true,
|
||||
[415]
|
||||
);
|
||||
if (!raw_response.ok || !raw_response.blob) return null;
|
||||
return raw_response.blob;
|
||||
}
|
||||
|
||||
export async function ping_ip(ip: string): Promise<DisplayStatus> {
|
||||
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
return raw_response.json.status ? to_display_status(raw_response.json.status) : null;
|
||||
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
|
||||
const status = raw_response.json.status;
|
||||
if (typeof status === 'string') {
|
||||
return to_display_status(status);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
async function request_display(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise<RequestResponse> {
|
||||
const url = `http://${ip}:1323/api${api_route}`;
|
||||
return await request(url, options, log_in_debug, supress_error_handling_http_codes);
|
||||
async function request_display(
|
||||
ip: string,
|
||||
api_route: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||
log_in_debug: boolean = false,
|
||||
supress_error_handling_http_codes: number[] = []
|
||||
): Promise<RequestResponse> {
|
||||
const url = `http://${ip}:1323/api${api_route}`;
|
||||
return await request(url, options, log_in_debug, supress_error_handling_http_codes);
|
||||
}
|
||||
|
||||
async function request_control(api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }): Promise<RequestResponse> {
|
||||
const url = `${window.location.origin}/api${api_route}`;
|
||||
return await request(url, options);
|
||||
async function request_control(
|
||||
api_route: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string }
|
||||
): Promise<RequestResponse> {
|
||||
const url = `${window.location.origin}/api${api_route}`;
|
||||
return await request(url, options);
|
||||
}
|
||||
|
||||
async function request(
|
||||
url: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||
log_in_debug: boolean = false,
|
||||
supress_error_handling_http_codes: number[] = []
|
||||
): Promise<RequestResponse> {
|
||||
try {
|
||||
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
||||
if (log_in_debug) {
|
||||
console.debug(url + cache_buster, options.body);
|
||||
} else {
|
||||
console.log(url + cache_buster, options.body);
|
||||
}
|
||||
const response = await fetch(url + cache_buster, options);
|
||||
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
let request_response: RequestResponse;
|
||||
if (!contentType.includes('application/json')) {
|
||||
const blob: Blob = await response.blob();
|
||||
request_response = { ok: response.ok, http_code: response.status, blob: blob };
|
||||
} else {
|
||||
const json: Record<string, unknown> = await response.json();
|
||||
request_response = { ok: response.ok, http_code: response.status, json: json };
|
||||
}
|
||||
if (log_in_debug) {
|
||||
console.debug(request_response);
|
||||
} else {
|
||||
console.log(request_response);
|
||||
}
|
||||
return request_response;
|
||||
}
|
||||
|
||||
async function request(url: string, options: { method: string, headers?: Record<string, string>, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise<RequestResponse> {
|
||||
try {
|
||||
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
||||
if (log_in_debug) {
|
||||
console.debug(url + cache_buster, options.body);
|
||||
} else {
|
||||
console.log(url + cache_buster, options.body);
|
||||
}
|
||||
const response = await fetch(url + cache_buster, options);
|
||||
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
|
||||
const contentType = response.headers.get("content-type") || "";
|
||||
let request_response: RequestResponse;
|
||||
if (!contentType.includes("application/json")) {
|
||||
const blob: Blob = await response.blob();
|
||||
request_response = { ok: response.ok, http_code: response.status, blob: blob };
|
||||
} else {
|
||||
const json: Object = await response.json();
|
||||
request_response = { ok: response.ok, http_code: response.status, json: json };
|
||||
}
|
||||
if (log_in_debug) {
|
||||
console.debug(request_response);
|
||||
} else {
|
||||
console.log(request_response);
|
||||
}
|
||||
return request_response;
|
||||
}
|
||||
|
||||
let error_description: string;
|
||||
try {
|
||||
const json: { description: string } = await response.json();
|
||||
error_description = json.description;
|
||||
} catch (error_on_json_parsing: any) {
|
||||
error_description = `unknown error: ${error_on_json_parsing}`;
|
||||
}
|
||||
console.error(url, error_description);
|
||||
notifications.push("error", `Fehler ${response.status} bei API-Anfrage`, `${url}\nFehler: ${error_description}`);
|
||||
} catch (error: any) {
|
||||
if (error instanceof TypeError && /fetch|NetworkError/i.test(error.message)) {
|
||||
console.log("Request failed - Is the targeted device online?")
|
||||
} else {
|
||||
console.error(url, error);
|
||||
notifications.push("error", `Fataler Fehler bei API-Anfrage`, `${url}\nFehler: ${error}`);
|
||||
}
|
||||
}
|
||||
return { ok: false };
|
||||
let error_description: string;
|
||||
try {
|
||||
const json: { description: string } = await response.json();
|
||||
error_description = json.description;
|
||||
} catch (error_on_json_parsing: unknown) {
|
||||
error_description = `unknown error: ${error_on_json_parsing}`;
|
||||
}
|
||||
console.error(url, error_description);
|
||||
notifications.push(
|
||||
'error',
|
||||
`Fehler ${response.status} bei API-Anfrage`,
|
||||
`${url}\nFehler: ${error_description}`
|
||||
);
|
||||
} catch (error: unknown) {
|
||||
if (error instanceof TypeError && /fetch|NetworkError/i.test(error.message)) {
|
||||
console.log('Request failed - Is the targeted device online?');
|
||||
} else {
|
||||
console.error(url, error);
|
||||
notifications.push('error', `Fataler Fehler bei API-Anfrage`, `${url}\nFehler: ${error}`);
|
||||
}
|
||||
}
|
||||
return { ok: false };
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
function handle_shell_error(ip: string, shell_response: ShellCommandResponse, shell_command: string, command_includs_cd: boolean): boolean {
|
||||
if (shell_response.exitCode !== 0) {
|
||||
if (command_includs_cd && shell_response.stderr && /bash: line \d+: cd: .+: No such file or directory/.test(shell_response.stderr)) {
|
||||
console.log("current file_path does not exist on display:", ip);
|
||||
return true;
|
||||
}
|
||||
console.error(shell_response);
|
||||
notifications.push("error", `Fehler ${shell_response.exitCode} in API-Shell`, `${ip}\n${shell_command}\nFehler: ${shell_response.stderr}`);
|
||||
return true;
|
||||
}
|
||||
if (shell_response.stdout.trim() === '') return true;
|
||||
return false;
|
||||
function handle_shell_error(
|
||||
ip: string,
|
||||
shell_response: ShellCommandResponse,
|
||||
shell_command: string,
|
||||
command_includs_cd: boolean
|
||||
): boolean {
|
||||
if (shell_response.exitCode !== 0) {
|
||||
if (
|
||||
command_includs_cd &&
|
||||
shell_response.stderr &&
|
||||
/bash: line \d+: cd: .+: No such file or directory/.test(shell_response.stderr)
|
||||
) {
|
||||
console.log('current file_path does not exist on display:', ip);
|
||||
return true;
|
||||
}
|
||||
console.error(shell_response);
|
||||
notifications.push(
|
||||
'error',
|
||||
`Fehler ${shell_response.exitCode} in API-Shell`,
|
||||
`${ip}\n${shell_command}\nFehler: ${shell_response.stderr}`
|
||||
);
|
||||
return true;
|
||||
}
|
||||
if (shell_response.stdout.trim() === '') return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
async function run_shell_command(ip: string, command: string, log_in_debug: boolean = false): Promise<RequestResponse> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
command: command
|
||||
})
|
||||
};
|
||||
return await request_display(ip, '/shellCommand', options, log_in_debug);
|
||||
}
|
||||
async function run_shell_command(
|
||||
ip: string,
|
||||
command: string,
|
||||
log_in_debug: boolean = false
|
||||
): Promise<RequestResponse> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
command: command
|
||||
})
|
||||
};
|
||||
return await request_display(ip, '/shellCommand', options, log_in_debug);
|
||||
}
|
||||
|
||||
Executable
+56
@@ -0,0 +1,56 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import type { Display, DisplayGroup, Inode } from './types';
|
||||
|
||||
export interface FileOnDisplay {
|
||||
display_id: string;
|
||||
file_primary_key: string; // JSON.stringify([string, string, number, string])
|
||||
date_created: Date;
|
||||
is_loading: boolean;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export class FileDatabase extends Dexie {
|
||||
files!: Table<Inode, [string, string, number, string]>;
|
||||
files_on_display!: Table<FileOnDisplay, [string, string]>;
|
||||
displays!: Table<Display, string>;
|
||||
display_groups!: Table<DisplayGroup, string>;
|
||||
|
||||
constructor() {
|
||||
super('FileDatabase');
|
||||
|
||||
this.version(1).stores({
|
||||
files: `
|
||||
[path+name+size+type],
|
||||
path,
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
thumbnail
|
||||
`,
|
||||
files_on_display: `
|
||||
[display_id+file_primary_key],
|
||||
display_id,
|
||||
file_primary_key,
|
||||
date_created,
|
||||
is_loading,
|
||||
percentage
|
||||
`,
|
||||
displays: `
|
||||
id,
|
||||
ip,
|
||||
mac,
|
||||
position,
|
||||
preview,
|
||||
group_id,
|
||||
name,
|
||||
status
|
||||
`,
|
||||
display_groups: `
|
||||
id,
|
||||
position
|
||||
`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new FileDatabase();
|
||||
@@ -1,20 +0,0 @@
|
||||
import Dexie, { type EntityTable } from "dexie";
|
||||
|
||||
export interface ThumbnailBlobDBEntry {
|
||||
hash: string;
|
||||
blob: Blob;
|
||||
}
|
||||
|
||||
export class FileDatabase extends Dexie {
|
||||
thumbnail_blobs!: EntityTable<ThumbnailBlobDBEntry, "hash">; // (Type, PrimaryKey)
|
||||
|
||||
constructor() {
|
||||
super("FileDatabase");
|
||||
|
||||
this.version(1).stores({
|
||||
thumbnail_blobs: "++hash, blob" // primary key
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new FileDatabase();
|
||||
Regular → Executable
+24
-24
@@ -1,34 +1,34 @@
|
||||
import { get } from "svelte/store";
|
||||
import { displays, run_on_all_selected_displays, update_displays_with_map, update_screenshot } from "./stores/displays"
|
||||
import { ping_ip } from "./api_handler";
|
||||
import type { Display } from "./types";
|
||||
import { change_file_path, update_folder_elements_recursively } from "./stores/files";
|
||||
import { db } from "./indexdb/file_thumbnails.db";
|
||||
import { screenshot_loop } from './stores/displays';
|
||||
import { ping_ip } from './api_handler';
|
||||
import type { Display } from './types';
|
||||
import { update_folder_elements_recursively } from './stores/files';
|
||||
import { db } from './files_display.db';
|
||||
|
||||
const update_display_status_interval_seconds = 20;
|
||||
|
||||
export async function on_start() {
|
||||
await db.thumbnail_blobs.clear();
|
||||
await update_all_display_status();
|
||||
await setInterval(update_all_display_status, update_display_status_interval_seconds * 1000);
|
||||
|
||||
await db.files.clear();
|
||||
await db.files_on_display.clear();
|
||||
await update_all_display_status();
|
||||
await setInterval(update_all_display_status, update_display_status_interval_seconds * 1000);
|
||||
}
|
||||
|
||||
|
||||
async function update_all_display_status() {
|
||||
await update_displays_with_map(async (display: Display) => {
|
||||
const new_status = await ping_ip(display.ip);
|
||||
if (new_status === null && display.status !== null) return display;
|
||||
if (new_status === "app_online" && display.status !== "app_online") {
|
||||
await on_display_start(display);
|
||||
}
|
||||
return { ...display, status: new_status, };
|
||||
});
|
||||
console.log("Display Status updated")
|
||||
const all_displays: Display[] = await db.displays.toArray();
|
||||
for (const display of all_displays) {
|
||||
const new_status = await ping_ip(display.ip);
|
||||
if (new_status === null && display.status !== null) continue;
|
||||
if (new_status === 'app_online' && display.status !== 'app_online') {
|
||||
on_display_start(display);
|
||||
}
|
||||
if (new_status !== display.status) {
|
||||
display.status = new_status;
|
||||
await db.displays.put(display); // save
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
async function on_display_start(display: Display) {
|
||||
await update_folder_elements_recursively(display, '/');
|
||||
await update_screenshot(display.id);
|
||||
}
|
||||
await update_folder_elements_recursively(display, '/');
|
||||
await screenshot_loop(display.id);
|
||||
}
|
||||
|
||||
Regular → Executable
+168
-163
@@ -1,191 +1,196 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { Display, DisplayGroup, DisplayStatus } from "../types";
|
||||
import { is_selected, select, selected_display_ids } from "./select";
|
||||
import { get_uuid, image_content_hash } from "../utils";
|
||||
import { get_screenshot } from "../api_handler";
|
||||
import { filter_file_selection_for_current_selected_displays } from "./files";
|
||||
import { get } from 'svelte/store';
|
||||
import type { Display, DisplayGroup, DisplayStatus } from '../types';
|
||||
import { is_selected, select, selected_display_ids } from './select';
|
||||
import { get_uuid, image_content_hash } from '../utils';
|
||||
import { get_screenshot } from '../api_handler';
|
||||
import { delete_and_deselect_unique_files_from_display } from './files';
|
||||
import { db } from '../files_display.db';
|
||||
|
||||
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
|
||||
id: get_uuid(),
|
||||
data: []
|
||||
}]);
|
||||
|
||||
|
||||
export function is_display_name_taken(name: string): boolean {
|
||||
const display_groups = get(displays);
|
||||
return display_groups.some(group =>
|
||||
group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase())
|
||||
);
|
||||
export async function is_display_name_taken(name: string): Promise<boolean> {
|
||||
const exists = await db.displays.where('name').equals(name).first();
|
||||
return !!exists;
|
||||
}
|
||||
|
||||
export function add_display(ip: string, mac: string | null, name: string, status: DisplayStatus) {
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
|
||||
return displays;
|
||||
});
|
||||
export async function add_display(
|
||||
ip: string,
|
||||
mac: string | null,
|
||||
name: string,
|
||||
status: DisplayStatus
|
||||
) {
|
||||
if (await is_display_name_taken(name)) return;
|
||||
const new_id = get_uuid();
|
||||
const group = await db.display_groups.toCollection().first();
|
||||
let group_id: string;
|
||||
if (group) {
|
||||
group_id = group.id;
|
||||
console.log('DISPLAYGROUP WURDE NICHT ERSTELLT');
|
||||
} else {
|
||||
group_id = get_uuid();
|
||||
await db.display_groups.put({ id: group_id, position: 0 });
|
||||
console.log('DISPLAYGROUP WURDE ERSTELLT');
|
||||
}
|
||||
const element_count_in_group = (await db.displays.where('group_id').equals(group_id).toArray())
|
||||
.length;
|
||||
await db.displays.put({
|
||||
id: new_id,
|
||||
ip,
|
||||
mac,
|
||||
position: element_count_in_group,
|
||||
preview: { currently_updating: false, url: null },
|
||||
group_id: group_id,
|
||||
name,
|
||||
status
|
||||
});
|
||||
}
|
||||
|
||||
export async function edit_display_data(display_id: string, ip: string, mac: string | null, name: string) {
|
||||
await update_displays_with_map((display: Display) => {
|
||||
if (display.id !== display_id) return display;
|
||||
return { ...display, ip: ip, mac: mac, name: name };
|
||||
})
|
||||
export async function edit_display_data(
|
||||
display_id: string,
|
||||
ip: string,
|
||||
mac: string | null,
|
||||
name: string
|
||||
) {
|
||||
let display = await db.displays.get(display_id);
|
||||
if (!display) return;
|
||||
display = { ...display, ip: ip, mac: mac, name: name };
|
||||
await db.displays.put(display); // save
|
||||
}
|
||||
|
||||
export function remove_display(display_id: string) {
|
||||
select(selected_display_ids, display_id, false);
|
||||
filter_file_selection_for_current_selected_displays();
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
displays = displays.map(display_group => ({
|
||||
...display_group,
|
||||
data: display_group.data.filter(display => display.id !== display_id)
|
||||
}));
|
||||
return displays;
|
||||
});
|
||||
export async function remove_display(display_id: string) {
|
||||
select(selected_display_ids, display_id, 'deselect');
|
||||
await delete_and_deselect_unique_files_from_display(display_id);
|
||||
|
||||
// TODO remove ID from Files usw.
|
||||
const group_id = (await db.displays.get(display_id))?.group_id;
|
||||
await db.displays.delete(display_id);
|
||||
if (group_id && (await db.displays.where('group_id').equals(group_id).toArray()).length === 0) {
|
||||
await db.display_groups.delete(group_id); // delete empty group
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
if (!is_selected(display.id, current_selected_displays)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
export async function all_displays_of_group_selected(
|
||||
display_group_id: string,
|
||||
current_selected_displays: string[]
|
||||
): Promise<boolean> {
|
||||
const displays_of_group: Display[] = await db.displays
|
||||
.where('group_id')
|
||||
.equals(display_group_id)
|
||||
.toArray();
|
||||
if (displays_of_group.length === 0) return false;
|
||||
|
||||
for (const display of displays_of_group) {
|
||||
if (!is_selected(display.id, current_selected_displays)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export function select_all_of_group(display_group: DisplayGroup, new_value: boolean | null = null) {
|
||||
for (const display of display_group.data) {
|
||||
select(selected_display_ids, display.id, new_value);
|
||||
}
|
||||
filter_file_selection_for_current_selected_displays();
|
||||
export async function select_all_of_group(
|
||||
display_group_id: string,
|
||||
new_value: boolean | null = null
|
||||
) {
|
||||
const displays_of_group: Display[] = await db.displays
|
||||
.where('group_id')
|
||||
.equals(display_group_id)
|
||||
.toArray();
|
||||
for (const display of displays_of_group) {
|
||||
let action: string;
|
||||
if (new_value === true) {
|
||||
action = 'select';
|
||||
} else {
|
||||
action = 'deselect';
|
||||
}
|
||||
|
||||
select(selected_display_ids, display.id, action as 'toggle' | 'select' | 'deselect');
|
||||
}
|
||||
}
|
||||
|
||||
export function set_new_display_group_data(display_group_id: string, new_data: Display[]) {
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
for (const display_group of displays) {
|
||||
if (display_group.id === display_group_id) {
|
||||
display_group.data = new_data;
|
||||
}
|
||||
}
|
||||
return displays;
|
||||
});
|
||||
export async function get_display_by_id(display_id: string): Promise<Display | null> {
|
||||
return (await db.displays.get(display_id)) ?? null;
|
||||
}
|
||||
|
||||
export function get_display_by_id(display_id: string, display_group_array: DisplayGroup[]) {
|
||||
const displays_array = display_group_array;
|
||||
for (const display_group of displays_array) {
|
||||
for (const display of display_group.data) {
|
||||
if (display.id === display_id) {
|
||||
return display;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
export async function screenshot_loop(display_id: string, initial_retry_count: number = 5) {
|
||||
const display = await db.displays.get(display_id);
|
||||
if (!display || display.preview.currently_updating) return;
|
||||
|
||||
display.preview.currently_updating = true;
|
||||
await db.displays.update(display.id, { preview: display.preview });
|
||||
|
||||
let last_hash: number | null = null;
|
||||
|
||||
let retry_count = initial_retry_count;
|
||||
while (retry_count > 0) {
|
||||
retry_count -= 1;
|
||||
|
||||
const new_blob = await get_screenshot(display.ip);
|
||||
if (!new_blob) {
|
||||
display.preview = { currently_updating: false, url: null };
|
||||
await db.displays.update(display.id, { preview: display.preview });
|
||||
return;
|
||||
}
|
||||
const new_hash = await image_content_hash(new_blob);
|
||||
if (last_hash !== new_hash) {
|
||||
if (display.preview.url) {
|
||||
URL.revokeObjectURL(display.preview.url);
|
||||
}
|
||||
|
||||
last_hash = new_hash;
|
||||
display.preview.url = URL.createObjectURL(new_blob);
|
||||
await db.displays.update(display.id, { preview: display.preview });
|
||||
|
||||
retry_count = initial_retry_count;
|
||||
}
|
||||
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000)); // sleep 2s
|
||||
}
|
||||
|
||||
display.preview.currently_updating = false;
|
||||
await db.displays.update(display.id, { preview: display.preview });
|
||||
}
|
||||
|
||||
|
||||
export function add_empty_display_group() {
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
displays.push({
|
||||
id: get_uuid(),
|
||||
data: [],
|
||||
});
|
||||
return displays;
|
||||
});
|
||||
export async function run_on_all_selected_displays<T extends unknown[]>(
|
||||
run_function: (ip: string, ...args: T) => void | Promise<void>,
|
||||
update_screenshot_afterwards: boolean,
|
||||
...args: T
|
||||
) {
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
const display_ip = (await get_display_by_id(display_id))?.ip;
|
||||
if (display_ip) {
|
||||
await run_function(display_ip, ...args);
|
||||
if (update_screenshot_afterwards) {
|
||||
await screenshot_loop(display_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export function remove_empty_display_groups() {
|
||||
displays.update((displays: DisplayGroup[]) => {
|
||||
for (let i = displays.length - 1; i >= 0; i--) {
|
||||
if (displays[i].data.length === 0) {
|
||||
displays.splice(i, 1);
|
||||
}
|
||||
}
|
||||
return displays;
|
||||
});
|
||||
export async function get_display_groups(): Promise<DisplayGroup[]> {
|
||||
return await db.display_groups.orderBy('position').toArray();
|
||||
}
|
||||
|
||||
export async function update_screenshot(display_id: string, check_type: "first_check" | "last_check_different" | "last_check_same" = "first_check") {
|
||||
const display_ip = get_display_by_id(display_id, get(displays))?.ip;
|
||||
if (!display_ip) return;
|
||||
const new_blob = await get_screenshot(display_ip);
|
||||
if (!new_blob) {
|
||||
update_displays_with_map((display: Display) => {
|
||||
if (display.id !== display_id) return display;
|
||||
return { ...display, preview_url: null, preview_timeout_id: null };
|
||||
})
|
||||
return;
|
||||
}
|
||||
const display = get_display_by_id(display_id, get(displays));
|
||||
|
||||
let update_needed = check_type === "first_check";
|
||||
if (check_type !== "first_check") {
|
||||
if (display && display.preview_url) {
|
||||
const old_blob = await fetch(display.preview_url).then(r => r.blob());
|
||||
const new_hash = await image_content_hash(new_blob);
|
||||
const old_hash = await image_content_hash(old_blob);
|
||||
update_needed = old_hash !== new_hash; // if different -> update
|
||||
}
|
||||
}
|
||||
|
||||
let new_preview_timeout_id: number | null = null;
|
||||
if (update_needed || check_type === "last_check_different") {
|
||||
new_preview_timeout_id = setTimeout(async () => { await update_screenshot(display_id, update_needed ? "last_check_different" : "last_check_same") }, 2 * 1000);
|
||||
}
|
||||
if (display?.preview_timeout_id) {
|
||||
clearInterval(display.preview_timeout_id);
|
||||
}
|
||||
|
||||
if (update_needed) {
|
||||
update_displays_with_map((display: Display) => {
|
||||
if (display.id !== display_id) return display;
|
||||
if (display.preview_url) {
|
||||
URL.revokeObjectURL(display.preview_url);
|
||||
}
|
||||
const new_url = URL.createObjectURL(new_blob);
|
||||
return { ...display, preview_url: new_url, preview_timeout_id: new_preview_timeout_id };
|
||||
})
|
||||
}
|
||||
export async function get_display_ids_in_group(display_group_id: string): Promise<Display[]> {
|
||||
const displays: Display[] = await db.displays
|
||||
.where('group_id')
|
||||
.equals(display_group_id)
|
||||
.sortBy('position');
|
||||
return displays;
|
||||
}
|
||||
|
||||
|
||||
export async function update_displays_with_map(update_function: (display: Display) => Display | Promise<Display>) {
|
||||
const display_groups = get(displays);
|
||||
const updated_groups = await Promise.all(
|
||||
display_groups.map(async (group: DisplayGroup) => ({
|
||||
...group,
|
||||
data: await Promise.all(group.data.map(update_function)),
|
||||
}))
|
||||
);
|
||||
displays.set(updated_groups);
|
||||
export async function set_new_display_order(new_ordered_items: Display[]) {
|
||||
for (let i = 0; i < new_ordered_items.length; i++) {
|
||||
new_ordered_items[i].position = i;
|
||||
await db.displays.put(new_ordered_items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
export async function run_on_all_selected_displays(run_function: ((ip: string, ...args: any[]) => void | Promise<void>), update_screenshot_afterwards: boolean, ...args: any[]) {
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
const display_ip = get_display_by_id(display_id, get(displays))?.ip;
|
||||
if (display_ip) {
|
||||
await run_function(display_ip, ...args)
|
||||
if (update_screenshot_afterwards) {
|
||||
await update_screenshot(display_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
export async function set_new_display_group_order(new_ordered_items: DisplayGroup[]) {
|
||||
for (let i = 0; i < new_ordered_items.length; i++) {
|
||||
new_ordered_items[i].position = i;
|
||||
await db.display_groups.put(new_ordered_items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
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("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
|
||||
// }
|
||||
|
||||
add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "host_offline");
|
||||
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "host_offline");
|
||||
}
|
||||
setTimeout(add_testing_displays, 0);
|
||||
async function add_testing_displays() {
|
||||
await add_display('127.0.0.1', '00:1A:2B:3C:4D:5E', 'PC', 'host_offline');
|
||||
// await add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "host_offline");
|
||||
}
|
||||
|
||||
Regular → Executable
+327
-271
@@ -1,317 +1,373 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import type { Display, FolderElement, TreeElement } from "../types";
|
||||
import { displays, get_display_by_id } from "./displays";
|
||||
import { select, selected_display_ids, selected_file_ids } from "./select";
|
||||
import { get_file_data, get_file_tree_data } from "../api_handler";
|
||||
import { notifications } from "./notification";
|
||||
import { CirclePoundSterling } from "lucide-svelte";
|
||||
import { deactivate_old_thumbnail_urls, generate_thumbnail } from "./thumbnails";
|
||||
|
||||
export const all_files: Writable<Record<string, Record<string, FolderElement[]>>> = writable<Record<string, Record<string, FolderElement[]>>>({});
|
||||
// {
|
||||
// path: {
|
||||
// display_id: FolderElement[]
|
||||
// ...
|
||||
// },
|
||||
// path2: {
|
||||
// display_id: FolderElement[]
|
||||
// ...
|
||||
// },
|
||||
// ...
|
||||
// }
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { get_file_primary_key, type Display, type Inode, type TreeElement } from '../types';
|
||||
import { get_display_by_id } from './displays';
|
||||
import { select, selected_display_ids, selected_file_ids } from './select';
|
||||
import { create_folders, get_file_data, get_file_tree_data } from '../api_handler';
|
||||
import { deactivate_old_thumbnail_urls, generate_thumbnail } from './thumbnails';
|
||||
import { db, type FileOnDisplay } from '../files_display.db';
|
||||
|
||||
export const current_file_path: Writable<string> = writable<string>('/');
|
||||
|
||||
|
||||
export async function change_file_path(new_path: string) {
|
||||
current_file_path.update(() => {
|
||||
return new_path;
|
||||
});
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
})
|
||||
current_file_path.update(() => {
|
||||
return new_path;
|
||||
});
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
deactivate_old_thumbnail_urls();
|
||||
deactivate_old_thumbnail_urls();
|
||||
|
||||
for (const display_group of get(displays)) {
|
||||
for (const display of display_group.data) {
|
||||
const changed_paths = await get_changed_directory_paths(display, new_path);
|
||||
if (!changed_paths) continue;
|
||||
console.log("Update file system from", display.name, ":", changed_paths);
|
||||
for (const path of changed_paths) {
|
||||
update_folder_elements_recursively(display, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
const displays = await db.displays.toArray();
|
||||
|
||||
for (const display of displays) {
|
||||
const changed_paths = await get_changed_directory_paths(display, new_path);
|
||||
if (!changed_paths) continue;
|
||||
console.log('Update file system from', display.name, ':', changed_paths);
|
||||
for (const path of changed_paths) {
|
||||
await update_folder_elements_recursively(display, path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function filter_file_selection_for_current_selected_displays() {
|
||||
for (const selected_file_id of get(selected_file_ids)) {
|
||||
if (!get_file_by_id(selected_file_id, get(all_files), get(current_file_path), true)) {
|
||||
// file not found in selected displays
|
||||
select(selected_file_ids, selected_file_id, false);
|
||||
}
|
||||
}
|
||||
export async function delete_and_deselect_unique_files_from_display(display_id: string) {
|
||||
const files_on_display = await db.files_on_display
|
||||
.where('display_id')
|
||||
.equals(display_id)
|
||||
.toArray();
|
||||
for (const file of files_on_display) {
|
||||
await remove_file_from_display(display_id, file.file_primary_key);
|
||||
}
|
||||
await remove_all_files_without_display();
|
||||
}
|
||||
|
||||
async function remove_file_from_display(display_id: string, file_primary_key: string) {
|
||||
const found = await db.files_on_display.get([display_id, file_primary_key]);
|
||||
if (!found) return;
|
||||
select(selected_file_ids, file_primary_key, 'deselect');
|
||||
await db.files_on_display.delete([display_id, file_primary_key]);
|
||||
}
|
||||
|
||||
async function remove_all_files_without_display() {
|
||||
const existing_file_id_strings: string[] = (await db.files_on_display
|
||||
.orderBy('file_primary_key')
|
||||
.uniqueKeys()) as string[];
|
||||
const existing_file_id_objects: [string, string, number, string][] = existing_file_id_strings.map(
|
||||
(e) => JSON.parse(e) as [string, string, number, string]
|
||||
);
|
||||
await db.files.where('[path+name+size+type]').noneOf(existing_file_id_objects).delete();
|
||||
}
|
||||
|
||||
export async function update_current_folder_on_selected_displays() {
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
});
|
||||
const current_path = get(current_file_path);
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
const display = get_display_by_id(display_id, get(displays));
|
||||
if (!display) continue;
|
||||
update_folder_elements_recursively(display, current_path);
|
||||
}
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
});
|
||||
const current_path = get(current_file_path);
|
||||
|
||||
for (const display of await db.displays.where('id').anyOf(get(selected_display_ids)).toArray()) {
|
||||
await update_folder_elements_recursively(display, current_path);
|
||||
}
|
||||
}
|
||||
|
||||
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 [selected_display_ids, []];
|
||||
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 async function get_missing_colliding_display_ids(
|
||||
file: Inode,
|
||||
selected_display_ids: string[]
|
||||
): Promise<{ missing: string[]; colliding: string[] }> {
|
||||
const missing: string[] = await get_display_ids_where_file_is_missing(file, selected_display_ids);
|
||||
|
||||
const colliding: string[] = [];
|
||||
const colliding_files = await db.files
|
||||
.where('[path+name]')
|
||||
.equals([file.path, file.name])
|
||||
.filter((e) => e.size !== file.size || e.type !== file.type)
|
||||
.toArray();
|
||||
for (const colliding_file of colliding_files) {
|
||||
colliding.push(
|
||||
...(await get_display_ids_where_file_is_missing(colliding_file, selected_display_ids))
|
||||
);
|
||||
}
|
||||
|
||||
return { missing, colliding };
|
||||
}
|
||||
|
||||
export function get_display_ids_where_path_does_not_exist(path: string, selected_display_ids: string[], all_files: Record<string, Record<string, FolderElement[]>>): string[] {
|
||||
if (!all_files.hasOwnProperty(path)) return selected_display_ids;
|
||||
const out: string[] = [];
|
||||
for (const selected_display_id of selected_display_ids) {
|
||||
if (!all_files[path].hasOwnProperty(selected_display_id)) {
|
||||
out.push(selected_display_id);
|
||||
}
|
||||
}
|
||||
return out;
|
||||
async function get_display_ids_where_file_is_missing(
|
||||
file: Inode,
|
||||
selected_display_ids: string[]
|
||||
): Promise<string[]> {
|
||||
const file_primary_key = get_file_primary_key(file);
|
||||
const files_on_selected_displays = await db.files_on_display
|
||||
.where('file_primary_key')
|
||||
.equals(file_primary_key)
|
||||
.filter((e) => selected_display_ids.includes(e.display_id))
|
||||
.toArray();
|
||||
return selected_display_ids.filter(
|
||||
(id) => !files_on_selected_displays.some((item) => item.display_id === id)
|
||||
);
|
||||
}
|
||||
|
||||
async function get_changed_directory_paths(display: Display, file_path: string): Promise<string[] | null> {
|
||||
const current_folder = await get_file_tree_data(display.ip, file_path);
|
||||
const directory_strings = get_recursive_changed_directory_paths(display, file_path, current_folder, get(all_files));
|
||||
if (directory_strings.size === 0) return null;
|
||||
const directory_strings_array = [...directory_strings];
|
||||
return directory_strings_array.filter((e) => (!directory_strings_array.some((f) => (f !== e && f.startsWith(e)))));
|
||||
export async function get_displays_where_path_exists(
|
||||
path: string,
|
||||
selected_display_ids: string[],
|
||||
invert: boolean
|
||||
): Promise<Display[]> {
|
||||
if (path === '/') return [];
|
||||
const last_path_part =
|
||||
path
|
||||
.slice(0, path.length - 1)
|
||||
.split('/')
|
||||
.at(-1) ?? '';
|
||||
const path_without_last_part = path.slice(0, path.length - (last_path_part.length + 1));
|
||||
|
||||
const folders_of_current_path = await db.files
|
||||
.where('[path+name+type]')
|
||||
.equals([path_without_last_part, last_path_part, 'inode/directory'])
|
||||
.first();
|
||||
if (!folders_of_current_path)
|
||||
return await db.displays.where('id').anyOf(selected_display_ids).toArray();
|
||||
const folder_primary_key = get_file_primary_key(folders_of_current_path);
|
||||
|
||||
const display_ids = selected_display_ids.filter(async (display_id) => {
|
||||
const folder_exists = await db.files_on_display.get([display_id, folder_primary_key]);
|
||||
if (invert) {
|
||||
return !folder_exists;
|
||||
} else {
|
||||
return folder_exists;
|
||||
}
|
||||
});
|
||||
|
||||
return (await db.displays.bulkGet(display_ids)).filter((e) => e !== undefined);
|
||||
}
|
||||
|
||||
function get_recursive_changed_directory_paths(display: Display, current_file_path: string, current_folder_elements: TreeElement[] | null, files: Record<string, Record<string, FolderElement[]>>): Set<string> {
|
||||
const files_folder: FolderElement[] = files[current_file_path][display.id];
|
||||
if ((!files_folder || files_folder.length === 0) && (!current_folder_elements || current_folder_elements.length === 0)) {
|
||||
return new Set([]); // no data -> no update needed
|
||||
} else if (!files_folder || !current_folder_elements || current_folder_elements.length !== files_folder.length) {
|
||||
return new Set([current_file_path]); // existing data does not match new data -> update
|
||||
}
|
||||
|
||||
let has_changed: Set<string> = new Set();
|
||||
for (const tree_folder_element of current_folder_elements) {
|
||||
const folder_element = files_folder.find(e => e.name === tree_folder_element.name);
|
||||
if (!folder_element || (tree_folder_element.type !== "directory" && folder_element.size !== tree_folder_element.size)) {
|
||||
return new Set([current_file_path]);
|
||||
}
|
||||
|
||||
if (tree_folder_element.type === "directory" && tree_folder_element.contents) {
|
||||
const new_file_path = current_file_path + tree_folder_element.name + '/';
|
||||
for (const string of get_recursive_changed_directory_paths(display, new_file_path, tree_folder_element.contents, files)) {
|
||||
has_changed.add(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return has_changed;
|
||||
async function get_changed_directory_paths(
|
||||
display: Display,
|
||||
file_path: string
|
||||
): Promise<string[] | null> {
|
||||
const current_folder = await get_file_tree_data(display.ip, file_path);
|
||||
const directory_strings = await get_recursive_changed_directory_paths(
|
||||
display,
|
||||
file_path,
|
||||
current_folder
|
||||
);
|
||||
if (directory_strings.size === 0) return null;
|
||||
const directory_strings_array = [...directory_strings];
|
||||
return directory_strings_array.filter(
|
||||
(e) => !directory_strings_array.some((f) => f !== e && f.startsWith(e))
|
||||
);
|
||||
}
|
||||
|
||||
export async function update_folder_elements_recursively(display: Display, file_path: string = '/'): Promise<number> {
|
||||
const new_folder_elements = await get_file_data(display.ip, file_path);
|
||||
if (new_folder_elements === null) return 0;
|
||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
||||
if (!files.hasOwnProperty(file_path)) {
|
||||
files[file_path] = {};
|
||||
}
|
||||
if (!files[file_path].hasOwnProperty(display.id)) {
|
||||
files[file_path][display.id] = [];
|
||||
}
|
||||
async function get_recursive_changed_directory_paths(
|
||||
display: Display,
|
||||
current_file_path: string,
|
||||
current_folder_elements: TreeElement[] | null
|
||||
): Promise<Set<string>> {
|
||||
const files_folder: Inode[] = await db.files.where('path').equals(current_file_path).toArray();
|
||||
if (
|
||||
(!files_folder || files_folder.length === 0) &&
|
||||
(!current_folder_elements || current_folder_elements.length === 0)
|
||||
) {
|
||||
return new Set([]); // no data -> no update needed
|
||||
} else if (
|
||||
!files_folder ||
|
||||
!current_folder_elements ||
|
||||
current_folder_elements.length !== files_folder.length
|
||||
) {
|
||||
return new Set([current_file_path]); // existing data does not match new data -> update
|
||||
}
|
||||
|
||||
const existing_folder_elements = files[file_path].hasOwnProperty(display.id) ? files[file_path][display.id] : [];
|
||||
const has_changed: Set<string> = new Set();
|
||||
for (const tree_folder_element of current_folder_elements) {
|
||||
const folder_element = files_folder.find((e) => e.name === tree_folder_element.name);
|
||||
if (
|
||||
!folder_element ||
|
||||
(tree_folder_element.type !== 'directory' && folder_element.size !== tree_folder_element.size)
|
||||
) {
|
||||
return new Set([current_file_path]);
|
||||
}
|
||||
|
||||
const diff = get_folder_elements_difference(existing_folder_elements, new_folder_elements);
|
||||
// Generate Thumbnails:
|
||||
setTimeout(async () => {
|
||||
for (const folder_element of diff.new) {
|
||||
await generate_thumbnail(display.ip, file_path, folder_element);
|
||||
}
|
||||
}, 0)
|
||||
|
||||
files[file_path][display.id].push(...diff.new);
|
||||
return remove_folder_elements_recursively(files, display, diff.deleted, file_path);
|
||||
})
|
||||
|
||||
let folder_size = 0;
|
||||
for (const element of new_folder_elements) {
|
||||
if (element.type === 'inode/directory') {
|
||||
const folder_content_size = await update_folder_elements_recursively(display, file_path + element.name + '/');
|
||||
folder_size += folder_content_size;
|
||||
// Update foldersize
|
||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
||||
for (const current_folder_element of files[file_path][display.id]) {
|
||||
if (current_folder_element.id === element.id) {
|
||||
current_folder_element.size = folder_content_size;
|
||||
}
|
||||
}
|
||||
return files;
|
||||
})
|
||||
} else {
|
||||
folder_size += element.size;
|
||||
}
|
||||
}
|
||||
return folder_size;
|
||||
if (tree_folder_element.type === 'directory' && tree_folder_element.contents) {
|
||||
const new_file_path = current_file_path + tree_folder_element.name + '/';
|
||||
for (const string of await get_recursive_changed_directory_paths(
|
||||
display,
|
||||
new_file_path,
|
||||
tree_folder_element.contents
|
||||
)) {
|
||||
has_changed.add(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return has_changed;
|
||||
}
|
||||
|
||||
function remove_folder_elements_recursively(files: Record<string, Record<string, FolderElement[]>>, display: Display, folder_elements: FolderElement[], file_path: string): Record<string, Record<string, FolderElement[]>> {
|
||||
if (!files.hasOwnProperty(file_path) || !files[file_path].hasOwnProperty(display.id)) {
|
||||
console.error("File remove path does not exist:", files, display, folder_elements, file_path);
|
||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist: ${file_path} display_ip: ${display.ip}`);
|
||||
return {};
|
||||
}
|
||||
for (const folder_element of folder_elements) {
|
||||
files[file_path][display.id] = files[file_path][display.id].filter((f) => f.id !== folder_element.id);
|
||||
export async function update_folder_elements_recursively(
|
||||
display: Display,
|
||||
file_path: string = '/'
|
||||
): Promise<void> {
|
||||
const new_folder_elements = await get_file_data(display.ip, file_path);
|
||||
if (!new_folder_elements) return;
|
||||
|
||||
if (folder_element.type === 'inode/directory') {
|
||||
const new_file_path = file_path + folder_element.name + '/';
|
||||
if (!files.hasOwnProperty(new_file_path) || !files[new_file_path].hasOwnProperty(display.id)) {
|
||||
console.error("File remove path does not exist (recursion):", files, display, folder_elements, file_path, new_file_path);
|
||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist (recursion): ${new_file_path} display_ip: ${display.ip}`);
|
||||
return {};
|
||||
}
|
||||
const sub_folder = files[new_file_path][display.id];
|
||||
remove_folder_elements_recursively(files, display, sub_folder, new_file_path);
|
||||
}
|
||||
}
|
||||
const existing_file_keys_on_display_in_path: [string, string, number, string][] = (
|
||||
await db.files_on_display.where('display_id').equals(display.id).toArray()
|
||||
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||
const existing_files_on_display_in_path: Inode[] = await db.files
|
||||
.where('[path+name+size+type]')
|
||||
.anyOf(existing_file_keys_on_display_in_path)
|
||||
.filter((e) => e.path === file_path)
|
||||
.toArray();
|
||||
|
||||
return files;
|
||||
const diff = get_folder_elements_difference(
|
||||
existing_files_on_display_in_path,
|
||||
new_folder_elements
|
||||
);
|
||||
|
||||
if (diff.new.length > 0) {
|
||||
// Add new Folder-Elements
|
||||
for (const new_element of diff.new) {
|
||||
await db.files.put(new_element.folder_element);
|
||||
const file_on_display: FileOnDisplay = {
|
||||
display_id: display.id,
|
||||
file_primary_key: get_file_primary_key(new_element.folder_element),
|
||||
is_loading: false,
|
||||
percentage: 0,
|
||||
date_created: new_element.date_created
|
||||
};
|
||||
await db.files_on_display.put(file_on_display);
|
||||
|
||||
if (new_element.folder_element.type === 'inode/directory') {
|
||||
await update_folder_elements_recursively(
|
||||
display,
|
||||
file_path + new_element.folder_element.name + '/'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Generate Thumbnails:
|
||||
setTimeout(async () => {
|
||||
for (const new_element of diff.new) {
|
||||
await generate_thumbnail(display.ip, file_path, new_element.folder_element);
|
||||
}
|
||||
}, 0);
|
||||
}
|
||||
if (diff.deleted.length > 0) {
|
||||
// Remove old Folder-Elements
|
||||
for (const old_element of diff.deleted) {
|
||||
remove_file_from_display(display.id, get_file_primary_key(old_element));
|
||||
}
|
||||
await remove_all_files_without_display();
|
||||
}
|
||||
}
|
||||
|
||||
function get_folder_elements_difference(old_elements: FolderElement[], new_elements: FolderElement[]): { deleted: FolderElement[], new: FolderElement[] } {
|
||||
const old_hashes = new Set(old_elements.map(e => e.hash));
|
||||
const new_hashes = new Set(new_elements.map(e => e.hash));
|
||||
function get_folder_elements_difference(
|
||||
old_elements: Inode[],
|
||||
new_elements: { folder_element: Inode; date_created: Date }[]
|
||||
): { deleted: Inode[]; new: { folder_element: Inode; date_created: Date }[] } {
|
||||
const old_keys = new Set(old_elements.map((e) => get_file_primary_key(e)));
|
||||
const new_keys = new Set(new_elements.map((e) => get_file_primary_key(e.folder_element)));
|
||||
|
||||
const only_in_old = old_elements.filter(e => !new_hashes.has(e.hash));
|
||||
const only_in_new = new_elements.filter(e => !old_hashes.has(e.hash));
|
||||
return { deleted: only_in_old, new: only_in_new };
|
||||
const only_in_old = old_elements.filter((e) => !new_keys.has(get_file_primary_key(e)));
|
||||
const only_in_new = new_elements.filter(
|
||||
(e) => !old_keys.has(get_file_primary_key(e.folder_element))
|
||||
);
|
||||
return { deleted: only_in_old, new: only_in_new };
|
||||
}
|
||||
|
||||
export async function get_current_folder_elements(
|
||||
current_file_path: string,
|
||||
selected_display_ids: string[]
|
||||
): Promise<Inode[]> {
|
||||
const existing_file_keys_on_selected_displays: [string, string, number, string][] = (
|
||||
await db.files_on_display.where('display_id').anyOf(selected_display_ids).toArray()
|
||||
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||
const existing_files_on_selected_displays_in_path: Inode[] = await db.files
|
||||
.where('[path+name+size+type]')
|
||||
.anyOf(existing_file_keys_on_selected_displays)
|
||||
.filter((e) => e.path === current_file_path)
|
||||
.toArray();
|
||||
|
||||
|
||||
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)) 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) {
|
||||
const both_same_folder = file_on_display.type === "inode/directory" && existing_file.type === "inode/directory" && file_on_display.name === existing_file.name;
|
||||
if (both_same_folder && file_on_display.size !== existing_file.size) {
|
||||
existing_file.size = -1;
|
||||
}
|
||||
if (file_on_display.hash === existing_file.hash) {
|
||||
continue FileOnDisplay;
|
||||
} else if (both_same_folder) {
|
||||
existing_file.date_created = null;
|
||||
continue FileOnDisplay;
|
||||
}
|
||||
}
|
||||
files.push({ ...file_on_display });
|
||||
}
|
||||
}
|
||||
}
|
||||
return sort_files(files);
|
||||
return sort_files(existing_files_on_selected_displays_in_path);
|
||||
}
|
||||
|
||||
function sort_files(files: FolderElement[]) {
|
||||
files.sort((a, b) => {
|
||||
const isDirA = a.type === 'inode/directory';
|
||||
const isDirB = b.type === 'inode/directory';
|
||||
function sort_files(files: Inode[]) {
|
||||
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;
|
||||
// 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;
|
||||
// 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
|
||||
if (!b.date_created || !a.date_created) return -1;
|
||||
return b.date_created.getTime() - a.date_created.getTime();
|
||||
});
|
||||
return files;
|
||||
return -1;
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
export function get_file_by_id(file_id: string, all_files: Record<string, Record<string, FolderElement[]>>, current_file_path: string, only_from_selected_displays: boolean = false): FolderElement | null {
|
||||
let current_path_elements: Record<string, FolderElement[]> | undefined = all_files[current_file_path];
|
||||
if (!current_path_elements) return null;
|
||||
if (only_from_selected_displays) {
|
||||
current_path_elements = Object.fromEntries(
|
||||
Object.entries(current_path_elements).filter(([key]) => get(selected_file_ids).includes(key))
|
||||
);
|
||||
}
|
||||
const all_folder_elements = Object.values(current_path_elements).flat();
|
||||
const found = all_folder_elements.find(el => el.id === file_id);
|
||||
if (!found) return null;
|
||||
return found
|
||||
export async function get_file_by_id(
|
||||
file_primary_key: string,
|
||||
only_from_selected_displays: boolean = false
|
||||
): Promise<Inode | null> {
|
||||
const file = (await db.files.get(JSON.parse(file_primary_key))) ?? null;
|
||||
if (!file || !only_from_selected_displays) {
|
||||
return file;
|
||||
} else {
|
||||
const exist_on_selected_display = !!(await db.files_on_display
|
||||
.where('file_primary_key')
|
||||
.equals(file_primary_key)
|
||||
.filter((e) => get(selected_display_ids).includes(e.display_id))
|
||||
.first());
|
||||
return exist_on_selected_display ? file : null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function run_for_selected_files_on_selected_displays(action: (ip: string, file_names: string[]) => Promise<void>): Promise<void> {
|
||||
const files = get(all_files);
|
||||
const file_path = get(current_file_path);
|
||||
const folder_elements: FolderElement[] = get(selected_file_ids)
|
||||
.map((file_id) => get_file_by_id(file_id, files, file_path))
|
||||
.filter((element) => element !== null);
|
||||
const folder_element_hashs = folder_elements
|
||||
.map((folder_element) => folder_element.hash)
|
||||
.filter((hash) => hash !== null);
|
||||
export async function run_for_selected_files_on_selected_displays(
|
||||
action: (ip: string, file_names: string[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
const file_keys_on_display: [string, string, number, string][] = (
|
||||
await db.files_on_display.where('display_id').equals(display_id).toArray()
|
||||
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||
const file_names_on_display: string[] = (
|
||||
await db.files.where('[path+name+size+type]').anyOf(file_keys_on_display).toArray()
|
||||
).map((e) => e.name);
|
||||
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
if (!files[file_path].hasOwnProperty(display_id)) continue;
|
||||
const files_on_display = files[file_path][display_id];
|
||||
|
||||
const selected_file_names_on_display: string[] = files_on_display
|
||||
.filter((e) => (e.hash && folder_element_hashs.includes(e.hash)) || (e.type === 'inode/directory' && folder_elements.some(e2 => e2.name === e.name && e2.type === 'inode/directory')))
|
||||
.map((folder_element) => folder_element.name);
|
||||
if (selected_file_names_on_display.length === 0) continue;
|
||||
|
||||
const display = get_display_by_id(display_id, get(displays));
|
||||
if (!display) continue;
|
||||
|
||||
await action(display.ip, selected_file_names_on_display);
|
||||
}
|
||||
const display = await get_display_by_id(display_id);
|
||||
if (!display) continue;
|
||||
|
||||
await action(display.ip, file_names_on_display);
|
||||
}
|
||||
}
|
||||
|
||||
export function get_longest_existing_path_and_needed_parts(path: string, display_id: string, all_files: Record<string, Record<string, FolderElement[]>>): { existing: string; needed: string[] } {
|
||||
const path_parts = path.slice(1, path.length - 1).split('/').filter(e => e.length !== 0); // remove front and back / and split after that, then remove empty strings
|
||||
for (let i = path_parts.length; i > 0; i--) {
|
||||
const current_path = '/' + [...path_parts].splice(0, i).join('/') + '/';;
|
||||
if (all_files.hasOwnProperty(current_path)) {
|
||||
if (all_files[current_path].hasOwnProperty(display_id)) {
|
||||
return { existing: current_path, needed: [...path_parts].splice(i) };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { existing: '/', needed: path_parts };
|
||||
}
|
||||
export async function create_folder_on_all_selected_displays(
|
||||
folder_name: string,
|
||||
path: string,
|
||||
selected_display_ids: string[]
|
||||
): Promise<void> {
|
||||
const path_parts = path
|
||||
.slice(1, path.length - 1)
|
||||
.split('/')
|
||||
.filter((e) => e.length !== 0);
|
||||
let remaining_display_ids = [...selected_display_ids];
|
||||
|
||||
const getDisplaysForPath = async (currentPath: string): Promise<Display[]> => {
|
||||
if (currentPath === '/') {
|
||||
const displays = await db.displays.bulkGet(remaining_display_ids);
|
||||
return displays.filter((d): d is Display => !!d);
|
||||
}
|
||||
return get_displays_where_path_exists(currentPath, remaining_display_ids, false);
|
||||
};
|
||||
|
||||
for (let depth = path_parts.length; depth >= 0 && remaining_display_ids.length; depth--) {
|
||||
const currentPath = depth === 0 ? '/' : `/${path_parts.slice(0, depth).join('/')}/`;
|
||||
|
||||
const displays = await getDisplaysForPath(currentPath);
|
||||
if (!displays.length) continue;
|
||||
|
||||
const folders_to_create = [...path_parts.slice(depth), folder_name];
|
||||
for (const display of displays) {
|
||||
await create_folders(display.ip, currentPath, folders_to_create);
|
||||
remaining_display_ids = remaining_display_ids.filter((id) => id !== display.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { get, writable } from "svelte/store";
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type Notification = {
|
||||
id: number;
|
||||
|
||||
Regular → Executable
+19
-16
@@ -1,23 +1,26 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const selected_file_ids: Writable<string[]> = writable<string[]>([]);
|
||||
export const selected_file_ids: Writable<string[]> = writable<string[]>([]); // JSON.stringify([string, string, number, 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 select(
|
||||
selected_ids: Writable<string[]>,
|
||||
id: string,
|
||||
action: 'toggle' | 'select' | 'deselect'
|
||||
) {
|
||||
selected_ids.update((all_ids: string[]) => {
|
||||
if (all_ids.includes(id)) {
|
||||
const index = all_ids.indexOf(id);
|
||||
if (index > -1 && action !== 'select') {
|
||||
all_ids.splice(index, 1);
|
||||
}
|
||||
} else if (action !== 'deselect') {
|
||||
all_ids.push(id);
|
||||
}
|
||||
return all_ids;
|
||||
});
|
||||
}
|
||||
|
||||
export function is_selected(id: string, selected_ids: string[]): boolean {
|
||||
return selected_ids.includes(id);
|
||||
return selected_ids.includes(id);
|
||||
}
|
||||
|
||||
Regular → Executable
+34
-29
@@ -1,38 +1,43 @@
|
||||
import { get, writable, type Writable } from "svelte/store";
|
||||
import { get_thumbnail_blob } from "../api_handler";
|
||||
import { type FolderElement } from "../types";
|
||||
import { db, type ThumbnailBlobDBEntry } from "../indexdb/file_thumbnails.db";
|
||||
import { get_file_type } from "../utils";
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { get_thumbnail_blob } from '../api_handler';
|
||||
import { type Inode } from '../types';
|
||||
import { db } from '../files_display.db';
|
||||
import { get_file_type } from '../utils';
|
||||
|
||||
export const active_thumbnail_urls: Writable<string[]> = writable<string[]>([]);
|
||||
|
||||
export async function generate_thumbnail(display_ip: string, path: string, folder_element: FolderElement): Promise<void> {
|
||||
const supported_file_type = get_file_type(folder_element);
|
||||
if (!supported_file_type || !folder_element.hash) return;
|
||||
const hash: string = folder_element.hash;
|
||||
if (await db.thumbnail_blobs.get(hash)) return;
|
||||
export async function generate_thumbnail(
|
||||
display_ip: string,
|
||||
path: string,
|
||||
folder_element: Inode
|
||||
): Promise<void> {
|
||||
const supported_file_type = get_file_type(folder_element);
|
||||
if (!supported_file_type) return;
|
||||
|
||||
const thumbnail_blob = await get_thumbnail_blob(display_ip, path + folder_element.name);
|
||||
if (!thumbnail_blob) return;
|
||||
await db.thumbnail_blobs.add({ hash: hash, blob: thumbnail_blob });
|
||||
const thumbnail_blob = await get_thumbnail_blob(display_ip, path + folder_element.name);
|
||||
if (!thumbnail_blob) return;
|
||||
|
||||
folder_element.thumbnail = thumbnail_blob;
|
||||
|
||||
await db.files.put(folder_element); // save
|
||||
}
|
||||
|
||||
export async function get_thumbnail_url(hash: string | null): Promise<string | null> {
|
||||
if (hash === null) return null;
|
||||
const thumbnail_blob = await db.thumbnail_blobs.get(hash) as ThumbnailBlobDBEntry;
|
||||
if (!thumbnail_blob) return null;
|
||||
const new_url = URL.createObjectURL(thumbnail_blob.blob);
|
||||
active_thumbnail_urls.update((current: string[]) => {
|
||||
current.push(new_url);
|
||||
return current;
|
||||
})
|
||||
return new_url;
|
||||
export async function get_thumbnail_url(file: Inode): Promise<string | null> {
|
||||
if (!file.thumbnail) return null;
|
||||
const new_url = URL.createObjectURL(file.thumbnail);
|
||||
active_thumbnail_urls.update((current: string[]) => {
|
||||
current.push(new_url);
|
||||
return current;
|
||||
});
|
||||
return new_url;
|
||||
}
|
||||
|
||||
export function deactivate_old_thumbnail_urls() {
|
||||
const current_urls = get(active_thumbnail_urls);
|
||||
for (const url of current_urls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
active_thumbnail_urls.update(() => { return []; })
|
||||
}
|
||||
const current_urls = get(active_thumbnail_urls);
|
||||
for (const url of current_urls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
active_thumbnail_urls.update(() => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
|
||||
Regular → Executable
+76
-68
@@ -1,89 +1,97 @@
|
||||
import { FileBox, FileImage, FileText, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
|
||||
import type { Snippet } from "svelte";
|
||||
import { FileBox, FileImage, FileText, FileVideoCamera, ImagePlay, type X } from 'lucide-svelte';
|
||||
import type { Snippet } from 'svelte';
|
||||
|
||||
export type RequestResponse = {
|
||||
ok: boolean,
|
||||
http_code?: number
|
||||
blob?: Blob,
|
||||
json?: any,
|
||||
}
|
||||
ok: boolean;
|
||||
http_code?: number;
|
||||
blob?: Blob;
|
||||
json?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ShellCommandResponse = {
|
||||
stdout: string,
|
||||
stderr: string,
|
||||
exitCode: number,
|
||||
}
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
};
|
||||
|
||||
export type SupportedFileType = {
|
||||
display_name: string;
|
||||
mime_type: string;
|
||||
display_name: string;
|
||||
mime_type: string;
|
||||
};
|
||||
export const supported_file_type_icon: Record<string, typeof X> = {
|
||||
'MP4': FileVideoCamera,
|
||||
'JPG': FileImage,
|
||||
'PNG': FileImage,
|
||||
'GIF': ImagePlay,
|
||||
'PPTX': FileBox,
|
||||
'ODP': FileBox,
|
||||
'PDF': FileText
|
||||
}
|
||||
MP4: FileVideoCamera,
|
||||
JPG: FileImage,
|
||||
PNG: FileImage,
|
||||
GIF: ImagePlay,
|
||||
PPTX: FileBox,
|
||||
ODP: FileBox,
|
||||
PDF: FileText
|
||||
};
|
||||
|
||||
export type FolderElement = {
|
||||
id: string;
|
||||
hash: string;
|
||||
name: string;
|
||||
type: string;
|
||||
date_created: Date | null;
|
||||
size: number;
|
||||
export type Inode = {
|
||||
path: string;
|
||||
name: string;
|
||||
size: number;
|
||||
type: string;
|
||||
date_created: Date;
|
||||
thumbnail: Blob | null;
|
||||
};
|
||||
|
||||
export function get_file_primary_key(file: Inode): string {
|
||||
return JSON.stringify([file.path, file.name, file.size, file.type]);
|
||||
}
|
||||
|
||||
export type TreeElement = {
|
||||
contents?: TreeElement[];
|
||||
type: "file" | "directory";
|
||||
name: string;
|
||||
size: number;
|
||||
}
|
||||
|
||||
|
||||
export type Display = {
|
||||
id: string;
|
||||
ip: string;
|
||||
preview_url: string | null;
|
||||
preview_timeout_id: number | null;
|
||||
mac: string | null;
|
||||
name: string;
|
||||
status: DisplayStatus;
|
||||
}
|
||||
|
||||
export type DisplayGroup = {
|
||||
id: string;
|
||||
data: Display[];
|
||||
contents?: TreeElement[];
|
||||
type: 'file' | 'directory';
|
||||
name: string;
|
||||
size: number;
|
||||
};
|
||||
|
||||
export type Display = {
|
||||
id: string;
|
||||
ip: string;
|
||||
mac: string | null;
|
||||
position: number;
|
||||
preview: PreviewObject;
|
||||
group_id: string;
|
||||
name: string;
|
||||
status: DisplayStatus;
|
||||
};
|
||||
|
||||
export type DisplayGroup = {
|
||||
id: string;
|
||||
position: number;
|
||||
};
|
||||
|
||||
export type PreviewObject = {
|
||||
currently_updating: boolean;
|
||||
url: string | null;
|
||||
};
|
||||
|
||||
export type MenuOption = {
|
||||
icon?: typeof X;
|
||||
name: string;
|
||||
class?: string;
|
||||
on_select?: () => void;
|
||||
disabled?: boolean;
|
||||
}
|
||||
icon?: typeof X;
|
||||
name: string;
|
||||
class?: string;
|
||||
on_select?: () => void | Promise<void>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type PopupContent = {
|
||||
open: boolean;
|
||||
snippet: Snippet<[string]> | null;
|
||||
snippet_arg?: string;
|
||||
title?: string;
|
||||
title_class?: string;
|
||||
title_icon?: typeof X | null;
|
||||
window_class?: string;
|
||||
closable?: boolean;
|
||||
}
|
||||
open: boolean;
|
||||
snippet: Snippet<[string]> | null;
|
||||
snippet_arg?: string;
|
||||
title?: string;
|
||||
title_class?: string;
|
||||
title_icon?: typeof X | null;
|
||||
window_class?: string;
|
||||
closable?: boolean;
|
||||
};
|
||||
|
||||
export type DisplayStatus = "host_offline" | "app_offline" | "app_online" | null;
|
||||
export type DisplayStatus = 'host_offline' | 'app_offline' | 'app_online' | null;
|
||||
|
||||
export function to_display_status(value: string): DisplayStatus {
|
||||
return ["host_offline", "app_offline", "app_online"].includes(value)
|
||||
? (value as DisplayStatus)
|
||||
: null;
|
||||
}
|
||||
return ['host_offline', 'app_offline', 'app_online'].includes(value)
|
||||
? (value as DisplayStatus)
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -1,82 +1,83 @@
|
||||
import type { DisplayStatus, FolderElement, SupportedFileType } from "./types";
|
||||
import type { DisplayStatus, Inode, SupportedFileType } from './types';
|
||||
import supported_file_types_json from './../../../../shared/supported_file_types.json';
|
||||
|
||||
const supported_file_types: Record<string, SupportedFileType> = supported_file_types_json as Record<string, SupportedFileType>;
|
||||
const supported_file_types: Record<string, SupportedFileType> = supported_file_types_json as Record<
|
||||
string,
|
||||
SupportedFileType
|
||||
>;
|
||||
|
||||
export 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;
|
||||
export function get_file_type(file: Inode): 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;
|
||||
}
|
||||
|
||||
export function get_uuid(): string {
|
||||
return crypto.randomUUID();
|
||||
return crypto.randomUUID();
|
||||
}
|
||||
|
||||
export function get_file_size_display_string(size: number, toFixed: number | null = null): string {
|
||||
if (size < 0) return toFixed === null ? "versch." : "Verschiedene Größen auf verschiedenen Bildschirmen";
|
||||
if (size === 0) return "0 B";
|
||||
if (size < 0)
|
||||
return toFixed === null ? 'versch.' : 'Verschiedene Größen auf verschiedenen Bildschirmen';
|
||||
if (size === 0) return '0 B';
|
||||
|
||||
const k = 1024;
|
||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
|
||||
const i = Math.floor(Math.log(size) / Math.log(k));
|
||||
const value = size / Math.pow(k, i);
|
||||
const i = Math.floor(Math.log(size) / Math.log(k));
|
||||
const value = size / Math.pow(k, i);
|
||||
|
||||
const size_string = `${value.toFixed(toFixed !== null ? toFixed : Math.max(0, 2 - Math.floor(Math.log10(value))))} ${sizes[i]}`;
|
||||
const size_string = `${value.toFixed(toFixed !== null ? toFixed : Math.max(0, 2 - Math.floor(Math.log10(value))))} ${sizes[i]}`;
|
||||
|
||||
return size_string.replace('.', ',');
|
||||
return size_string.replace('.', ',');
|
||||
}
|
||||
|
||||
|
||||
export async function image_content_hash(blob: Blob, size = 32): Promise<number> {
|
||||
// Blob → ImageBitmap (GPU-dekodiert, superschnell)
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
// Blob → ImageBitmap (GPU-dekodiert, superschnell)
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
|
||||
// OffscreenCanvas ist sehr schnell (kein Layout nötig)
|
||||
const canvas = new OffscreenCanvas(size, size);
|
||||
const ctx = canvas.getContext("2d")!;
|
||||
ctx.drawImage(bitmap, 0, 0, size, size);
|
||||
// OffscreenCanvas ist sehr schnell (kein Layout nötig)
|
||||
const canvas = new OffscreenCanvas(size, size);
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
ctx.drawImage(bitmap, 0, 0, size, size);
|
||||
|
||||
// Pixel-Daten holen
|
||||
const { data } = ctx.getImageData(0, 0, size, size);
|
||||
// Pixel-Daten holen
|
||||
const { data } = ctx.getImageData(0, 0, size, size);
|
||||
|
||||
// Einfacher, schneller Integer-Hash (FNV-1a)
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
hash ^= data[i];
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
// Einfacher, schneller Integer-Hash (FNV-1a)
|
||||
let hash = 2166136261;
|
||||
for (let i = 0; i < data.length; i++) {
|
||||
hash ^= data[i];
|
||||
hash = Math.imul(hash, 16777619);
|
||||
}
|
||||
|
||||
bitmap.close(); // GPU-Ressourcen freigeben
|
||||
return hash >>> 0; // unsigned int
|
||||
bitmap.close(); // GPU-Ressourcen freigeben
|
||||
return hash >>> 0; // unsigned int
|
||||
}
|
||||
|
||||
|
||||
|
||||
export function is_valid_name(input: string): boolean {
|
||||
return /^[\p{L}\p{N}\p{M}\-_.+,()[\]{}@!§$%&=~^ ]+$/u.test(input);
|
||||
return /^[\p{L}\p{N}\p{M}\-_.+,()[\]{}@!§$%&=~^ ]+$/u.test(input);
|
||||
}
|
||||
|
||||
export function display_status_to_info(status: DisplayStatus): string {
|
||||
switch (status) {
|
||||
case 'app_online':
|
||||
return 'Online';
|
||||
case 'app_offline':
|
||||
return 'Lädt';
|
||||
case 'host_offline':
|
||||
return 'Offline';
|
||||
case null:
|
||||
return '???';
|
||||
}
|
||||
}
|
||||
switch (status) {
|
||||
case 'app_online':
|
||||
return 'Online';
|
||||
case 'app_offline':
|
||||
return 'Lädt';
|
||||
case 'host_offline':
|
||||
return 'Offline';
|
||||
case null:
|
||||
return '???';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user