From efb99cbac060e7bf3b3f20c77df3b80f4870f2f6 Mon Sep 17 00:00:00 2001 From: E44 <129310925+programmer-44@users.noreply.github.com> Date: Sat, 22 Nov 2025 23:30:36 +0100 Subject: [PATCH] chore(control): add delete file and create folder action --- .../frontend/src/components/FileView.svelte | 151 +++++++++++++++--- .../src/components/FolderElementObject.svelte | 121 +++++++------- control/frontend/src/ts/api_handler.ts | 84 ++++++---- control/frontend/src/ts/stores/files.ts | 73 ++++++++- 4 files changed, 313 insertions(+), 116 deletions(-) diff --git a/control/frontend/src/components/FileView.svelte b/control/frontend/src/components/FileView.svelte index 2da45d7..c0aa712 100644 --- a/control/frontend/src/components/FileView.svelte +++ b/control/frontend/src/components/FileView.svelte @@ -16,17 +16,30 @@ import Button from './Button.svelte'; import PathBar from './PathBar.svelte'; import { selected_display_ids, selected_file_ids } from '../ts/stores/select'; - import { all_files, current_file_path, get_current_folder_elements } from '../ts/stores/files'; + import { + all_files, + current_file_path, + get_current_folder_elements, + get_display_ids_where_file_is_missing, + get_display_ids_where_path_does_not_exist, + get_file_from_id, + get_longest_existing_path_and_needed_parts, + run_for_selected_files_on_selected_displays, + update_current_folder_on_selected_displays + } from '../ts/stores/files'; import { slide } from 'svelte/transition'; import FolderElementObject from './FolderElementObject.svelte'; import PopUp from './PopUp.svelte'; import type { PopupContent } from '../ts/types'; import TextInput from './TextInput.svelte'; import { is_valid_name } from '../ts/utils'; + import { displays, get_display_by_id, run_on_all_selected_displays } from '../ts/stores/displays'; + import { create_folders, delete_files } from '../ts/api_handler'; + import { get } from 'svelte/store'; import HighlightedText from './HighlightedText.svelte'; - let current_name: string = ''; - let current_valid: boolean = false; + let current_name: string = $state(''); + let current_valid: boolean = $state(false); let popup_content: PopupContent = $state({ open: false, @@ -40,6 +53,8 @@ } const show_new_folder_popup = () => { + current_name = ''; + current_valid = false; popup_content = { open: true, snippet: new_folder_popup, @@ -48,35 +63,120 @@ closable: true }; }; + + const delete_folder_element_popup = () => { + popup_content = { + open: true, + snippet: delete_request_popup, + title: `${$selected_file_ids.length} ${$selected_file_ids.length === 1 ? 'Objekt' : 'Objekte'} wirklich löschen?`, + title_icon: Trash2, + closable: true + }; + }; {#snippet new_folder_popup()} -
- { - if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen']; - const trimmed_input = input.trim(); - if (trimmed_input.length === 0 || trimmed_input.length > 50) - return [false, 'Ungültige Länge']; - if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen']; - if ( - get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some( - (e) => e.name === trimmed_input - ) + {#if get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files).length > 0} + Der aktuelle Pfad {$current_file_path.slice(0, $current_file_path.length - 1)} existiert nicht auf {get_display_ids_where_path_does_not_exist( + $current_file_path, + $selected_display_ids, + $all_files + ).length === 1 + ? 'dem Bildschirm' + : 'den Bildschirmen'} + {#each get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files) as display_id, i} + {#if i !== 0} + , + {/if} + {get_display_by_id(display_id, $displays)?.name} + {/each}. Mit der Erstellung dieses Ordners wird der Pfad automatisch mit leeren Ordnern bis + zum aktuellen Pfad aufgefüllt. + + {/if} + { + if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen']; + const trimmed_input = input.trim(); + if (trimmed_input.length === 0 || trimmed_input.length > 50) + return [false, 'Ungültige Länge']; + if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen']; + if ( + get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some( + (e) => e.name === trimmed_input ) - return [false, 'Name bereits verwendet']; - return [true, 'Gültiger Name']; + ) + return [false, 'Name bereits verwendet']; + return [true, 'Gültiger Name']; + }} + /> +
+ +
+{/snippet} + +{#snippet delete_request_popup()} +
+ {`${$selected_file_ids.length === 1 ? 'Folgendes Objekt' : `Folgende ${$selected_file_ids.length} Objekte`} löschen? (Wiederherstellung nicht möglich)`} +
+ {#each $selected_file_ids + .map((file_id) => get_file_from_id(file_id, $all_files, $current_file_path)) + .filter((element) => element !== null) as file} + + {/each} +
- + +
{/snippet} +{#snippet clipboard_hover_snippet()} +
+{/snippet} +
@@ -110,7 +210,7 @@
-
+
-
+
{#if $selected_display_ids.length === 0} Es wurden keine Bildschirme ausgewählt. diff --git a/control/frontend/src/components/FolderElementObject.svelte b/control/frontend/src/components/FolderElementObject.svelte index d79053c..f408aee 100644 --- a/control/frontend/src/components/FolderElementObject.svelte +++ b/control/frontend/src/components/FolderElementObject.svelte @@ -46,7 +46,10 @@ import { onDestroy, onMount } from 'svelte'; import { liveQuery } from 'dexie'; - let { file } = $props<{ file: FolderElement }>(); + let { file, not_interactable = false } = $props<{ + file: FolderElement; + not_interactable?: boolean; + }>(); let thumbnail_url: string | null = $state(null); // Update thumbnail_url automatically if data is available @@ -88,18 +91,21 @@ } function get_grayed_out_text_color_strings(is_selected: boolean): string { + if (not_interactable) return 'text-stone-400'; const color = is_selected ? 'text-stone-600' : 'text-stone-400'; const factor = is_selected ? -1 : 1; return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`; } function get_grayed_out_border_color_strings(is_selected: boolean): string { + if (not_interactable) return 'border-stone-550'; const color = is_selected ? 'border-stone-450' : 'border-stone-550'; const factor = is_selected ? 1 : 1; return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`; } function onclick(e: Event) { + if (not_interactable) return; select(selected_file_ids, file.id); e.stopPropagation(); } @@ -115,51 +121,53 @@
-
- -
+ {#if !not_interactable} +
+ +
+ {/if}
@@ -203,7 +216,7 @@ is_selected(file.id, $selected_file_ids) )} duration-200 transition-colors" > - {#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0} +
{ +export async function get_file_data(ip: string, path: string): Promise { interface FileInfo { name: string; type: string; @@ -44,25 +44,20 @@ export async function get_file_data(ip: string, path: string): Promise { - const options = { - method: 'PATCH', - headers: { 'content-type': 'application/json' }, - body: JSON.stringify({ - command: `cd ".${path}" && tree -Js` - }) - }; - const raw_response = await request_display(ip, '/shellCommand', options); + const raw_response = await run_shell_command(ip, `cd ".${path}" && tree -Js`); if (!raw_response.ok || !raw_response.json) return null; const json_response = raw_response.json as ShellCommandResponse; @@ -110,6 +98,25 @@ export async function get_file_tree_data(ip: string, path: string): Promise { + let command = `cd ".${path}"`; + + for (const part of folder_names) { + command += `&& mkdir "${part}" && cd "${part}/"`; + } + + await run_shell_command(ip, command); +} + +export async function delete_files(ip: string, current_path: string, file_names: string[]) { + let del_string: string = ''; + for (const file_name of file_names) { + del_string += `&& rm -r "${file_name}"`; + } + await run_shell_command(ip, `cd ".${current_path}" ${del_string}`); + +} + export async function show_blackscreen(ip: string): Promise { const options = { @@ -122,19 +129,17 @@ export async function show_blackscreen(ip: string): Promise { await request_display(ip, '/showHTML', options); } - -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; -} - 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' }, [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; +} @@ -203,4 +208,15 @@ function is_cd_directory_error(ip: string, shell_response: ShellCommandResponse) } if (shell_response.stdout.trim() === '') return true; return false; +} + +async function run_shell_command(ip: string, command: string): Promise { + const options = { + method: 'PATCH', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ + command: command + }) + }; + return await request_display(ip, '/shellCommand', options); } \ No newline at end of file diff --git a/control/frontend/src/ts/stores/files.ts b/control/frontend/src/ts/stores/files.ts index 41f7777..f9e3c17 100644 --- a/control/frontend/src/ts/stores/files.ts +++ b/control/frontend/src/ts/stores/files.ts @@ -1,7 +1,7 @@ import { get, writable, type Writable } from "svelte/store"; import type { Display, FolderElement, TreeElement } from "../types"; -import { displays } from "./displays"; -import { selected_file_ids } from "./select"; +import { displays, get_display_by_id } from "./displays"; +import { 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"; @@ -45,8 +45,17 @@ export async function change_file_path(new_path: string) { } } +export async function update_current_folder_on_selected_displays() { + 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); + } +} + 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 []; + if (!all_files.hasOwnProperty(path)) return [selected_display_ids, []]; const missing: string[] = []; const colliding: string[] = []; Display: @@ -68,6 +77,17 @@ export function get_display_ids_where_file_is_missing(path: string, file: Folder 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_changed_directory_paths(display: Display, file_path: string): Promise { const current_folder = await get_file_tree_data(display.ip, file_path); if (current_folder === null) return [file_path]; @@ -103,6 +123,7 @@ function get_recursive_changed_directory_paths(display: Display, current_file_pa 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] = {}; @@ -223,8 +244,54 @@ function sort_files(files: FolderElement[]) { 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; } +export function get_file_from_id(file_id: string, all_files: Record>, current_file_path: string): FolderElement | null { + const current_path_elements: Record | undefined = all_files[current_file_path]; + if (!current_path_elements) return null; + 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 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_element_hashs: string[] = get(selected_file_ids) + .map((file_id) => get_file_from_id(file_id, files, file_path)) + .filter((element) => element !== null) + .map((folder_element) => folder_element.hash) + .filter((hash) => hash !== null); + + 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)).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); + } + +} + +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(0, path.length - 1).split('/'); + for (let i = path_parts.length; i > 1; 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