mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
refactor(frontent): move components and ts to lib folder
This commit is contained in:
Executable
+302
@@ -0,0 +1,302 @@
|
||||
import { notifications } from './stores/notification';
|
||||
import {
|
||||
to_display_status,
|
||||
type DisplayStatus,
|
||||
type Inode,
|
||||
type RequestResponse,
|
||||
type ShellCommandResponse,
|
||||
type TreeElement
|
||||
} from './types';
|
||||
|
||||
export async function get_screenshot(ip: string): Promise<Blob | null> {
|
||||
const options = { method: 'PATCH' };
|
||||
const response = await request_display(ip, '/takeScreenshot', options);
|
||||
if (!response.ok || !response.blob) return null;
|
||||
return response.blob;
|
||||
}
|
||||
|
||||
export async function open_file(ip: string, path_to_file: string): Promise<void> {
|
||||
const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } };
|
||||
await request_display(ip, `/file${path_to_file}`, options);
|
||||
}
|
||||
|
||||
export async function send_keyboard_input(ip: string, key: string): Promise<void> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
key: key
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/keyboardInput', options);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
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")
|
||||
[ "$created" = "-" ] && created=$(stat -c '%y' -- "$f")
|
||||
jq -n --arg name "$f" --arg type "$typ" --arg size "$size" --arg created "$created" \
|
||||
'{name:$name, type:$type, size:($size|tostring), created:$created}' | tr -d '\n'
|
||||
echo
|
||||
done
|
||||
`;
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
if (json_response.exitCode === 0 && json_response.stdout.trim() === '') return [];
|
||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||
|
||||
const response: FileInfo[] = json_response.stdout
|
||||
.trim()
|
||||
.split('\n')
|
||||
.filter(Boolean)
|
||||
.map((line: string) => JSON.parse(line) as FileInfo);
|
||||
|
||||
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) === '.') continue;
|
||||
const folder_element: Inode = {
|
||||
path: path,
|
||||
name: response_element.name.slice(2), // remove "./"
|
||||
type: response_element.type,
|
||||
size: Number(response_element.size),
|
||||
thumbnail: null
|
||||
};
|
||||
folder_element_list.push({ folder_element, date_created: new Date(response_element.created) });
|
||||
}
|
||||
return folder_element_list;
|
||||
}
|
||||
|
||||
export async function get_file_tree_data(ip: string, path: string): Promise<TreeElement[] | null> {
|
||||
const command = `cd ".${path}" && tree -Js`;
|
||||
const raw_response = await run_shell_command(ip, command, true);
|
||||
|
||||
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;
|
||||
|
||||
const tree_element: TreeElement | null = JSON.parse(json_response.stdout.trim())[0] || null;
|
||||
|
||||
return tree_element?.contents || null;
|
||||
}
|
||||
|
||||
export async function create_folders(
|
||||
ip: string,
|
||||
path: string,
|
||||
folder_names: string[]
|
||||
): Promise<void> {
|
||||
let command = `cd ".${path}"`;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
export async function rename_file(
|
||||
ip: string,
|
||||
path: string,
|
||||
old_file_name: string,
|
||||
new_file_name: string
|
||||
): Promise<void> {
|
||||
const command: string = `cd ".${path}" && mv "${old_file_name}" "${new_file_name}"`;
|
||||
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
}
|
||||
|
||||
export async function delete_files(
|
||||
ip: string,
|
||||
current_path: string,
|
||||
file_names: string[]
|
||||
): Promise<void> {
|
||||
let command: string = `cd ".${current_path}"`;
|
||||
for (const file_name of file_names) {
|
||||
command += ` && rm -r "${file_name}"`;
|
||||
}
|
||||
const raw_response = await run_shell_command(ip, command);
|
||||
if (!raw_response.ok || !raw_response.json) return;
|
||||
const json_response = raw_response.json as ShellCommandResponse;
|
||||
handle_shell_error(ip, json_response, command, true);
|
||||
}
|
||||
|
||||
export async function show_blackscreen(ip: string): Promise<void> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
html: ``
|
||||
})
|
||||
};
|
||||
await request_display(ip, '/showHTML', options);
|
||||
}
|
||||
|
||||
export async function get_thumbnail_blob(ip: string, path_to_file: string): Promise<Blob | null> {
|
||||
const raw_response = await request_display(
|
||||
ip,
|
||||
`/file/preview${path_to_file}`,
|
||||
{ method: 'GET' },
|
||||
true,
|
||||
[415]
|
||||
);
|
||||
if (!raw_response.ok || !raw_response.blob) return null;
|
||||
return raw_response.blob;
|
||||
}
|
||||
|
||||
export async function ping_ip(ip: string): Promise<DisplayStatus> {
|
||||
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
||||
if (!raw_response.ok || !raw_response.json) return null;
|
||||
|
||||
const status = raw_response.json.status;
|
||||
if (typeof status === 'string') {
|
||||
return to_display_status(status);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
async function request_display(
|
||||
ip: string,
|
||||
api_route: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||
log_in_debug: boolean = false,
|
||||
supress_error_handling_http_codes: number[] = []
|
||||
): Promise<RequestResponse> {
|
||||
const url = `http://${ip}:1323/api${api_route}`;
|
||||
return await request(url, options, log_in_debug, supress_error_handling_http_codes);
|
||||
}
|
||||
|
||||
async function request_control(
|
||||
api_route: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string }
|
||||
): Promise<RequestResponse> {
|
||||
const url = `${window.location.origin}/api${api_route}`;
|
||||
return await request(url, options);
|
||||
}
|
||||
|
||||
async function request(
|
||||
url: string,
|
||||
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||
log_in_debug: boolean = false,
|
||||
supress_error_handling_http_codes: number[] = []
|
||||
): Promise<RequestResponse> {
|
||||
try {
|
||||
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
||||
if (log_in_debug) {
|
||||
console.debug(url + cache_buster, options.body);
|
||||
} else {
|
||||
console.log(url + cache_buster, options.body);
|
||||
}
|
||||
const response = await fetch(url + cache_buster, options);
|
||||
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
|
||||
const contentType = response.headers.get('content-type') || '';
|
||||
let request_response: RequestResponse;
|
||||
if (!contentType.includes('application/json')) {
|
||||
const blob: Blob = await response.blob();
|
||||
request_response = { ok: response.ok, http_code: response.status, blob: blob };
|
||||
} else {
|
||||
const json: Record<string, unknown> = await response.json();
|
||||
request_response = { ok: response.ok, http_code: response.status, json: json };
|
||||
}
|
||||
if (log_in_debug) {
|
||||
console.debug(request_response);
|
||||
} else {
|
||||
console.log(request_response);
|
||||
}
|
||||
return request_response;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
async function run_shell_command(
|
||||
ip: string,
|
||||
command: string,
|
||||
log_in_debug: boolean = false
|
||||
): Promise<RequestResponse> {
|
||||
const options = {
|
||||
method: 'PATCH',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
command: command
|
||||
})
|
||||
};
|
||||
return await request_display(ip, '/shellCommand', options, log_in_debug);
|
||||
}
|
||||
+56
@@ -0,0 +1,56 @@
|
||||
import Dexie, { type Table } from 'dexie';
|
||||
import type { Display, DisplayGroup, Inode } from './types';
|
||||
|
||||
export interface FileOnDisplay {
|
||||
display_id: string;
|
||||
file_primary_key: string; // JSON.stringify([string, string, number, string])
|
||||
date_created: Date;
|
||||
is_loading: boolean;
|
||||
percentage: number;
|
||||
}
|
||||
|
||||
export class FileDatabase extends Dexie {
|
||||
files!: Table<Inode, [string, string, number, string]>;
|
||||
files_on_display!: Table<FileOnDisplay, [string, string]>;
|
||||
displays!: Table<Display, string>;
|
||||
display_groups!: Table<DisplayGroup, string>;
|
||||
|
||||
constructor() {
|
||||
super('FileDatabase');
|
||||
|
||||
this.version(1).stores({
|
||||
files: `
|
||||
[path+name+size+type],
|
||||
path,
|
||||
name,
|
||||
size,
|
||||
type,
|
||||
thumbnail
|
||||
`,
|
||||
files_on_display: `
|
||||
[display_id+file_primary_key],
|
||||
display_id,
|
||||
file_primary_key,
|
||||
date_created,
|
||||
is_loading,
|
||||
percentage
|
||||
`,
|
||||
displays: `
|
||||
id,
|
||||
ip,
|
||||
mac,
|
||||
position,
|
||||
preview,
|
||||
group_id,
|
||||
name,
|
||||
status
|
||||
`,
|
||||
display_groups: `
|
||||
id,
|
||||
position
|
||||
`
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export const db = new FileDatabase();
|
||||
Executable
+34
@@ -0,0 +1,34 @@
|
||||
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.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() {
|
||||
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 screenshot_loop(display.id);
|
||||
}
|
||||
+196
@@ -0,0 +1,196 @@
|
||||
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 async function is_display_name_taken(name: string): Promise<boolean> {
|
||||
const exists = await db.displays.where('name').equals(name).first();
|
||||
return !!exists;
|
||||
}
|
||||
|
||||
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
|
||||
) {
|
||||
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 async function remove_display(display_id: string) {
|
||||
select(selected_display_ids, display_id, 'deselect');
|
||||
await delete_and_deselect_unique_files_from_display(display_id);
|
||||
|
||||
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 async function all_displays_of_group_selected(
|
||||
display_group_id: string,
|
||||
current_selected_displays: string[]
|
||||
): Promise<boolean> {
|
||||
const displays_of_group: Display[] = await db.displays
|
||||
.where('group_id')
|
||||
.equals(display_group_id)
|
||||
.toArray();
|
||||
if (displays_of_group.length === 0) return false;
|
||||
|
||||
for (const display of displays_of_group) {
|
||||
if (!is_selected(display.id, current_selected_displays)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export 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 async function get_display_by_id(display_id: string): Promise<Display | null> {
|
||||
return (await db.displays.get(display_id)) ?? 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 async function run_on_all_selected_displays<T extends unknown[]>(
|
||||
run_function: (ip: string, ...args: T) => void | Promise<void>,
|
||||
update_screenshot_afterwards: boolean,
|
||||
...args: T
|
||||
) {
|
||||
for (const display_id of get(selected_display_ids)) {
|
||||
const display_ip = (await get_display_by_id(display_id))?.ip;
|
||||
if (display_ip) {
|
||||
await run_function(display_ip, ...args);
|
||||
if (update_screenshot_afterwards) {
|
||||
await screenshot_loop(display_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function get_display_groups(): Promise<DisplayGroup[]> {
|
||||
return await db.display_groups.orderBy('position').toArray();
|
||||
}
|
||||
|
||||
export async function get_display_ids_in_group(display_group_id: string): Promise<Display[]> {
|
||||
const displays: Display[] = await db.displays
|
||||
.where('group_id')
|
||||
.equals(display_group_id)
|
||||
.sortBy('position');
|
||||
return displays;
|
||||
}
|
||||
|
||||
export async function 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 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]);
|
||||
}
|
||||
}
|
||||
|
||||
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");
|
||||
}
|
||||
Executable
+373
@@ -0,0 +1,373 @@
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { get_file_primary_key, type Display, type Inode, type TreeElement } from '../types';
|
||||
import { get_display_by_id } from './displays';
|
||||
import { select, selected_display_ids, selected_file_ids } from './select';
|
||||
import { create_folders, get_file_data, get_file_tree_data } from '../api_handler';
|
||||
import { deactivate_old_thumbnail_urls, generate_thumbnail } from './thumbnails';
|
||||
import { db, type FileOnDisplay } from '../files_display.db';
|
||||
|
||||
export const current_file_path: Writable<string> = writable<string>('/');
|
||||
|
||||
export async function change_file_path(new_path: string) {
|
||||
current_file_path.update(() => {
|
||||
return new_path;
|
||||
});
|
||||
selected_file_ids.update(() => {
|
||||
return [];
|
||||
});
|
||||
|
||||
deactivate_old_thumbnail_urls();
|
||||
|
||||
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 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 of await db.displays.where('id').anyOf(get(selected_display_ids)).toArray()) {
|
||||
await update_folder_elements_recursively(display, current_path);
|
||||
}
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
async function get_display_ids_where_file_is_missing(
|
||||
file: Inode,
|
||||
selected_display_ids: string[]
|
||||
): Promise<string[]> {
|
||||
const file_primary_key = get_file_primary_key(file);
|
||||
const files_on_selected_displays = await db.files_on_display
|
||||
.where('file_primary_key')
|
||||
.equals(file_primary_key)
|
||||
.filter((e) => selected_display_ids.includes(e.display_id))
|
||||
.toArray();
|
||||
return selected_display_ids.filter(
|
||||
(id) => !files_on_selected_displays.some((item) => item.display_id === id)
|
||||
);
|
||||
}
|
||||
|
||||
export async function get_displays_where_path_exists(
|
||||
path: string,
|
||||
selected_display_ids: string[],
|
||||
invert: boolean
|
||||
): Promise<Display[]> {
|
||||
if (path === '/') return [];
|
||||
const last_path_part =
|
||||
path
|
||||
.slice(0, path.length - 1)
|
||||
.split('/')
|
||||
.at(-1) ?? '';
|
||||
const path_without_last_part = path.slice(0, path.length - (last_path_part.length + 1));
|
||||
|
||||
const folders_of_current_path = await db.files
|
||||
.where('[path+name+type]')
|
||||
.equals([path_without_last_part, last_path_part, 'inode/directory'])
|
||||
.first();
|
||||
if (!folders_of_current_path)
|
||||
return await db.displays.where('id').anyOf(selected_display_ids).toArray();
|
||||
const folder_primary_key = get_file_primary_key(folders_of_current_path);
|
||||
|
||||
const display_ids = selected_display_ids.filter(async (display_id) => {
|
||||
const folder_exists = await db.files_on_display.get([display_id, folder_primary_key]);
|
||||
if (invert) {
|
||||
return !folder_exists;
|
||||
} else {
|
||||
return folder_exists;
|
||||
}
|
||||
});
|
||||
|
||||
return (await db.displays.bulkGet(display_ids)).filter((e) => e !== undefined);
|
||||
}
|
||||
|
||||
async function get_changed_directory_paths(
|
||||
display: Display,
|
||||
file_path: string
|
||||
): Promise<string[] | null> {
|
||||
const current_folder = await get_file_tree_data(display.ip, file_path);
|
||||
const directory_strings = await get_recursive_changed_directory_paths(
|
||||
display,
|
||||
file_path,
|
||||
current_folder
|
||||
);
|
||||
if (directory_strings.size === 0) return null;
|
||||
const directory_strings_array = [...directory_strings];
|
||||
return directory_strings_array.filter(
|
||||
(e) => !directory_strings_array.some((f) => f !== e && f.startsWith(e))
|
||||
);
|
||||
}
|
||||
|
||||
async function get_recursive_changed_directory_paths(
|
||||
display: Display,
|
||||
current_file_path: string,
|
||||
current_folder_elements: TreeElement[] | null
|
||||
): Promise<Set<string>> {
|
||||
const files_folder: Inode[] = await db.files.where('path').equals(current_file_path).toArray();
|
||||
if (
|
||||
(!files_folder || files_folder.length === 0) &&
|
||||
(!current_folder_elements || current_folder_elements.length === 0)
|
||||
) {
|
||||
return new Set([]); // no data -> no update needed
|
||||
} else if (
|
||||
!files_folder ||
|
||||
!current_folder_elements ||
|
||||
current_folder_elements.length !== files_folder.length
|
||||
) {
|
||||
return new Set([current_file_path]); // existing data does not match new data -> update
|
||||
}
|
||||
|
||||
const has_changed: Set<string> = new Set();
|
||||
for (const tree_folder_element of current_folder_elements) {
|
||||
const folder_element = files_folder.find((e) => e.name === tree_folder_element.name);
|
||||
if (
|
||||
!folder_element ||
|
||||
(tree_folder_element.type !== 'directory' && folder_element.size !== tree_folder_element.size)
|
||||
) {
|
||||
return new Set([current_file_path]);
|
||||
}
|
||||
|
||||
if (tree_folder_element.type === 'directory' && tree_folder_element.contents) {
|
||||
const new_file_path = current_file_path + tree_folder_element.name + '/';
|
||||
for (const string of await get_recursive_changed_directory_paths(
|
||||
display,
|
||||
new_file_path,
|
||||
tree_folder_element.contents
|
||||
)) {
|
||||
has_changed.add(string);
|
||||
}
|
||||
}
|
||||
}
|
||||
return has_changed;
|
||||
}
|
||||
|
||||
export async function update_folder_elements_recursively(
|
||||
display: Display,
|
||||
file_path: string = '/'
|
||||
): Promise<void> {
|
||||
const new_folder_elements = await get_file_data(display.ip, file_path);
|
||||
if (!new_folder_elements) return;
|
||||
|
||||
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();
|
||||
|
||||
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: 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_keys.has(get_file_primary_key(e)));
|
||||
const only_in_new = new_elements.filter(
|
||||
(e) => !old_keys.has(get_file_primary_key(e.folder_element))
|
||||
);
|
||||
return { deleted: only_in_old, new: only_in_new };
|
||||
}
|
||||
|
||||
export async function get_current_folder_elements(
|
||||
current_file_path: string,
|
||||
selected_display_ids: string[]
|
||||
): Promise<Inode[]> {
|
||||
const existing_file_keys_on_selected_displays: [string, string, number, string][] = (
|
||||
await db.files_on_display.where('display_id').anyOf(selected_display_ids).toArray()
|
||||
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||
const existing_files_on_selected_displays_in_path: Inode[] = await db.files
|
||||
.where('[path+name+size+type]')
|
||||
.anyOf(existing_file_keys_on_selected_displays)
|
||||
.filter((e) => e.path === current_file_path)
|
||||
.toArray();
|
||||
|
||||
return sort_files(existing_files_on_selected_displays_in_path);
|
||||
}
|
||||
|
||||
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;
|
||||
|
||||
// Danach alphabetisch nach name (case-insensitive)
|
||||
const nameCompare = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||||
if (nameCompare !== 0) return nameCompare;
|
||||
|
||||
return -1;
|
||||
});
|
||||
return files;
|
||||
}
|
||||
|
||||
export async function get_file_by_id(
|
||||
file_primary_key: string,
|
||||
only_from_selected_displays: boolean = false
|
||||
): Promise<Inode | null> {
|
||||
const file = (await db.files.get(JSON.parse(file_primary_key))) ?? null;
|
||||
if (!file || !only_from_selected_displays) {
|
||||
return file;
|
||||
} else {
|
||||
const exist_on_selected_display = !!(await db.files_on_display
|
||||
.where('file_primary_key')
|
||||
.equals(file_primary_key)
|
||||
.filter((e) => get(selected_display_ids).includes(e.display_id))
|
||||
.first());
|
||||
return exist_on_selected_display ? file : null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function run_for_selected_files_on_selected_displays(
|
||||
action: (ip: string, file_names: string[]) => Promise<void>
|
||||
): Promise<void> {
|
||||
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);
|
||||
|
||||
const display = await get_display_by_id(display_id);
|
||||
if (!display) continue;
|
||||
|
||||
await action(display.ip, file_names_on_display);
|
||||
}
|
||||
}
|
||||
|
||||
export async function create_folder_on_all_selected_displays(
|
||||
folder_name: string,
|
||||
path: string,
|
||||
selected_display_ids: string[]
|
||||
): Promise<void> {
|
||||
const path_parts = path
|
||||
.slice(1, path.length - 1)
|
||||
.split('/')
|
||||
.filter((e) => e.length !== 0);
|
||||
let remaining_display_ids = [...selected_display_ids];
|
||||
|
||||
const getDisplaysForPath = async (currentPath: string): Promise<Display[]> => {
|
||||
if (currentPath === '/') {
|
||||
const displays = await db.displays.bulkGet(remaining_display_ids);
|
||||
return displays.filter((d): d is Display => !!d);
|
||||
}
|
||||
return get_displays_where_path_exists(currentPath, remaining_display_ids, false);
|
||||
};
|
||||
|
||||
for (let depth = path_parts.length; depth >= 0 && remaining_display_ids.length; depth--) {
|
||||
const currentPath = depth === 0 ? '/' : `/${path_parts.slice(0, depth).join('/')}/`;
|
||||
|
||||
const displays = await getDisplaysForPath(currentPath);
|
||||
if (!displays.length) continue;
|
||||
|
||||
const folders_to_create = [...path_parts.slice(depth), folder_name];
|
||||
for (const display of displays) {
|
||||
await create_folders(display.ip, currentPath, folders_to_create);
|
||||
remaining_display_ids = remaining_display_ids.filter((id) => id !== display.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { writable } from "svelte/store";
|
||||
|
||||
export type Notification = {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
duration: number;
|
||||
className: string;
|
||||
type?: "error" | "success" | "info";
|
||||
};
|
||||
|
||||
function createNotifications() {
|
||||
const { subscribe, update } = writable<Notification[]>([]);
|
||||
|
||||
function push(type: "error" | "success" | "info", title: string, message: string = "", className: string = "") {
|
||||
const id = Date.now();
|
||||
const duration = type === "error" ? 16000 : 4000;
|
||||
update((n) => [...n, { id, title, message, duration, className, type }]);
|
||||
setTimeout(() => {
|
||||
update((n) => n.filter((x) => x.id !== id));
|
||||
}, duration);
|
||||
}
|
||||
const remove = (id: number) => update(n => n.filter(x => x.id !== id));
|
||||
|
||||
return { subscribe, push, remove };
|
||||
}
|
||||
|
||||
export const notifications = createNotifications();
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
import { writable, type Writable } from 'svelte/store';
|
||||
|
||||
export const selected_file_ids: Writable<string[]> = writable<string[]>([]); // JSON.stringify([string, string, number, string])
|
||||
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
|
||||
|
||||
export function select(
|
||||
selected_ids: Writable<string[]>,
|
||||
id: string,
|
||||
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);
|
||||
}
|
||||
+43
@@ -0,0 +1,43 @@
|
||||
import { get, writable, type Writable } from 'svelte/store';
|
||||
import { get_thumbnail_blob } from '../api_handler';
|
||||
import { type Inode } from '../types';
|
||||
import { db } from '../files_display.db';
|
||||
import { get_file_type } from '../utils';
|
||||
|
||||
export const active_thumbnail_urls: Writable<string[]> = writable<string[]>([]);
|
||||
|
||||
export async function generate_thumbnail(
|
||||
display_ip: string,
|
||||
path: string,
|
||||
folder_element: Inode
|
||||
): Promise<void> {
|
||||
const supported_file_type = get_file_type(folder_element);
|
||||
if (!supported_file_type) return;
|
||||
|
||||
const thumbnail_blob = await get_thumbnail_blob(display_ip, path + folder_element.name);
|
||||
if (!thumbnail_blob) return;
|
||||
|
||||
folder_element.thumbnail = thumbnail_blob;
|
||||
|
||||
await db.files.put(folder_element); // save
|
||||
}
|
||||
|
||||
export async function get_thumbnail_url(file: Inode): Promise<string | null> {
|
||||
if (!file.thumbnail) return null;
|
||||
const new_url = URL.createObjectURL(file.thumbnail);
|
||||
active_thumbnail_urls.update((current: string[]) => {
|
||||
current.push(new_url);
|
||||
return current;
|
||||
});
|
||||
return new_url;
|
||||
}
|
||||
|
||||
export function deactivate_old_thumbnail_urls() {
|
||||
const current_urls = get(active_thumbnail_urls);
|
||||
for (const url of current_urls) {
|
||||
URL.revokeObjectURL(url);
|
||||
}
|
||||
active_thumbnail_urls.update(() => {
|
||||
return [];
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { writable, type Writable } from "svelte/store";
|
||||
|
||||
export const dnd_flip_duration_ms = 300;
|
||||
|
||||
const heights_options: Record<string, { min: number; max: number; step: number }> = {
|
||||
display: {
|
||||
min: 15,
|
||||
max: 40,
|
||||
step: 5,
|
||||
},
|
||||
file: {
|
||||
min: 10,
|
||||
max: 20,
|
||||
step: 5,
|
||||
}
|
||||
}
|
||||
|
||||
export const current_height: Writable<Record<string, number>> = writable<Record<string, number>>({
|
||||
display: 25,
|
||||
file: 10,
|
||||
});
|
||||
|
||||
|
||||
export const is_group_drag: Writable<boolean> = writable<boolean>(false);
|
||||
export const is_display_drag: Writable<boolean> = writable<boolean>(false);
|
||||
|
||||
export const pinned_display_id: Writable<string | null> = writable<string | null>(null);
|
||||
|
||||
|
||||
export function change_height(key: 'display' | 'file', factor: number) {
|
||||
current_height.update((height) => {
|
||||
height[key] = next_height_step_size(key, height, factor) || height[key];
|
||||
return height;
|
||||
});
|
||||
}
|
||||
|
||||
export function next_height_step_size(key: 'display' | 'file', current_height_array: Record<string, number>, factor: number): number {
|
||||
const new_size = current_height_array[key] + (factor * heights_options[key].step);
|
||||
if (new_size > heights_options[key].max || new_size < heights_options[key].min) {
|
||||
return 0;
|
||||
} else {
|
||||
return new_size;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
export function get_selectable_color_classes(
|
||||
selected: boolean,
|
||||
returning_classes: { bg?: boolean; hover?: boolean; active?: boolean; text?: boolean } = {},
|
||||
base_bg_distance: number = 0,
|
||||
shifted_distance: number = 0
|
||||
) {
|
||||
let base_bg = selected ? 'bg-stone-400' : 'bg-stone-600';
|
||||
const base_text = selected ? 'text-stone-950' : 'text-stone-200';
|
||||
base_bg = get_shifted_color(base_bg, base_bg_distance);
|
||||
|
||||
const { bg = false, hover = false, active = false, text = false } = returning_classes;
|
||||
|
||||
const out: string[] = [];
|
||||
if (bg) out.push(base_bg);
|
||||
if (hover) out.push('hover:' + get_shifted_color(base_bg, 100 + shifted_distance));
|
||||
if (active) out.push('active:' + get_shifted_color(base_bg, 150 + shifted_distance));
|
||||
if (text) out.push(base_text);
|
||||
|
||||
return out.join(' ');
|
||||
}
|
||||
|
||||
export function get_shifted_color(base_color: string, distance: number): string {
|
||||
return base_color.replace(/(\d+)(?=(?:\/\d+)?$)/, (m: string) => String(+Number(m) - distance));
|
||||
}
|
||||
Executable
+97
@@ -0,0 +1,97 @@
|
||||
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?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type ShellCommandResponse = {
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exitCode: number;
|
||||
};
|
||||
|
||||
export type SupportedFileType = {
|
||||
display_name: string;
|
||||
mime_type: string;
|
||||
};
|
||||
export const supported_file_type_icon: Record<string, typeof X> = {
|
||||
MP4: FileVideoCamera,
|
||||
JPG: FileImage,
|
||||
PNG: FileImage,
|
||||
GIF: ImagePlay,
|
||||
PPTX: FileBox,
|
||||
ODP: FileBox,
|
||||
PDF: FileText
|
||||
};
|
||||
|
||||
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;
|
||||
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 | Promise<void>;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
export type PopupContent = {
|
||||
open: boolean;
|
||||
snippet: Snippet<[string]> | null;
|
||||
snippet_arg?: string;
|
||||
title?: string;
|
||||
title_class?: string;
|
||||
title_icon?: typeof X | null;
|
||||
window_class?: string;
|
||||
closable?: boolean;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
import type { DisplayStatus, Inode, SupportedFileType } from './types';
|
||||
import supported_file_types_json from './../../../../../shared/supported_file_types.json';
|
||||
|
||||
const supported_file_types: Record<string, SupportedFileType> = supported_file_types_json as Record<
|
||||
string,
|
||||
SupportedFileType
|
||||
>;
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
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';
|
||||
|
||||
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 size_string = `${value.toFixed(toFixed !== null ? toFixed : Math.max(0, 2 - Math.floor(Math.log10(value))))} ${sizes[i]}`;
|
||||
|
||||
return size_string.replace('.', ',');
|
||||
}
|
||||
|
||||
export async function image_content_hash(blob: Blob, size = 32): Promise<number> {
|
||||
// Blob → ImageBitmap (GPU-dekodiert, superschnell)
|
||||
const bitmap = await createImageBitmap(blob);
|
||||
|
||||
// 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);
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
export function is_valid_name(input: string): boolean {
|
||||
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 '???';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user