mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
add file thumbnails
This commit is contained in:
Generated
+7
@@ -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",
|
||||||
|
|||||||
@@ -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}>
|
||||||
|
|||||||
@@ -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',
|
||||||
|
|||||||
@@ -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,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";
|
||||||
|
|||||||
@@ -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 []; })
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user