mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-05 16:37:09 +00:00
add screenshot preview logic
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
export function on_start() {
|
||||
|
||||
}
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user