mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
add screenshot preview logic
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import {
|
import {
|
||||||
current_height,
|
current_height,
|
||||||
get_selectable_color_classes,
|
get_selectable_color_classes,
|
||||||
pinned_display_id
|
pinned_display_id
|
||||||
} from '../ts/stores/ui_behavior';
|
} from '../ts/stores/ui_behavior';
|
||||||
@@ -61,6 +61,8 @@
|
|||||||
<div class="size-[50%]">
|
<div class="size-[50%]">
|
||||||
<Pin class="size-full" />
|
<Pin class="size-full" />
|
||||||
</div>
|
</div>
|
||||||
|
{:else if display.preview_url}
|
||||||
|
<img src={display.preview_url} alt="API-Bild" class="w-full object-cover" />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- No Signal -->
|
<!-- No Signal -->
|
||||||
<VideoOff class="size-[30%]" />
|
<VideoOff class="size-[30%]" />
|
||||||
|
|||||||
@@ -1,5 +1,13 @@
|
|||||||
<script lang="ts">
|
<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 {
|
import {
|
||||||
current_height,
|
current_height,
|
||||||
get_selectable_color_classes,
|
get_selectable_color_classes,
|
||||||
@@ -21,6 +29,8 @@
|
|||||||
} from '../ts/stores/files';
|
} from '../ts/stores/files';
|
||||||
import RefreshPlay from './RefreshPlay.svelte';
|
import RefreshPlay from './RefreshPlay.svelte';
|
||||||
import { get_file_size_display_string } from '../ts/utils';
|
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 }>();
|
let { file } = $props<{ file: FolderElement }>();
|
||||||
|
|
||||||
@@ -82,11 +92,18 @@
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function open() {
|
async function open() {
|
||||||
if (is_folder) {
|
if (is_folder) {
|
||||||
change_file_path($current_file_path + file.name + '/');
|
change_file_path($current_file_path + file.name + '/');
|
||||||
} else {
|
} 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>
|
</script>
|
||||||
@@ -222,7 +239,10 @@
|
|||||||
is_selected(file.id, $selected_file_ids)
|
is_selected(file.id, $selected_file_ids)
|
||||||
)} duration-200 transition-colors"
|
)} duration-200 transition-colors"
|
||||||
></div>
|
></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)}
|
{get_file_size_display_string(file.size)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,19 +1,29 @@
|
|||||||
import type { FolderElement } from "./types";
|
import type { FolderElement } from "./types";
|
||||||
import { get_uuid } from "./utils";
|
import { get_uuid } from "./utils";
|
||||||
|
|
||||||
interface FileInfo {
|
export async function get_screenshot(ip: string) {
|
||||||
name: string;
|
const options = { method: 'PATCH' };
|
||||||
type: string;
|
return await request(ip, '/takeScreenshot', options);
|
||||||
size: string;
|
}
|
||||||
created: string;
|
|
||||||
|
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[]> {
|
export async function get_file_data(ip: string, path: string): Promise<FolderElement[]> {
|
||||||
|
interface FileInfo {
|
||||||
|
name: string;
|
||||||
|
type: string;
|
||||||
|
size: string;
|
||||||
|
created: string;
|
||||||
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
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")
|
typ=$(file -b --mime-type -- "$f")
|
||||||
size=$(stat -c '%s' -- "$f")
|
size=$(stat -c '%s' -- "$f")
|
||||||
created=$(stat -c '%w' -- "$f")
|
created=$(stat -c '%w' -- "$f")
|
||||||
@@ -38,7 +48,7 @@ done
|
|||||||
const folder_element: FolderElement = {
|
const folder_element: FolderElement = {
|
||||||
id: get_uuid(),
|
id: get_uuid(),
|
||||||
hash: JSON.stringify(response),
|
hash: JSON.stringify(response),
|
||||||
thumbnail: null,
|
thumbnail_url: null,
|
||||||
name: response_element.name.slice(2), // remove "./"
|
name: response_element.name.slice(2), // remove "./"
|
||||||
type: response_element.type,
|
type: response_element.type,
|
||||||
date_created: new Date(response_element.created),
|
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 {
|
try {
|
||||||
const url = `http://${ip}:1323/api${api_route}`;
|
const url = `http://${ip}:1323/api${api_route}?t=${Date.now()}`;
|
||||||
console.log(url)
|
console.log(url)
|
||||||
const response = await fetch(url, options);
|
const response = await fetch(url, options);
|
||||||
const data = await response.json();
|
if (!response.ok) {
|
||||||
return data;
|
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) {
|
} catch (error) {
|
||||||
console.error(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 { get, writable, type Writable } from "svelte/store";
|
||||||
import type { Display, DisplayGroup } from "../types";
|
import type { Display, DisplayGroup } from "../types";
|
||||||
import { is_selected, select, selected_display_ids } from "./select";
|
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[]>([{
|
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
|
||||||
id: get_uuid(),
|
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) {
|
function add_display(ip: string, mac: string, name: string, status: string) {
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
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;
|
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();
|
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", 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 = {
|
export type FolderElement = {
|
||||||
id?: string;
|
id?: string;
|
||||||
hash: string | null;
|
hash: string | null;
|
||||||
thumbnail: Blob | null;
|
thumbnail_url: string | null;
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
date_created: Date;
|
date_created: Date;
|
||||||
@@ -58,6 +58,8 @@ export type FolderElement = {
|
|||||||
export type Display = {
|
export type Display = {
|
||||||
id: string;
|
id: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
|
preview_url: string | null;
|
||||||
|
preview_timeout_id: number | null;
|
||||||
mac: string;
|
mac: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: 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]}`;
|
const size_string = `${value.toFixed(toFixed !== null ? toFixed : Math.max(0, 2 - Math.floor(Math.log10(value))))} ${sizes[i]}`;
|
||||||
|
|
||||||
return size_string.replace('.', ',');
|
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
|
auth: inherit
|
||||||
}
|
}
|
||||||
|
|
||||||
body:file {
|
|
||||||
file: @file(/home/fedorra-44/Downloads/10 Revenge Party und Playoff.mov) @contentType(video/quicktime)
|
|
||||||
}
|
|
||||||
|
|
||||||
settings {
|
settings {
|
||||||
encodeUrl: true
|
encodeUrl: true
|
||||||
timeout: 0
|
timeout: 0
|
||||||
|
|||||||
+3
-3
@@ -186,7 +186,7 @@ func keyboardInputRoute(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Keyboard input sent", "key", request.Key)
|
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 {
|
func uploadFileRoute(ctx echo.Context) error {
|
||||||
@@ -276,7 +276,7 @@ func openFileRoute(ctx echo.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
slog.Info("Successfully run file", "file", pathParam)
|
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 {
|
func showHTMLRoute(ctx echo.Context) error {
|
||||||
@@ -297,7 +297,7 @@ func showHTMLRoute(ctx echo.Context) error {
|
|||||||
sseConnection <- request.HTML
|
sseConnection <- request.HTML
|
||||||
|
|
||||||
slog.Info("HTML content sent to client")
|
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 {
|
func pingRoute(ctx echo.Context) error {
|
||||||
|
|||||||
Reference in New Issue
Block a user