Abbrechen
@@ -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 '???';
+ }
+}