improve folder view - test with display-api

This commit is contained in:
E44
2025-10-30 08:59:22 +01:00
parent d92d8abc77
commit 9df03b6ccb
20 changed files with 1067 additions and 197 deletions
@@ -13,6 +13,8 @@
title = '',
click_function = (e: MouseEvent) => {},
menu_options = null,
menu_class = 'right-0',
div_class = '',
children
} = $props<{
className?: string;
@@ -23,6 +25,8 @@
title?: string;
click_function?: (e: MouseEvent) => void;
menu_options?: MenuOption[] | null;
menu_class?: string;
div_class?: string,
children?: any;
}>();
@@ -126,7 +130,7 @@
}
</script>
<div class="relative min-w-0 flex" {title}>
<div class="relative min-w-0 flex {div_class}" {title}>
<button
bind:this={button_element}
class="{className} {menu_shown ? hover_bg : bg} {disabled
@@ -150,7 +154,7 @@
transition:fade={{ duration: 50 }}
class="absolute {position_bottom
? 'top-full'
: 'bottom-full'} right-0 z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
: 'bottom-full'} {menu_class} z-100 my-1.5 min-w-64 rounded-xl backdrop-blur bg-stone-600/30 border border-stone-400/10 shadow-xl/20 p-2 flex flex-col gap-2 text-stone-200 cursor-auto"
onclick={(e) => {
e.stopPropagation();
}}
@@ -175,7 +179,7 @@
{/if}
</div>
{/if}
<div class="pr-2">
<div class="truncate min-w-0" title={option.name}>
{option.name}
</div>
</button>
@@ -7,16 +7,14 @@
import {
add_empty_display_group,
all_displays_of_group_selected,
is_selected,
remove_empty_display_groups,
select,
select_all_of_group,
selected_display_ids,
set_new_display_group_data
} from '../ts/stores/displays';
import DNDGrip from './DNDGrip.svelte';
import { fade } from 'svelte/transition';
import type { DisplayGroup } from '../ts/types';
import { selected_display_ids } from '../ts/stores/select';
let { display_group } = $props<{
display_group: DisplayGroup;
@@ -1,16 +1,15 @@
<script lang="ts">
import Button from './Button.svelte';
import { is_selected, select, selected_display_ids } from '../ts/stores/displays';
import {
display_screen_height,
current_height,
get_selectable_color_classes,
pinned_display_id
} from '../ts/stores/ui_behavior';
import DNDGrip from './DNDGrip.svelte';
import { Menu, Pencil, Pin, PinOff, Trash2, VideoOff, X } from 'lucide-svelte';
import { fade } from 'svelte/transition';
import OnlineState from './OnlineState.svelte';
import type { Display } from '../ts/types';
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
let { display } = $props<{
display: Display;
@@ -19,7 +18,7 @@
let hovering_unselectable = $state(false);
function onclick(e: Event) {
select(display.id);
select(selected_display_ids, display.id);
e.stopPropagation();
}
@@ -45,7 +44,7 @@
hover: true,
active: !hovering_unselectable,
text: true
})} rounded-xl flex flex-row justify-between h-{$display_screen_height} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
})} rounded-xl flex flex-row justify-between h-{$current_height.display} transition-colors duration-100 gap-2 cursor-pointer w-full text-stone-200"
>
<div class="flex flex-row gap-4 min-w-0 flex-1">
<!-- Left Preview Screen -->
@@ -1,48 +0,0 @@
<script lang="ts">
import { CloudAlert, Folder, RefreshCcwDot, Shield } from 'lucide-svelte';
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
import Button from './Button.svelte';
let {} = $props<{}>();
</script>
<div
class="p-1 {get_selectable_color_classes(false, {
bg: true,
hover: true,
active: true,
text: true
})} h-10 rounded-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group"
>
<div class="flex flex-row gap-2 min-w-0">
<div class="aspect-square rounded-md flex justify-center items-center">
<Folder />
</div>
<div class="content-center truncate select-none">
Tolle
Dateifjisodfhsuidfhdufighdfiughdfuighdfuihguifdghiudfhguidfgdfijghdfjkghdfkjghdfkjghdfkughdfjghdfkjghdfkjghfdkghdfkjghdfkghdfkghdfkjghdfkghdfkjghdfgkhdfkghfdkjghdfkghfdkghdfkjghdfjkghdk.zip
</div>
</div>
<div class="flex flex-row gap-1 pr-1 text-stone-400 group-hover:text-stone-300 group-active:text-stone-250 duration-200 transition-colors">
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
bg="bg-transparent"
hover_bg={get_selectable_color_classes(false, {
bg: true
})}
active_bg={get_selectable_color_classes(false, {
bg: true
})}
click_function={(e) => {
e.stopPropagation();
}}
>
<RefreshCcwDot class="size-full" />
</Button>
<div class="content-center select-none text-xs whitespace-nowrap">25.09.25</div>
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
<div class="content-center select-none text-xs whitespace-nowrap">MPEG</div>
<div class="border border-stone-550 group-hover:border-stone-450 group-active:border-stone-400 duration-200 transition-colors my-1"></div>
<div class="content-center select-none text-xs whitespace-nowrap">523 MB</div>
</div>
</div>
@@ -0,0 +1,223 @@
<script lang="ts">
import { ArrowRight, Folder, Play, RefreshCcwDot, TriangleAlert } from 'lucide-svelte';
import {
current_height,
get_selectable_color_classes,
get_shifted_color
} from '../ts/stores/ui_behavior';
import Button from './Button.svelte';
import { supported_file_types, type FolderElement, type SupportedFileType } from '../ts/types';
import {
is_selected,
select,
selected_display_ids,
selected_file_ids
} from '../ts/stores/select';
import {
all_files,
change_file_path,
current_file_path,
get_display_ids_where_file_is_missing
} from '../ts/stores/files';
import RefreshPlay from './RefreshPlay.svelte';
let { file } = $props<{ file: FolderElement }>();
const is_folder = file.type === 'inode/directory';
function get_file_type(file: FolderElement): SupportedFileType | null {
for (const key of Object.keys(supported_file_types)) {
if (file.type === supported_file_types[key].mime_type) {
return supported_file_types[key];
}
}
// Fallback:
const extension = file.name.split('.').pop();
if (extension) {
if (Object.keys(supported_file_types).includes('.' + extension)) {
return supported_file_types['.' + extension];
}
}
return null;
}
function get_created_string(date_object: Date, full_string = false) {
if (full_string) {
return (
get_formated_date_string(date_object, true) + ' ' + get_formated_time_string(date_object)
);
} else if (date_object.toDateString() === new Date().toDateString()) {
return get_formated_time_string(date_object);
} else {
return get_formated_date_string(date_object);
}
}
function get_formated_time_string(date_object: Date) {
return `${date_object.getHours().toString().padStart(2, '0')}:${date_object.getMinutes().toString().padStart(2, '0')}`;
}
function get_formated_date_string(date_object: Date, full_year = false) {
return `${date_object.getDate().toString().padStart(2, '0')}.${(date_object.getMonth() + 1).toString().padStart(2, '0')}.${date_object
.getFullYear()
.toString()
.slice(full_year ? 0 : 2)}`;
}
function get_grayed_out_text_color_strings(is_selected: boolean): string {
const color = is_selected ? 'text-stone-600' : 'text-stone-400';
const factor = is_selected ? -1 : 1;
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
}
function get_grayed_out_border_color_strings(is_selected: boolean): string {
const color = is_selected ? 'border-stone-450' : 'border-stone-550';
const factor = is_selected ? 1 : 1;
return `${color} group-hover:${get_shifted_color(color, factor * 100)} group-active:${get_shifted_color(color, factor * 150)}`;
}
function onclick(e: Event) {
select(selected_file_ids, file.id);
e.stopPropagation();
}
function open() {
if (is_folder) {
change_file_path($current_file_path + file.name + '/');
} else {
// TODO
}
}
</script>
<div class="flex flex-row h-{$current_height.file} w-full">
<div class="h-{$current_height.file} aspect-square max-w-15 flex">
<Button
className="flex rounded-l-lg rounded-r-none {is_folder
? 'text-stone-450'
: 'text-stone-800'} w-full"
div_class="w-full"
bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
-50
)}
hover_bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
50
)}
active_bg={get_selectable_color_classes(
!is_folder,
{
bg: true
},
100
)}
click_function={(e) => {
open();
e.stopPropagation();
}}
>
{#if is_folder}
<ArrowRight class="size-full" strokeWidth="3" />
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
<RefreshPlay className="size-full" />
{:else}
<Play class="size-full" strokeWidth="3" />
{/if}
</Button>
</div>
<div
role="button"
tabindex="0"
onkeydown={(e) => {
if (e.key === 'Enter' || e.key === ' ') onclick(e);
}}
{onclick}
class="{get_selectable_color_classes(is_selected(file.id, $selected_file_ids), {
bg: true,
hover: true,
active: true,
text: true
})} rounded-r-lg transition-colors duration-200 gap-4 flex flex-row justify-between cursor-pointer group w-full h-full min-w-0"
>
<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">
{#if is_folder}
<Folder class="size-full" />
{:else if file.thumbnail}
<div></div>
{:else if get_file_type(file)?.icon}
{@const Icon = get_file_type(file)?.icon}
<Icon class="size-full" />
{/if}
</div>
<div class="content-center truncate select-none w-full" title={file.name}>
{file.name.includes('.') ? file.name.slice(0, file.name.lastIndexOf('.')) : file.name}
</div>
</div>
<div
class=" p-1 flex flex-row items-center gap-1 pr-1 {get_grayed_out_text_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors"
>
{#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0}
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5 text-stone-100"
bg="bg-red-500"
click_function={(e) => {
e.stopPropagation();
}}
>
<TriangleAlert class="size-full" />
</Button>
{:else if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[0].length !== 0}
<Button
className="h-8 aspect-square transition-colors duration-200 !p-1.5"
bg="bg-transparent"
hover_bg={get_selectable_color_classes(false, {
bg: true
})}
active_bg={get_selectable_color_classes(false, {
bg: true
})}
click_function={(e) => {
e.stopPropagation();
}}
>
<RefreshCcwDot class="size-full" />
</Button>
{/if}
<div
class="w-14 content-center text-center select-none text-xs whitespace-nowrap"
title={get_created_string(file.date_created, true)}
>
{get_created_string(file.date_created)}
</div>
<div
class="h-[70%] border {get_grayed_out_border_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors my-1"
></div>
<div
class="w-12 content-center text-center select-none text-xs whitespace-nowrap truncate"
title={file.type}
>
{is_folder ? 'Ordner' : (get_file_type(file)?.display_name ?? '?')}
</div>
<div
class="h-[70%] border {get_grayed_out_border_color_strings(
is_selected(file.id, $selected_file_ids)
)} duration-200 transition-colors"
></div>
<div class="w-12 content-center text-center select-none text-xs whitespace-nowrap">
{file.size}
</div>
</div>
</div>
</div>
@@ -0,0 +1,160 @@
<script lang="ts">
import { ChevronRight, House } from 'lucide-svelte';
import Button from './Button.svelte';
import { onDestroy, onMount } from 'svelte';
import type { MenuOption } from '../ts/types';
import { change_file_path, current_file_path } from '../ts/stores/files';
import { fade } from 'svelte/transition';
import { flip } from 'svelte/animate';
import { cubicOut } from 'svelte/easing';
let { bg = 'bg-stone-700' } = $props<{
bg?: string;
}>();
let outside_container: HTMLDivElement;
let inside_container: HTMLDivElement;
let resize_observer: ResizeObserver;
let w10_div: HTMLDivElement;
let cut_folders: number = $state(0);
// let folders_length: number = $state(get_folders($current_file_path).length);
// let all_folders_length: number = $state(get_folders($current_file_path).length);
function get_folders(path: string): string[] {
path = path.slice(1); // Cut first '/'
if (path === '') return [];
if (path.endsWith('/')) {
path = path.slice(0, path.length - 1);
}
return path.split('/');
}
function get_sliced_folders(path: string, cut_folders: number): string[] {
const folders = get_folders(path);
let sliced_folders: string[] = [];
if (cut_folders !== get_folders($current_file_path).length)
sliced_folders = folders.slice(cut_folders);
return sliced_folders;
}
function get_hidden_folders(path: string, cut_folders: number): string[] {
const folders = get_folders(path);
let hidden_folders: string[] = [];
if (cut_folders !== get_folders($current_file_path).length)
hidden_folders = folders.slice(0, cut_folders);
return hidden_folders;
}
function recalc() {
if (!outside_container || !inside_container || !w10_div) return;
const first_shrink = cut_folders === 0;
const second_last_grow = cut_folders === 2;
const difference = outside_container.offsetWidth - inside_container.offsetWidth;
const w10_px = parseFloat(getComputedStyle(w10_div).width);
if ((!first_shrink && difference < 2 * w10_px) || (first_shrink && difference < w10_px)) {
if (cut_folders < get_folders($current_file_path).length) {
cut_folders += first_shrink ? 2 : 1;
}
} else if (
(!second_last_grow && difference > 6 * w10_px) ||
(second_last_grow && difference > 7 * w10_px)
) {
if (cut_folders >= 1) {
if (second_last_grow) {
cut_folders -= 2;
} else {
cut_folders -= 1;
}
}
}
}
function get_hidden_menu_options(hidden_folders: string[], path: string): MenuOption[] {
const out: MenuOption[] = [];
for (let i = 0; i < hidden_folders.length; i++) {
out.push({
name: '  '.repeat(i) + hidden_folders[i],
class: 'truncate max-w-80',
on_select: () => {
open_path(i + 1, path);
}
});
}
return out;
}
function open_path(index_of_all_folders: number, path: string) {
let new_path = '/';
const all_folders = get_folders(path);
for (let i = 0; i < index_of_all_folders; i++) {
new_path += all_folders[i] + '/';
}
change_file_path(new_path);
}
onMount(() => {
resize_observer = new ResizeObserver(() => recalc());
resize_observer.observe(outside_container);
resize_observer.observe(inside_container);
// initial
setTimeout(recalc, 0);
});
onDestroy(() => resize_observer?.disconnect());
</script>
<div class="{bg} rounded-xl flex" bind:this={outside_container}>
<div class="flex flex-row">
<div class="flex flex-row">
<div bind:this={w10_div} class="flex">
<Button
className="py-1 shrink-0 grow-0 w-10"
{bg}
click_function={(e) => {
open_path(0, $current_file_path);
}}
>
<House class="size-full" />
</Button>
</div>
{#if cut_folders !== 0}
<Button
className="pl-0 py-1 grow"
{bg}
menu_options={get_hidden_menu_options(
get_hidden_folders($current_file_path, cut_folders),
$current_file_path
)}
menu_class="left-0"
>
<ChevronRight class="shrink-0 text-stone-500 h-full" />
<span class="font-bold" title="Weiteren Pfad zeigen"> ... </span>
</Button>
{/if}
</div>
<div class="flex flex-row" bind:this={inside_container}>
{#each get_sliced_folders($current_file_path, cut_folders) as folder, i (i)}
<div animate:flip={{ duration: 100, easing: cubicOut }}>
<Button
className="shrink-0 py-1 pl-0 {i ===
get_sliced_folders($current_file_path, cut_folders).length - 1
? 'max-w-80 font-bold'
: 'max-w-30'}"
{bg}
click_function={(e) => {
open_path(cut_folders + i + 1, $current_file_path);
}}
>
<ChevronRight class="shrink-0 text-stone-500 h-full" />
<span class="truncate" title={folder}>
{folder}
</span>
</Button>
</div>
{/each}
</div>
</div>
</div>
@@ -0,0 +1,25 @@
<script lang="ts">
let {className = ''} = $props<{className?: string}>();
</script>
<svg
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2.7"
stroke-linecap="round"
stroke-linejoin="round"
class="{className} lucide lucide-refresh-ccw-dot-icon lucide-refresh-ccw-dot"
>
<path d="M21 12a9 9 0 0 0-9-9 9.75 9.75 0 0 0-6.74 2.74L3 8" />
<path d="M3 3v5h5" />
<path d="M3 12a9 9 0 0 0 9 9 9.75 9.75 0 0 0 6.74-2.74L21 16" />
<path d="M16 16h5v5" />
<path
d="M15.033 9.44a.647.647 0 0 1 0 1.12l-4.065 2.352a.645.645 0 0 1-.968-.56V7.648a.645.645 0 0 1 .967-.56z"
transform="translate(0,2.0)"
/>
</svg>