diff --git a/control/frontend/package-lock.json b/control/frontend/package-lock.json index f4afed5..4fd174d 100644 --- a/control/frontend/package-lock.json +++ b/control/frontend/package-lock.json @@ -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", diff --git a/control/frontend/src/components/Button.svelte b/control/frontend/src/components/Button.svelte index 2231a63..b92bf18 100644 --- a/control/frontend/src/components/Button.svelte +++ b/control/frontend/src/components/Button.svelte @@ -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(); }} > diff --git a/control/frontend/src/components/ControlView.svelte b/control/frontend/src/components/ControlView.svelte index 8db40b7..0606d40 100644 --- a/control/frontend/src/components/ControlView.svelte +++ b/control/frontend/src/components/ControlView.svelte @@ -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'); }}> @@ -102,8 +102,8 @@
diff --git a/control/frontend/src/components/DNDGrip.svelte b/control/frontend/src/components/DNDGrip.svelte index d0c491f..4c48dc0 100644 --- a/control/frontend/src/components/DNDGrip.svelte +++ b/control/frontend/src/components/DNDGrip.svelte @@ -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();
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); }
- {#each display_group.data as display (display.id)} + {#each $display_ids_in_group || [] as display (display.id)}
{/each} - {#if display_group.data.length === 0} + {#if ($display_ids_in_group || []).length === 0}
Hier in leere neue Gruppe ablegen
@@ -95,18 +105,18 @@
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)} > 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(); } @@ -65,8 +71,8 @@
- {:else if display.preview_url} - preview + {:else if display.preview.url} + preview {:else}
diff --git a/control/frontend/src/components/DisplayView.svelte b/control/frontend/src/components/DisplayView.svelte old mode 100644 new mode 100755 index 2a9de85..eebff14 --- a/control/frontend/src/components/DisplayView.svelte +++ b/control/frontend/src/components/DisplayView.svelte @@ -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 = 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 @@ > - {pinned_display?.name} + {$pinned_display?.name || '...'}
Aktueller Status:
@@ -159,9 +174,9 @@
- {#if pinned_display?.preview_url} + {#if $pinned_display?.preview.url} preview @@ -188,7 +203,7 @@
@@ -213,10 +216,8 @@ >{`${$selected_file_ids.length === 1 ? 'Folgendes Objekt' : `Folgende ${$selected_file_ids.length} Objekte`} löschen? (Wiederherstellung nicht möglich)`}
- {#each $selected_file_ids - .map((file_id) => get_file_by_id(file_id, $all_files, $current_file_path)) - .filter((element) => element !== null) as file} - + {#each $selected_files || [] as file} + {/each}
@@ -239,10 +240,6 @@
{/snippet} -{#snippet clipboard_hover_snippet()} -
-{/snippet} -
@@ -312,7 +309,7 @@ > @@ -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 @@ -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); }} />
@@ -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 diff --git a/control/frontend/src/ts/api_handler.ts b/control/frontend/src/ts/api_handler.ts old mode 100644 new mode 100755 index 180c33e..fe9051c --- a/control/frontend/src/ts/api_handler.ts +++ b/control/frontend/src/ts/api_handler.ts @@ -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 { - 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 { - 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 { - 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 { - 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 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 { - 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 { - let command = `cd ".${path}"`; +export async function create_folders( + ip: string, + path: string, + folder_names: string[] +): Promise { + 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 { - 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 { + 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 { - 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 { + 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 { - 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 { - 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 { - 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, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise { - 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; body?: string }, + log_in_debug: boolean = false, + supress_error_handling_http_codes: number[] = [] +): Promise { + 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, body?: any }): Promise { - 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; body?: string } +): Promise { + const url = `${window.location.origin}/api${api_route}`; + return await request(url, options); } +async function request( + url: string, + options: { method: string; headers?: Record; body?: string }, + log_in_debug: boolean = false, + supress_error_handling_http_codes: number[] = [] +): Promise { + 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 = 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, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise { - 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 { - const options = { - method: 'PATCH', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - command: command - }) - }; - return await request_display(ip, '/shellCommand', options, log_in_debug); -} \ No newline at end of file +async function run_shell_command( + ip: string, + command: string, + log_in_debug: boolean = false +): Promise { + const options = { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + command: command + }) + }; + return await request_display(ip, '/shellCommand', options, log_in_debug); +} diff --git a/control/frontend/src/ts/files_display.db.ts b/control/frontend/src/ts/files_display.db.ts new file mode 100755 index 0000000..d03d10e --- /dev/null +++ b/control/frontend/src/ts/files_display.db.ts @@ -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; + files_on_display!: Table; + displays!: Table; + display_groups!: Table; + + 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(); diff --git a/control/frontend/src/ts/indexdb/file_thumbnails.db.ts b/control/frontend/src/ts/indexdb/file_thumbnails.db.ts deleted file mode 100644 index d1d0771..0000000 --- a/control/frontend/src/ts/indexdb/file_thumbnails.db.ts +++ /dev/null @@ -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; // (Type, PrimaryKey) - - constructor() { - super("FileDatabase"); - - this.version(1).stores({ - thumbnail_blobs: "++hash, blob" // primary key - }); - } -} - -export const db = new FileDatabase(); diff --git a/control/frontend/src/ts/main.ts b/control/frontend/src/ts/main.ts old mode 100644 new mode 100755 index 757f1ca..37682d0 --- a/control/frontend/src/ts/main.ts +++ b/control/frontend/src/ts/main.ts @@ -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); -} \ No newline at end of file + await update_folder_elements_recursively(display, '/'); + await screenshot_loop(display.id); +} diff --git a/control/frontend/src/ts/stores/displays.ts b/control/frontend/src/ts/stores/displays.ts old mode 100644 new mode 100755 index 141a616..7a1f4d0 --- a/control/frontend/src/ts/stores/displays.ts +++ b/control/frontend/src/ts/stores/displays.ts @@ -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 = writable([{ - 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 { + 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 { + 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 { + 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( + run_function: (ip: string, ...args: T) => void | Promise, + 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 { + 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 { + 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) { - 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), 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"); -} \ No newline at end of file +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"); +} diff --git a/control/frontend/src/ts/stores/files.ts b/control/frontend/src/ts/stores/files.ts old mode 100644 new mode 100755 index e13d5c7..ba7a332 --- a/control/frontend/src/ts/stores/files.ts +++ b/control/frontend/src/ts/stores/files.ts @@ -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>> = writable>>({}); -// { -// 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 = writable('/'); - 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[][] { - 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[] { - 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 { + 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 { - 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 { + 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>): Set { - 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 = 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 { + 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 { - const new_folder_elements = await get_file_data(display.ip, file_path); - if (new_folder_elements === null) return 0; - all_files.update((files: Record>) => { - 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> { + 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 = 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>) => { - 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>, display: Display, folder_elements: FolderElement[], file_path: string): Record> { - 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 { + 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 { + 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>, 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>, current_file_path: string, only_from_selected_displays: boolean = false): FolderElement | null { - let current_path_elements: Record | 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 { + 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): Promise { - 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 +): Promise { + 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>): { 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 }; -} \ No newline at end of file +export async function create_folder_on_all_selected_displays( + folder_name: string, + path: string, + selected_display_ids: string[] +): Promise { + 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 => { + 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); + } + } +} diff --git a/control/frontend/src/ts/stores/notification.ts b/control/frontend/src/ts/stores/notification.ts index 9b2d0f9..9835303 100644 --- a/control/frontend/src/ts/stores/notification.ts +++ b/control/frontend/src/ts/stores/notification.ts @@ -1,4 +1,4 @@ -import { get, writable } from "svelte/store"; +import { writable } from "svelte/store"; export type Notification = { id: number; diff --git a/control/frontend/src/ts/stores/select.ts b/control/frontend/src/ts/stores/select.ts old mode 100644 new mode 100755 index 31d36fe..a04d5e3 --- a/control/frontend/src/ts/stores/select.ts +++ b/control/frontend/src/ts/stores/select.ts @@ -1,23 +1,26 @@ -import { writable, type Writable } from "svelte/store"; +import { writable, type Writable } from 'svelte/store'; -export const selected_file_ids: Writable = writable([]); +export const selected_file_ids: Writable = writable([]); // JSON.stringify([string, string, number, string]) export const selected_display_ids: Writable = writable([]); - -export function select(selected_ids: Writable, 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, + 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); } diff --git a/control/frontend/src/ts/stores/thumbnails.ts b/control/frontend/src/ts/stores/thumbnails.ts old mode 100644 new mode 100755 index 7a9a075..d7046b5 --- a/control/frontend/src/ts/stores/thumbnails.ts +++ b/control/frontend/src/ts/stores/thumbnails.ts @@ -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 = writable([]); -export async function generate_thumbnail(display_ip: string, path: string, folder_element: FolderElement): Promise { - 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 { + 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 { - 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 { + 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 []; }) -} \ No newline at end of file + const current_urls = get(active_thumbnail_urls); + for (const url of current_urls) { + URL.revokeObjectURL(url); + } + active_thumbnail_urls.update(() => { + return []; + }); +} diff --git a/control/frontend/src/ts/types.ts b/control/frontend/src/ts/types.ts old mode 100644 new mode 100755 index aabf2ac..08679b9 --- a/control/frontend/src/ts/types.ts +++ b/control/frontend/src/ts/types.ts @@ -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; +}; 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 = { - '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; + 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; -} \ No newline at end of file + return ['host_offline', 'app_offline', 'app_online'].includes(value) + ? (value as DisplayStatus) + : null; +} diff --git a/control/frontend/src/ts/utils.ts b/control/frontend/src/ts/utils.ts index f69c976..4805896 100644 --- a/control/frontend/src/ts/utils.ts +++ b/control/frontend/src/ts/utils.ts @@ -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 = supported_file_types_json as Record; +const supported_file_types: Record = 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 { - // 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 '???'; - } -} \ No newline at end of file + switch (status) { + case 'app_online': + return 'Online'; + case 'app_offline': + return 'Lädt'; + case 'host_offline': + return 'Offline'; + case null: + return '???'; + } +}