add file thumbnails

This commit is contained in:
E44
2025-11-10 12:45:46 +01:00
parent 706e8350af
commit a247f59dd5
11 changed files with 120 additions and 11 deletions
+7
View File
@@ -9,6 +9,7 @@
"version": "0.0.1", "version": "0.0.1",
"dependencies": { "dependencies": {
"@thisux/sveltednd": "^0.0.20", "@thisux/sveltednd": "^0.0.20",
"dexie": "^4.2.1",
"lucide-svelte": "^0.545.0", "lucide-svelte": "^0.545.0",
"svelte-splitpanes": "^8.0.9" "svelte-splitpanes": "^8.0.9"
}, },
@@ -2136,6 +2137,12 @@
"dev": true, "dev": true,
"license": "MIT" "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": { "node_modules/enhanced-resolve": {
"version": "5.18.3", "version": "5.18.3",
"resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz",
+1
View File
@@ -38,6 +38,7 @@
}, },
"dependencies": { "dependencies": {
"@thisux/sveltednd": "^0.0.20", "@thisux/sveltednd": "^0.0.20",
"dexie": "^4.2.1",
"lucide-svelte": "^0.545.0", "lucide-svelte": "^0.545.0",
"svelte-splitpanes": "^8.0.9" "svelte-splitpanes": "^8.0.9"
} }
@@ -36,9 +36,23 @@
run_on_all_selected_displays, run_on_all_selected_displays,
update_screenshot update_screenshot
} from '../ts/stores/displays'; } 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 { 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'; const is_folder = file.type === 'inode/directory';
function get_file_type(file: FolderElement): SupportedFileType | null { 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" })} rounded-r-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group w-full min-w-0"
> >
<div class="flex flex-row gap-2 min-w-0 w-full"> <div class="flex flex-row gap-2 min-w-0 w-full">
<div class="aspect-square rounded-md flex justify-center items-center p-2"> <div
class="aspect-square rounded-md flex justify-center items-center"
>
{#if is_folder} {#if is_folder}
<Folder class="size-full" /> <Folder class="size-full p-2" />
{:else if file.thumbnail} {:else if thumbnail_url}
<div></div> <img
src={thumbnail_url}
alt="file_thumbnail"
class="object-contain size-full select-none block"
draggable="false"
/>
{:else if get_file_type(file)?.icon} {:else if get_file_type(file)?.icon}
{@const Icon = get_file_type(file)?.icon} {@const Icon = get_file_type(file)?.icon}
<Icon class="size-full" /> <Icon class="size-full p-2" />
{:else} {:else}
<FileIcon /> <FileIcon class="size-full p-2" />
{/if} {/if}
</div> </div>
<div class="content-center truncate select-none w-full" title={file.name}> <div class="content-center truncate select-none w-full" title={file.name}>
+1
View File
@@ -201,6 +201,7 @@ module.exports = {
'group-active:border-stone-900', 'group-active:border-stone-900',
'group-active:border-stone-950', 'group-active:border-stone-950',
'h-10',
'h-15', 'h-15',
'h-20', 'h-20',
'h-25', 'h-25',
+7 -3
View File
@@ -2,7 +2,7 @@ import { notifications } from "./stores/notification";
import { to_display_status, type DisplayStatus, type FolderElement, type TreeElement } from "./types"; import { to_display_status, type DisplayStatus, type FolderElement, type TreeElement } from "./types";
import { get_uuid } from "./utils"; import { get_uuid } from "./utils";
export async function get_screenshot(ip: string) { export async function get_screenshot(ip: string): Promise<Blob|null> {
const options = { method: 'PATCH' }; const options = { method: 'PATCH' };
return await request_display(ip, '/takeScreenshot', options); return await request_display(ip, '/takeScreenshot', options);
} }
@@ -63,8 +63,7 @@ done
if (response_element.name.charAt(2) !== '.') { if (response_element.name.charAt(2) !== '.') {
const folder_element: FolderElement = { const folder_element: FolderElement = {
id: get_uuid(), id: get_uuid(),
hash: JSON.stringify(response), hash: JSON.stringify(response_element),
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),
@@ -111,6 +110,11 @@ export async function ping_ip(ip: string): Promise<DisplayStatus> {
return raw_response.status ? to_display_status(raw_response.status) : null; return raw_response.status ? to_display_status(raw_response.status) : null;
} }
export async function get_thumbnail_blob(ip: string, path_to_file: string): Promise<Blob|null> {
const raw_response = await request_display(ip, `/file/preview${path_to_file}`, { method: 'GET' });
return raw_response;
}
@@ -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<ThumbnailBlobDBEntry, "hash">; // (Type, PrimaryKey)
constructor() {
super("FileDatabase");
this.version(1).stores({
thumbnail_blobs: "++hash, blob" // primary key
});
}
}
export const db = new FileDatabase();
+3 -1
View File
@@ -3,10 +3,12 @@ import { displays, run_on_all_selected_displays, update_displays_with_map, updat
import { ping_ip } from "./api_handler"; import { ping_ip } from "./api_handler";
import type { Display } from "./types"; import type { Display } from "./types";
import { change_file_path, update_folder_elements_recursively } from "./stores/files"; 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; const update_display_status_interval_seconds = 20;
export async function on_start() { export async function on_start() {
await db.thumbnail_blobs.clear();
await update_all_display_status(); await update_all_display_status();
await setInterval(update_all_display_status, update_display_status_interval_seconds * 1000); 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) { async function on_display_start(display: Display) {
await update_folder_elements_recursively(display, '/'); await update_folder_elements_recursively(display, '/');
await update_screenshot(display.id); await update_screenshot(display.id);
} }
@@ -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; const display_ip = get_display_by_id(display_id, get(displays))?.ip;
if (!display_ip) return; if (!display_ip) return;
const new_blob = await get_screenshot(display_ip); 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)); const display = get_display_by_id(display_id, get(displays));
let update_needed = check_type === "first_check"; let update_needed = check_type === "first_check";
+11
View File
@@ -5,6 +5,7 @@ import { selected_file_ids } from "./select";
import { get_file_data, get_file_tree_data } from "../api_handler"; import { get_file_data, get_file_tree_data } from "../api_handler";
import { notifications } from "./notification"; import { notifications } from "./notification";
import { CirclePoundSterling } from "lucide-svelte"; import { CirclePoundSterling } from "lucide-svelte";
import { deactivate_old_thumbnail_urls, generate_thumbnail } from "./thumbnails";
export const all_files: Writable<Record<string, Record<string, FolderElement[]>>> = writable<Record<string, Record<string, FolderElement[]>>>({}); export const all_files: Writable<Record<string, Record<string, FolderElement[]>>> = writable<Record<string, Record<string, FolderElement[]>>>({});
// { // {
@@ -30,6 +31,8 @@ export async function change_file_path(new_path: string) {
return []; return [];
}) })
deactivate_old_thumbnail_urls();
for (const display_group of get(displays)) { for (const display_group of get(displays)) {
for (const display of display_group.data) { for (const display of display_group.data) {
const changed_paths = await get_changed_directory_paths(display, new_path); 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 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); 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); files[file_path][display.id].push(...diff.new);
return remove_folder_elements_recursively(files, display, diff.deleted, file_path); 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) { // export function updates_files_on_display(display_id: string, new_folder_elements: FolderElement[], file_path: string) {
// all_files.update((files) => { // all_files.update((files) => {
// if (!files.hasOwnProperty(file_path)) { // if (!files.hasOwnProperty(file_path)) {
@@ -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<string[]> = writable<string[]>([]);
export async function generate_thumbnail(display_ip: string, path: string, folder_element: FolderElement): Promise<void> {
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<string | null> {
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 []; })
}
-1
View File
@@ -48,7 +48,6 @@ 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_url: string | null;
name: string; name: string;
type: string; type: string;
date_created: Date; date_created: Date;