diff --git a/control/frontend/src/components/DisplayObject.svelte b/control/frontend/src/components/DisplayObject.svelte index 67071ab..3c54969 100644 --- a/control/frontend/src/components/DisplayObject.svelte +++ b/control/frontend/src/components/DisplayObject.svelte @@ -1,7 +1,7 @@ @@ -222,7 +239,10 @@ is_selected(file.id, $selected_file_ids) )} duration-200 transition-colors" > -
+
{get_file_size_display_string(file.size)}
diff --git a/control/frontend/src/ts/api_handler.ts b/control/frontend/src/ts/api_handler.ts index db7269d..193996f 100644 --- a/control/frontend/src/ts/api_handler.ts +++ b/control/frontend/src/ts/api_handler.ts @@ -1,19 +1,29 @@ import type { FolderElement } from "./types"; import { get_uuid } from "./utils"; -interface FileInfo { - name: string; - type: string; - size: string; - created: string; +export async function get_screenshot(ip: string) { + const options = { method: 'PATCH' }; + return await request(ip, '/takeScreenshot', options); +} + +export async function open_file(ip: string, path_to_file: string) { + const options = { method: 'PATCH', headers: { 'content-type': 'application/octet-stream' } }; + const raw_response = await request(ip, `/file${path_to_file}`, options); } export async function get_file_data(ip: string, path: string): Promise { + interface FileInfo { + name: string; + type: string; + size: string; + created: string; + } + const options = { method: 'PATCH', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ - command: `cd .${path} && find . -maxdepth 1 -mindepth 1 -print0 | while IFS= read -r -d '' f; do + 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") @@ -38,7 +48,7 @@ done const folder_element: FolderElement = { id: get_uuid(), hash: JSON.stringify(response), - thumbnail: null, + thumbnail_url: null, name: response_element.name.slice(2), // remove "./" type: response_element.type, date_created: new Date(response_element.created), @@ -53,13 +63,20 @@ done -async function request(ip: string, api_route: string, options: { method: string, headers?: Record, body?: any }): Promise { +async function request(ip: string, api_route: string, options: { method: string, headers?: Record, body?: any }) { try { - const url = `http://${ip}:1323/api${api_route}`; + const url = `http://${ip}:1323/api${api_route}?t=${Date.now()}`; console.log(url) const response = await fetch(url, options); - const data = await response.json(); - return data; + if (!response.ok) { + console.error(`HTTP error! Status: ${response.status}`); + } + const contentType = response.headers.get("content-type") || ""; + if (!contentType.includes("application/json")) { + return await response.blob(); + } else { + return await response.json(); + } } catch (error) { console.error(error); } diff --git a/control/frontend/src/ts/main.ts b/control/frontend/src/ts/main.ts new file mode 100644 index 0000000..5fa754b --- /dev/null +++ b/control/frontend/src/ts/main.ts @@ -0,0 +1,4 @@ +export function on_start() { + +} + diff --git a/control/frontend/src/ts/stores/displays.ts b/control/frontend/src/ts/stores/displays.ts index e36ad9d..52f5e23 100644 --- a/control/frontend/src/ts/stores/displays.ts +++ b/control/frontend/src/ts/stores/displays.ts @@ -1,7 +1,8 @@ import { get, writable, type Writable } from "svelte/store"; import type { Display, DisplayGroup } from "../types"; import { is_selected, select, selected_display_ids } from "./select"; -import { get_uuid } from "../utils"; +import { get_uuid, image_content_hash } from "../utils"; +import { get_screenshot } from "../api_handler"; export const displays: Writable = writable([{ id: get_uuid(), @@ -11,7 +12,7 @@ export const displays: Writable = writable([{ function add_display(ip: string, mac: string, name: string, status: string) { displays.update((displays: DisplayGroup[]) => { - displays[0].data.push({ id: get_uuid(), ip, mac, name, status }); + displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status }); return displays; }); } @@ -78,6 +79,48 @@ export function remove_empty_display_groups() { }); } +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)?.ip; + if (!display_ip) return; + const new_blob = await get_screenshot(display_ip); + const display = get_display_by_id(display_id); + + 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); + console.log(old_hash, new_hash); + 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) { + displays.update((display_groups) => + display_groups.map((group) => ({ + ...group, + data: group.data.map((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 }; + }), + })) + ); + } +} + add_testing_displays(); @@ -87,5 +130,6 @@ function add_testing_displays() { // 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", "Test", "Offline") + add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "Offline"); + // add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "Online"); } \ No newline at end of file diff --git a/control/frontend/src/ts/types.ts b/control/frontend/src/ts/types.ts index 33471ad..50a7f96 100644 --- a/control/frontend/src/ts/types.ts +++ b/control/frontend/src/ts/types.ts @@ -47,7 +47,7 @@ export const supported_file_types: Record = { export type FolderElement = { id?: string; hash: string | null; - thumbnail: Blob | null; + thumbnail_url: string | null; name: string; type: string; date_created: Date; @@ -58,6 +58,8 @@ export type FolderElement = { export type Display = { id: string; ip: string; + preview_url: string | null; + preview_timeout_id: number | null; mac: string; name: string; status: string; diff --git a/control/frontend/src/ts/utils.ts b/control/frontend/src/ts/utils.ts index 22004a9..216f0d7 100644 --- a/control/frontend/src/ts/utils.ts +++ b/control/frontend/src/ts/utils.ts @@ -14,4 +14,29 @@ export function get_file_size_display_string(size: number, toFixed: number|null const size_string = `${value.toFixed(toFixed !== null ? toFixed : Math.max(0, 2 - Math.floor(Math.log10(value))))} ${sizes[i]}`; return size_string.replace('.', ','); -} \ No newline at end of file +} + + +export async function image_content_hash(blob: Blob, size = 32): Promise { + // 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 +} + diff --git a/display/requests/uploadFile.bru b/display/requests/uploadFile.bru index 57dee83..118fc6c 100644 --- a/display/requests/uploadFile.bru +++ b/display/requests/uploadFile.bru @@ -10,10 +10,6 @@ post { auth: inherit } -body:file { - file: @file(/home/fedorra-44/Downloads/10 Revenge Party und Playoff.mov) @contentType(video/quicktime) -} - settings { encodeUrl: true timeout: 0 diff --git a/display/web/main.go b/display/web/main.go index f3b5070..47c0026 100644 --- a/display/web/main.go +++ b/display/web/main.go @@ -186,7 +186,7 @@ func keyboardInputRoute(ctx echo.Context) error { } slog.Info("Keyboard input sent", "key", request.Key) - return ctx.NoContent(http.StatusOK) + return ctx.JSON(http.StatusCreated, struct{ Message string }{Message: "Success"}) } func uploadFileRoute(ctx echo.Context) error { @@ -276,7 +276,7 @@ func openFileRoute(ctx echo.Context) error { } slog.Info("Successfully run file", "file", pathParam) - return ctx.NoContent(http.StatusOK) + return ctx.JSON(http.StatusCreated, struct{ Message string }{Message: "Success"}) } func showHTMLRoute(ctx echo.Context) error { @@ -297,7 +297,7 @@ func showHTMLRoute(ctx echo.Context) error { sseConnection <- request.HTML slog.Info("HTML content sent to client") - return ctx.NoContent(http.StatusOK) + return ctx.JSON(http.StatusCreated, struct{ Message string }{Message: "Success"}) } func pingRoute(ctx echo.Context) error {