diff --git a/control/frontend/package-lock.json b/control/frontend/package-lock.json index 61c92d0..738dd07 100644 --- a/control/frontend/package-lock.json +++ b/control/frontend/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "dependencies": { "@thisux/sveltednd": "^0.0.20", + "dexie": "^4.2.1", "lucide-svelte": "^0.545.0", "svelte-splitpanes": "^8.0.9" }, @@ -2136,6 +2137,12 @@ "dev": true, "license": "MIT" }, + "node_modules/dexie": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/dexie/-/dexie-4.2.1.tgz", + "integrity": "sha512-Ckej0NS6jxQ4Po3OrSQBFddayRhTCic2DoCAG5zacOfOVB9P2Q5Xc5uL/nVa7ZVs+HdMnvUPzLFCB/JwpB6Csg==", + "license": "Apache-2.0" + }, "node_modules/enhanced-resolve": { "version": "5.18.3", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", diff --git a/control/frontend/package.json b/control/frontend/package.json index 198dafd..7b7be79 100644 --- a/control/frontend/package.json +++ b/control/frontend/package.json @@ -38,6 +38,7 @@ }, "dependencies": { "@thisux/sveltednd": "^0.0.20", + "dexie": "^4.2.1", "lucide-svelte": "^0.545.0", "svelte-splitpanes": "^8.0.9" } diff --git a/control/frontend/src/components/FolderElementObject.svelte b/control/frontend/src/components/FolderElementObject.svelte index d9c4d6d..a2d6d69 100644 --- a/control/frontend/src/components/FolderElementObject.svelte +++ b/control/frontend/src/components/FolderElementObject.svelte @@ -36,9 +36,23 @@ run_on_all_selected_displays, update_screenshot } from '../ts/stores/displays'; + import { get_thumbnail_url } from '../ts/stores/thumbnails'; + import { db } from '../ts/indexdb/file_thumbnails.db'; + import { onDestroy, onMount } from 'svelte'; + import { liveQuery } from 'dexie'; let { file } = $props<{ file: FolderElement }>(); + let thumbnail_url: string | null = $state(null); + // Update thumbnail_url automatically if data is available + const subscription = liveQuery(() => db.thumbnail_blobs.get(file.hash)).subscribe({ + next: async () => { + thumbnail_url = await get_thumbnail_url(file.hash); + }, + error: (err) => console.error('Dexie subscription error:', err) + }); + onDestroy(() => subscription.unsubscribe()); + const is_folder = file.type === 'inode/directory'; function get_file_type(file: FolderElement): SupportedFileType | null { @@ -168,16 +182,23 @@ })} rounded-r-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group w-full min-w-0" >
-
+
{#if is_folder} - - {:else if file.thumbnail} -
+ + {:else if thumbnail_url} + file_thumbnail {:else if get_file_type(file)?.icon} {@const Icon = get_file_type(file)?.icon} - + {:else} - + {/if}
diff --git a/control/frontend/src/tailwind.config.js b/control/frontend/src/tailwind.config.js index 22263da..717cdf2 100644 --- a/control/frontend/src/tailwind.config.js +++ b/control/frontend/src/tailwind.config.js @@ -201,6 +201,7 @@ module.exports = { 'group-active:border-stone-900', 'group-active:border-stone-950', + 'h-10', 'h-15', 'h-20', 'h-25', diff --git a/control/frontend/src/ts/api_handler.ts b/control/frontend/src/ts/api_handler.ts index 45f514b..3b865bd 100644 --- a/control/frontend/src/ts/api_handler.ts +++ b/control/frontend/src/ts/api_handler.ts @@ -2,7 +2,7 @@ import { notifications } from "./stores/notification"; import { to_display_status, type DisplayStatus, type FolderElement, type TreeElement } from "./types"; import { get_uuid } from "./utils"; -export async function get_screenshot(ip: string) { +export async function get_screenshot(ip: string): Promise { const options = { method: 'PATCH' }; return await request_display(ip, '/takeScreenshot', options); } @@ -63,8 +63,7 @@ done if (response_element.name.charAt(2) !== '.') { const folder_element: FolderElement = { id: get_uuid(), - hash: JSON.stringify(response), - thumbnail_url: null, + hash: JSON.stringify(response_element), name: response_element.name.slice(2), // remove "./" type: response_element.type, date_created: new Date(response_element.created), @@ -111,6 +110,11 @@ export async function ping_ip(ip: string): Promise { return raw_response.status ? to_display_status(raw_response.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' }); + return raw_response; +} + diff --git a/control/frontend/src/ts/indexdb/file_thumbnails.db.ts b/control/frontend/src/ts/indexdb/file_thumbnails.db.ts new file mode 100644 index 0000000..d1d0771 --- /dev/null +++ b/control/frontend/src/ts/indexdb/file_thumbnails.db.ts @@ -0,0 +1,20 @@ +import Dexie, { type EntityTable } from "dexie"; + +export interface ThumbnailBlobDBEntry { + hash: string; + blob: Blob; +} + +export class FileDatabase extends Dexie { + thumbnail_blobs!: EntityTable; // (Type, PrimaryKey) + + constructor() { + super("FileDatabase"); + + this.version(1).stores({ + thumbnail_blobs: "++hash, blob" // primary key + }); + } +} + +export const db = new FileDatabase(); diff --git a/control/frontend/src/ts/main.ts b/control/frontend/src/ts/main.ts index 0193427..757f1ca 100644 --- a/control/frontend/src/ts/main.ts +++ b/control/frontend/src/ts/main.ts @@ -3,10 +3,12 @@ import { displays, run_on_all_selected_displays, update_displays_with_map, updat import { ping_ip } from "./api_handler"; import type { Display } from "./types"; import { change_file_path, update_folder_elements_recursively } from "./stores/files"; +import { db } from "./indexdb/file_thumbnails.db"; const update_display_status_interval_seconds = 20; export async function on_start() { + await db.thumbnail_blobs.clear(); await update_all_display_status(); await setInterval(update_all_display_status, update_display_status_interval_seconds * 1000); @@ -28,5 +30,5 @@ async function update_all_display_status() { async function on_display_start(display: Display) { await update_folder_elements_recursively(display, '/'); - await update_screenshot(display.id); + await update_screenshot(display.id); } \ No newline at end of file diff --git a/control/frontend/src/ts/stores/displays.ts b/control/frontend/src/ts/stores/displays.ts index bc0e30c..9422cdb 100644 --- a/control/frontend/src/ts/stores/displays.ts +++ b/control/frontend/src/ts/stores/displays.ts @@ -109,6 +109,13 @@ export async function update_screenshot(display_id: string, check_type: "first_c const display_ip = get_display_by_id(display_id, get(displays))?.ip; if (!display_ip) return; const new_blob = await get_screenshot(display_ip); + if (!new_blob) { + update_displays_with_map((display: Display) => { + if (display.id !== display_id) return display; + return { ...display, preview_url: null, preview_timeout_id: null }; + }) + return; + } const display = get_display_by_id(display_id, get(displays)); let update_needed = check_type === "first_check"; diff --git a/control/frontend/src/ts/stores/files.ts b/control/frontend/src/ts/stores/files.ts index 9350369..01bb1b0 100644 --- a/control/frontend/src/ts/stores/files.ts +++ b/control/frontend/src/ts/stores/files.ts @@ -5,6 +5,7 @@ import { 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"; +import { deactivate_old_thumbnail_urls, generate_thumbnail } from "./thumbnails"; export const all_files: Writable>> = writable>>({}); // { @@ -30,6 +31,8 @@ export async function change_file_path(new_path: string) { return []; }) + deactivate_old_thumbnail_urls(); + for (const display_group of get(displays)) { for (const display of display_group.data) { const changed_paths = await get_changed_directory_paths(display, new_path); @@ -111,6 +114,13 @@ export async function update_folder_elements_recursively(display: Display, file_ const existing_folder_elements = files[file_path].hasOwnProperty(display.id) ? files[file_path][display.id] : []; const diff = get_folder_elements_difference(existing_folder_elements, new_folder_elements); + // Generate Thumbnails: + setTimeout(async () => { + for (const folder_element of diff.new) { + await generate_thumbnail(display.ip, file_path, folder_element); + } + }, 0) + files[file_path][display.id].push(...diff.new); return remove_folder_elements_recursively(files, display, diff.deleted, file_path); }) @@ -171,6 +181,7 @@ function get_folder_elements_difference(old_elements: FolderElement[], new_eleme + // export function updates_files_on_display(display_id: string, new_folder_elements: FolderElement[], file_path: string) { // all_files.update((files) => { // if (!files.hasOwnProperty(file_path)) { diff --git a/control/frontend/src/ts/stores/thumbnails.ts b/control/frontend/src/ts/stores/thumbnails.ts new file mode 100644 index 0000000..064d8e1 --- /dev/null +++ b/control/frontend/src/ts/stores/thumbnails.ts @@ -0,0 +1,36 @@ +import { get, writable, type Writable } from "svelte/store"; +import { get_thumbnail_blob } from "../api_handler"; +import { supported_file_types, type FolderElement } from "../types"; +import { db } from "../indexdb/file_thumbnails.db"; + +export const active_thumbnail_urls: Writable = writable([]); + +export async function generate_thumbnail(display_ip: string, path: string, folder_element: FolderElement): Promise { + if (!Object.values(supported_file_types).some(e => e.mime_type === folder_element.type) || !folder_element.hash) return; + const hash: string = folder_element.hash; + if (await db.thumbnail_blobs.get(hash)) return; + + const thumbnail_blob = await get_thumbnail_blob(display_ip, path + folder_element.name); + if (!thumbnail_blob) return; + await db.thumbnail_blobs.add({ hash: hash, blob: thumbnail_blob }); +} + +export async function get_thumbnail_url(hash: string): Promise { + if (hash === null) return null; + const thumbnail_blob = await db.thumbnail_blobs.get(hash); + if (!thumbnail_blob) return null; + const new_url = URL.createObjectURL(thumbnail_blob.blob); + active_thumbnail_urls.update((current: string[]) => { + current.push(new_url); + return current; + }) + return new_url; +} + +export function deactivate_old_thumbnail_urls() { + const current_urls = get(active_thumbnail_urls); + for (const url of current_urls) { + URL.revokeObjectURL(url); + } + active_thumbnail_urls.update(() => { return []; }) +} \ No newline at end of file diff --git a/control/frontend/src/ts/types.ts b/control/frontend/src/ts/types.ts index 197f825..5a34cab 100644 --- a/control/frontend/src/ts/types.ts +++ b/control/frontend/src/ts/types.ts @@ -48,7 +48,6 @@ export const supported_file_types: Record = { export type FolderElement = { id?: string; hash: string | null; - thumbnail_url: string | null; name: string; type: string; date_created: Date;