add screenshot preview logic

This commit is contained in:
E44
2025-11-01 16:28:22 +01:00
parent 67fe1a4e83
commit 7420e15892
9 changed files with 138 additions and 28 deletions
@@ -1,7 +1,7 @@
<script lang="ts">
import Button from './Button.svelte';
import {
current_height,
current_height,
get_selectable_color_classes,
pinned_display_id
} from '../ts/stores/ui_behavior';
@@ -61,6 +61,8 @@
<div class="size-[50%]">
<Pin class="size-full" />
</div>
{:else if display.preview_url}
<img src={display.preview_url} alt="API-Bild" class="w-full object-cover" />
{:else}
<!-- No Signal -->
<VideoOff class="size-[30%]" />
@@ -1,5 +1,13 @@
<script lang="ts">
import { ArrowRight, Ban, FileIcon, Folder, Play, RefreshCcwDot, TriangleAlert } from 'lucide-svelte';
import {
ArrowRight,
Ban,
FileIcon,
Folder,
Play,
RefreshCcwDot,
TriangleAlert
} from 'lucide-svelte';
import {
current_height,
get_selectable_color_classes,
@@ -21,6 +29,8 @@
} from '../ts/stores/files';
import RefreshPlay from './RefreshPlay.svelte';
import { get_file_size_display_string } from '../ts/utils';
import { open_file } from '../ts/api_handler';
import { get_display_by_id, update_screenshot } from '../ts/stores/displays';
let { file } = $props<{ file: FolderElement }>();
@@ -82,11 +92,18 @@
e.stopPropagation();
}
function open() {
async function open() {
if (is_folder) {
change_file_path($current_file_path + file.name + '/');
} else {
// TODO
const path_to_file = $current_file_path + file.name;
for (const display_id of $selected_display_ids) {
const ip = get_display_by_id(display_id)?.ip ?? null;
if (ip) {
await open_file(ip, path_to_file);
await update_screenshot(display_id);
}
}
}
}
</script>
@@ -222,7 +239,10 @@
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors"
></div>
<div class="w-12 content-center text-center select-none text-xs whitespace-nowrap" title={get_file_size_display_string(file.size, 3)}>
<div
class="w-12 content-center text-center select-none text-xs whitespace-nowrap"
title={get_file_size_display_string(file.size, 3)}
>
{get_file_size_display_string(file.size)}
</div>
</div>
+28 -11
View File
@@ -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<FolderElement[]> {
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<string, string>, body?: any }): Promise<any | null> {
async function request(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, 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);
}
+4
View File
@@ -0,0 +1,4 @@
export function on_start() {
}
+47 -3
View File
@@ -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<DisplayGroup[]> = writable<DisplayGroup[]>([{
id: get_uuid(),
@@ -11,7 +12,7 @@ export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
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");
}
+3 -1
View File
@@ -47,7 +47,7 @@ export const supported_file_types: Record<string, SupportedFileType> = {
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;
+26 -1
View File
@@ -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('.', ',');
}
}
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
}
-4
View File
@@ -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
+3 -3
View File
@@ -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 {