import { get, writable, type Writable } from 'svelte/store'; import { get_file_primary_key, type Display, type FileOnDisplay, type Inode, type TreeElement } from '../types'; import { get_display_by_id } from './displays'; import { is_selected, select, selected_display_ids, selected_file_ids } from './select'; import { create_folders, get_file_data, get_file_tree_data } from '../api_handler'; import { deactivate_old_thumbnail_urls, generate_thumbnail } from './thumbnails'; import { db } from '../database'; export const current_file_path: Writable = writable('/'); export async function change_file_path(new_path: string) { current_file_path.update(() => { return new_path; }); selected_file_ids.update(() => { return []; }); deactivate_old_thumbnail_urls(); const displays = await db.displays.toArray(); for (const display of displays) { const changed_paths = await get_changed_directory_paths(display, new_path); if (!changed_paths) continue; console.debug('Update file system from', display.name, ':', changed_paths); for (const path of changed_paths) { await update_folder_elements_recursively(display, path); } } } export async function delete_and_deselect_unique_files_from_display(display_id: string) { const files_on_display = await db.files_on_display .where('display_id') .equals(display_id) .toArray(); for (const file of files_on_display) { await remove_file_from_display_recusively(display_id, file.file_primary_key); } await remove_all_files_without_display(); } export async function remove_file_from_display_recusively(display_id: string, file_primary_key: string) { const found = await db.files_on_display.get([display_id, file_primary_key]); if (!found) return; select(selected_file_ids, file_primary_key, 'deselect'); await db.files_on_display.delete([display_id, file_primary_key]); const inode_element = await get_file_by_id(found.file_primary_key); if (!inode_element) return; if (inode_element.type === 'inode/directory') { const path_inside_folder = inode_element.path + inode_element.name + `/`; const primary_file_keys_in_folder = ( await db.files.where('path').equals(path_inside_folder).toArray() ).map((e) => get_file_primary_key(e)); const primary_file_keys_in_folder_on_display = ( await db.files_on_display .where('file_primary_key') .anyOf(primary_file_keys_in_folder) .filter((file) => file.display_id === display_id) .toArray() ).map((e) => e.file_primary_key); for (const file_key of primary_file_keys_in_folder_on_display) { await remove_file_from_display_recusively(display_id, file_key); } } } export async function remove_all_files_without_display() { const existing_file_id_strings: string[] = (await db.files_on_display .orderBy('file_primary_key') .uniqueKeys()) as string[]; const existing_file_id_objects: [string, string, number, string][] = existing_file_id_strings.map( (e) => JSON.parse(e) as [string, string, number, string] ); await db.files.where('[path+name+size+type]').noneOf(existing_file_id_objects).delete(); } export async function update_current_folder_on_selected_displays() { selected_file_ids.update(() => { return []; }); const current_path = get(current_file_path); for (const display of await db.displays.where('id').anyOf(get(selected_display_ids)).toArray()) { await update_folder_elements_recursively(display, current_path); } } export async function get_missing_colliding_display_ids( file: Inode, selected_display_ids: string[] ): Promise<{ missing: string[]; colliding: string[] }> { const missing: string[] = await get_display_ids_where_file_is_missing(file, selected_display_ids); const colliding: string[] = []; const colliding_files = await db.files .where('[path+name]') .equals([file.path, file.name]) .filter((e) => e.size !== file.size || e.type !== file.type) .toArray(); for (const colliding_file of colliding_files) { colliding.push( ...(await get_display_ids_where_file_is_missing(colliding_file, selected_display_ids)) ); } return { missing, colliding }; } async function get_display_ids_where_file_is_missing( file: Inode, selected_display_ids: string[] ): Promise { const file_primary_key = get_file_primary_key(file); const files_on_selected_displays = await db.files_on_display .where('file_primary_key') .equals(file_primary_key) .filter((e) => selected_display_ids.includes(e.display_id)) .toArray(); return selected_display_ids.filter( (id) => !files_on_selected_displays.some((item) => item.display_id === id) ); } export async function get_displays_where_path_exists( path: string, selected_display_ids: string[], invert: boolean ): Promise { if (path === '/') return []; const last_path_part = path .slice(0, path.length - 1) .split('/') .at(-1) ?? ''; const path_without_last_part = path.slice(0, path.length - (last_path_part.length + 1)); const folders_of_current_path = await db.files .where('name') .equals(last_path_part) .filter((inode) => inode.path === path_without_last_part && inode.type === 'inode/directory') .first(); if (!folders_of_current_path) return await db.displays.where('id').anyOf(selected_display_ids).toArray(); const folder_primary_key = get_file_primary_key(folders_of_current_path); const display_ids_with_folder = ( await db.files_on_display.where('file_primary_key').equals(folder_primary_key).toArray() ).map((fod) => fod.display_id); const display_ids = selected_display_ids.filter((display_id) => { if (invert) { return !display_ids_with_folder.includes(display_id); } else { return display_ids_with_folder.includes(display_id); } }); return (await db.displays.bulkGet(display_ids)).filter((e) => e !== undefined); } async function get_changed_directory_paths( display: Display, file_path: string ): Promise { const current_folder = await get_file_tree_data(display.ip, file_path); const directory_strings = await get_recursive_changed_directory_paths( display, file_path, current_folder ); if (directory_strings.size === 0) return null; const directory_strings_array = [...directory_strings]; return directory_strings_array.filter( (e) => !directory_strings_array.some((f) => f !== e && f.startsWith(e)) ); } async function get_recursive_changed_directory_paths( display: Display, current_file_path: string, current_folder_elements: TreeElement[] | null ): Promise> { const files_folder: Inode[] = await db.files.where('path').equals(current_file_path).toArray(); if ( (!files_folder || files_folder.length === 0) && (!current_folder_elements || current_folder_elements.length === 0) ) { return new Set([]); // no data -> no update needed } else if ( !files_folder || !current_folder_elements || current_folder_elements.length !== files_folder.length ) { return new Set([current_file_path]); // existing data does not match new data -> update } const has_changed: Set = new Set(); for (const tree_folder_element of current_folder_elements) { const folder_element = files_folder.find((e) => e.name === tree_folder_element.name); if ( !folder_element || (tree_folder_element.type !== 'directory' && folder_element.size !== tree_folder_element.size) ) { return new Set([current_file_path]); } if (tree_folder_element.type === 'directory' && tree_folder_element.contents) { const new_file_path = current_file_path + tree_folder_element.name + '/'; for (const string of await get_recursive_changed_directory_paths( display, new_file_path, tree_folder_element.contents )) { has_changed.add(string); } } } return has_changed; } export async function update_folder_elements_recursively( display: Display, file_path: string = '/' ): Promise { const new_folder_elements = await get_file_data(display.ip, file_path); if (!new_folder_elements) return; const existing_files_on_display_in_path: FileOnDisplay[] = await db.files_on_display .where('display_id') .equals(display.id) .toArray(); const existing_file_keys_on_display_in_path: [string, string, number, string][] = existing_files_on_display_in_path.map( (e) => JSON.parse(e.file_primary_key) as [string, string, number, string] ); const existing_files: Inode[] = await db.files .where('[path+name+size+type]') .anyOf(existing_file_keys_on_display_in_path) .filter((e) => e.path === file_path) .toArray(); const existing_files_with_loading_state: { folder_element: Inode; is_loading: boolean }[] = existing_files.map((folder_element) => ({ folder_element, is_loading: !!existing_files_on_display_in_path.find( (e) => e.file_primary_key === get_file_primary_key(folder_element) )?.loading_data })); const diff = get_folder_elements_difference( existing_files_with_loading_state, new_folder_elements ); if (diff.new.length > 0) { // Add new Folder-Elements for (const new_element of diff.new) { await db.files.put(new_element.folder_element); const file_on_display: FileOnDisplay = { display_id: display.id, file_primary_key: get_file_primary_key(new_element.folder_element), loading_data: null, date_created: new_element.date_created }; await db.files_on_display.put(file_on_display); if (new_element.folder_element.type === 'inode/directory') { await update_folder_elements_recursively( display, file_path + new_element.folder_element.name + '/' ); } } // Generate Thumbnails: setTimeout(async () => { for (const new_element of diff.new) { await generate_thumbnail(display.ip, file_path, new_element.folder_element); } }, 0); } if (diff.deleted.length > 0) { // Remove old Folder-Elements for (const old_element of diff.deleted) { remove_file_from_display_recusively(display.id, get_file_primary_key(old_element)); } await remove_all_files_without_display(); } } function get_folder_elements_difference( old_elements: { folder_element: Inode; is_loading: boolean }[], new_elements: { folder_element: Inode; date_created: Date }[] ): { deleted: Inode[]; new: { folder_element: Inode; date_created: Date }[] } { const old_keys = new Set(old_elements.map((e) => get_file_primary_key(e.folder_element))); const new_keys = new Set(new_elements.map((e) => get_file_primary_key(e.folder_element))); const only_in_old = old_elements .filter((e) => !new_keys.has(get_file_primary_key(e.folder_element)) && !e.is_loading) .map((e) => e.folder_element); const only_in_new = new_elements.filter( (e) => !old_keys.has(get_file_primary_key(e.folder_element)) ); return { deleted: only_in_old, new: only_in_new }; } export async function get_current_folder_elements( current_file_path: string, selected_display_ids: string[] ): Promise { const existing_file_keys_on_selected_displays: [string, string, number, string][] = ( await db.files_on_display.where('display_id').anyOf(selected_display_ids).toArray() ).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]); const existing_files_on_selected_displays_in_path: Inode[] = await db.files .where('[path+name+size+type]') .anyOf(existing_file_keys_on_selected_displays) .filter((e) => e.path === current_file_path) .toArray(); return sort_files(existing_files_on_selected_displays_in_path); } function sort_files(files: Inode[]) { files.sort((a, b) => { const isDirA = a.type === 'inode/directory'; const isDirB = b.type === 'inode/directory'; // Ordner zuerst if (isDirA && !isDirB) return -1; if (!isDirA && isDirB) return 1; // Danach alphabetisch nach name (case-insensitive) const nameCompare = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' }); if (nameCompare !== 0) return nameCompare; return -1; }); return files; } export async function get_file_by_id( file_primary_key: string, only_from_selected_displays: boolean = false ): Promise { const file = (await db.files.get(JSON.parse(file_primary_key))) ?? null; if (!file || !only_from_selected_displays) { return file; } else { const exist_on_selected_display = !!(await db.files_on_display .where('file_primary_key') .equals(file_primary_key) .filter((e) => get(selected_display_ids).includes(e.display_id)) .first()); return exist_on_selected_display ? file : null; } } export async function run_for_selected_files_on_selected_displays( action: (ip: string, file_names: string[]) => Promise ): Promise { for (const display_id of get(selected_display_ids)) { const file_key_strings_on_display: string[] = ( await db.files_on_display.where('display_id').equals(display_id).toArray() ).map((e) => e.file_primary_key); const selected_file_keys_on_display: [string, string, number, string][] = file_key_strings_on_display .filter((primary_key_string) => is_selected(primary_key_string, get(selected_file_ids))) .map( (primary_key_string) => JSON.parse(primary_key_string) as [string, string, number, string] ); if (selected_file_keys_on_display.length === 0) continue; const selected_file_names_on_display: string[] = ( await db.files.where('[path+name+size+type]').anyOf(selected_file_keys_on_display).toArray() ).map((e) => e.name); const display = await get_display_by_id(display_id); if (!display) continue; await action(display.ip, selected_file_names_on_display); } } export async function create_folder_on_all_selected_displays( folder_name: string, path: string, selected_display_ids: string[] ): Promise { const path_with_folder_name = (path += folder_name + '/'); await create_path_on_all_selected_displays(path_with_folder_name, selected_display_ids); } export async function create_path_on_all_selected_displays( path: string, selected_display_ids: string[] ) { const path_parts = path .slice(1, path.length - 1) .split('/') .filter((e) => e.length !== 0); let remaining_display_ids = [...selected_display_ids]; const getDisplaysForPath = async (currentPath: string): Promise => { if (currentPath === '/') { const displays = await db.displays.bulkGet(remaining_display_ids); return displays.filter((d): d is Display => !!d); } return get_displays_where_path_exists(currentPath, remaining_display_ids, false); }; for (let depth = path_parts.length; depth >= 0 && remaining_display_ids.length; depth--) { const currentPath = depth === 0 ? '/' : `/${path_parts.slice(0, depth).join('/')}/`; const displays = await getDisplaysForPath(currentPath); if (!displays.length) continue; for (const display of displays) { await create_folders(display.ip, currentPath, path_parts.slice(depth)); remaining_display_ids = remaining_display_ids.filter((id) => id !== display.id); } } } export async function get_date_mapping(file_primary_key: string): Promise> { const same_file_on_displays: FileOnDisplay[] = await db.files_on_display .where('file_primary_key') .equals(file_primary_key) .toArray(); const displays_with_that_file: Display[] = await db.displays .where('id') .anyOf(same_file_on_displays.map((e) => e.display_id)) .toArray(); const out: Record = {}; for (const current_file_on_display of same_file_on_displays) { const current_name = displays_with_that_file.find( (e) => e.id === current_file_on_display.display_id )?.name; if (!current_name) continue; const current_date = current_file_on_display.date_created; out[current_name] = current_date; } return out; }