mirror of
https://codeberg.org/PLG-Development/PLG-MuDiCS
synced 2026-07-06 00:47:09 +00:00
refactor(frontend): use db for files & displays
Co-Authored-By: E44 <129310925+programmer-44@users.noreply.github.com>
This commit is contained in:
Generated
-18
@@ -1506,7 +1506,6 @@
|
|||||||
"integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==",
|
"integrity": "sha512-zN2yzBc2dIES2BSzLhNP2weYhwB77kgM/oAktICZVmmljyEmPZrlUwr14jjdK9/eDu7WdAuf6gTdYIJLTcN3Fw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@standard-schema/spec": "^1.0.0",
|
"@standard-schema/spec": "^1.0.0",
|
||||||
"@sveltejs/acorn-typescript": "^1.0.5",
|
"@sveltejs/acorn-typescript": "^1.0.5",
|
||||||
@@ -1546,7 +1545,6 @@
|
|||||||
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
"integrity": "sha512-YZs/OSKOQAQCnJvM/P+F1URotNnYNeU3P2s4oIpzm1uFaqUEqRxUB0g5ejMjEb5Gjb9/PiBI5Ktrq4rUUF8UVQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
"@sveltejs/vite-plugin-svelte-inspector": "^5.0.0",
|
||||||
"debug": "^4.4.1",
|
"debug": "^4.4.1",
|
||||||
@@ -1903,7 +1901,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/core/-/core-3.11.0.tgz",
|
||||||
"integrity": "sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==",
|
"integrity": "sha512-kmS7ZVpHm1EMnW1Wmft9H5ZLM7E0G0NGBx+aGEHGDcNxZBXD2ZUa76CuWjIhOGpwsPbELp684ZdpF2JWoNi4Dg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
@@ -2105,7 +2102,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extension-list/-/extension-list-3.11.0.tgz",
|
||||||
"integrity": "sha512-4Ane7VCVZ+GFOQNuy2nMP+SoWH7EemC3geTTqvgHm1H0tbSosxLJAVaZ9dF06F35RJmYCm+jLJUhRVd156eCRQ==",
|
"integrity": "sha512-4Ane7VCVZ+GFOQNuy2nMP+SoWH7EemC3geTTqvgHm1H0tbSosxLJAVaZ9dF06F35RJmYCm+jLJUhRVd156eCRQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
@@ -2238,7 +2234,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/extensions/-/extensions-3.11.0.tgz",
|
||||||
"integrity": "sha512-g43beA73ZMLezez1st9LEwYrRHZ0FLzlsSlOZKk7sdmtHLmuqWHf4oyb0XAHol1HZIdGv104rYaGNgmQXr1ecQ==",
|
"integrity": "sha512-g43beA73ZMLezez1st9LEwYrRHZ0FLzlsSlOZKk7sdmtHLmuqWHf4oyb0XAHol1HZIdGv104rYaGNgmQXr1ecQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/ueberdosis"
|
"url": "https://github.com/sponsors/ueberdosis"
|
||||||
@@ -2253,7 +2248,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.11.0.tgz",
|
"resolved": "https://registry.npmjs.org/@tiptap/pm/-/pm-3.11.0.tgz",
|
||||||
"integrity": "sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==",
|
"integrity": "sha512-plCQDLCZIOc92cizB8NNhBRN0szvYR3cx9i5IXo6v9Xsgcun8KHNcJkesc2AyeqdIs0BtOJZaqQ9adHThz8UDw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-changeset": "^2.3.0",
|
"prosemirror-changeset": "^2.3.0",
|
||||||
"prosemirror-collab": "^1.3.1",
|
"prosemirror-collab": "^1.3.1",
|
||||||
@@ -2403,7 +2397,6 @@
|
|||||||
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
"integrity": "sha512-n1H6IcDhmmUEG7TNVSspGmiHHutt7iVKtZwRppD7e04wha5MrkV1h3pti9xQLcCMt6YWsncpoT0HMjkH1FNwWQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@typescript-eslint/scope-manager": "8.46.0",
|
"@typescript-eslint/scope-manager": "8.46.0",
|
||||||
"@typescript-eslint/types": "8.46.0",
|
"@typescript-eslint/types": "8.46.0",
|
||||||
@@ -2621,7 +2614,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
|
||||||
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"acorn": "bin/acorn"
|
"acorn": "bin/acorn"
|
||||||
},
|
},
|
||||||
@@ -3018,7 +3010,6 @@
|
|||||||
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
"integrity": "sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@eslint-community/eslint-utils": "^4.8.0",
|
"@eslint-community/eslint-utils": "^4.8.0",
|
||||||
"@eslint-community/regexpp": "^4.12.1",
|
"@eslint-community/regexpp": "^4.12.1",
|
||||||
@@ -4198,7 +4189,6 @@
|
|||||||
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"engines": {
|
"engines": {
|
||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
},
|
},
|
||||||
@@ -4226,7 +4216,6 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"nanoid": "^3.3.11",
|
"nanoid": "^3.3.11",
|
||||||
"picocolors": "^1.1.1",
|
"picocolors": "^1.1.1",
|
||||||
@@ -4346,7 +4335,6 @@
|
|||||||
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"prettier": "bin/prettier.cjs"
|
"prettier": "bin/prettier.cjs"
|
||||||
},
|
},
|
||||||
@@ -4480,7 +4468,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-model/-/prosemirror-model-1.25.4.tgz",
|
||||||
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
"integrity": "sha512-PIM7E43PBxKce8OQeezAs9j4TP+5yDpZVbuurd1h5phUxEKIu+G2a+EUZzIC5nS1mJktDJWzbqS23n1tsAf5QA==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"orderedmap": "^2.0.0"
|
"orderedmap": "^2.0.0"
|
||||||
}
|
}
|
||||||
@@ -4510,7 +4497,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-state/-/prosemirror-state-1.4.4.tgz",
|
||||||
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
"integrity": "sha512-6jiYHH2CIGbCfnxdHbXZ12gySFY/fz/ulZE333G6bPqIZ4F+TXo9ifiR86nAHpWnfoNjOb3o5ESi7J8Uz1jXHw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.0.0",
|
"prosemirror-model": "^1.0.0",
|
||||||
"prosemirror-transform": "^1.0.0",
|
"prosemirror-transform": "^1.0.0",
|
||||||
@@ -4559,7 +4545,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz",
|
"resolved": "https://registry.npmjs.org/prosemirror-view/-/prosemirror-view-1.41.3.tgz",
|
||||||
"integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==",
|
"integrity": "sha512-SqMiYMUQNNBP9kfPhLO8WXEk/fon47vc52FQsUiJzTBuyjKgEcoAwMyF04eQ4WZ2ArMn7+ReypYL60aKngbACQ==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"prosemirror-model": "^1.20.0",
|
"prosemirror-model": "^1.20.0",
|
||||||
"prosemirror-state": "^1.0.0",
|
"prosemirror-state": "^1.0.0",
|
||||||
@@ -5222,7 +5207,6 @@
|
|||||||
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz",
|
"resolved": "https://registry.npmjs.org/svelte/-/svelte-5.39.11.tgz",
|
||||||
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==",
|
"integrity": "sha512-8MxWVm2+3YwrFbPaxOlT1bbMi6OTenrAgks6soZfiaS8Fptk4EVyRIFhJc3RpO264EeSNwgjWAdki0ufg4zkGw==",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@jridgewell/remapping": "^2.3.4",
|
"@jridgewell/remapping": "^2.3.4",
|
||||||
"@jridgewell/sourcemap-codec": "^1.5.0",
|
"@jridgewell/sourcemap-codec": "^1.5.0",
|
||||||
@@ -5472,7 +5456,6 @@
|
|||||||
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"peer": true,
|
|
||||||
"bin": {
|
"bin": {
|
||||||
"tsc": "bin/tsc",
|
"tsc": "bin/tsc",
|
||||||
"tsserver": "bin/tsserver"
|
"tsserver": "bin/tsserver"
|
||||||
@@ -5541,7 +5524,6 @@
|
|||||||
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
"integrity": "sha512-uzcxnSDVjAopEUjljkWh8EIrg6tlzrjFUfMcR1EVsRDGwf/ccef0qQPRyOrROwhrTDaApueq+ja+KLPlzR/zdg==",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peer": true,
|
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"esbuild": "^0.25.0",
|
"esbuild": "^0.25.0",
|
||||||
"fdir": "^6.5.0",
|
"fdir": "^6.5.0",
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
menu_class = 'right-0',
|
menu_class = 'right-0',
|
||||||
div_class = '',
|
div_class = '',
|
||||||
children
|
children
|
||||||
} = $props<{
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
hover_bg?: string;
|
hover_bg?: string;
|
||||||
@@ -28,7 +28,7 @@
|
|||||||
menu_class?: string;
|
menu_class?: string;
|
||||||
div_class?: string;
|
div_class?: string;
|
||||||
children?: any;
|
children?: any;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
let menu_shown = $state(false);
|
let menu_shown = $state(false);
|
||||||
let button_element: HTMLButtonElement;
|
let button_element: HTMLButtonElement;
|
||||||
@@ -166,8 +166,8 @@
|
|||||||
? 'text-stone-500 cursor-not-allowed'
|
? 'text-stone-500 cursor-not-allowed'
|
||||||
: 'hover:bg-white/35 active:bg-white/60 cursor-pointer ' +
|
: 'hover:bg-white/35 active:bg-white/60 cursor-pointer ' +
|
||||||
option.class} rounded-lg p-2 transition-colors duration-200 select-none flex flex-row gap-2 items-center"
|
option.class} rounded-lg p-2 transition-colors duration-200 select-none flex flex-row gap-2 items-center"
|
||||||
onclick={(e) => {
|
onclick={async (e) => {
|
||||||
if (option.on_select) option.on_select();
|
if (option.on_select) await option.on_select();
|
||||||
close_menu();
|
close_menu();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@
|
|||||||
import PopUp from './PopUp.svelte';
|
import PopUp from './PopUp.svelte';
|
||||||
import type { PopupContent } from '../ts/types';
|
import type { PopupContent } from '../ts/types';
|
||||||
import KeyInput from './KeyInput.svelte';
|
import KeyInput from './KeyInput.svelte';
|
||||||
import { send_keyboard_input, show_blackscreen, show_html } from '../ts/api_handler';
|
import { send_keyboard_input, show_blackscreen } from '../ts/api_handler';
|
||||||
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||||
import { selected_display_ids } from '../ts/stores/select';
|
import { selected_display_ids } from '../ts/stores/select';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
@@ -81,16 +81,16 @@
|
|||||||
title="Vorherige Folie (Pfeil nach Links)"
|
title="Vorherige Folie (Pfeil nach Links)"
|
||||||
className="px-9"
|
className="px-9"
|
||||||
disabled={$selected_display_ids.length === 0}
|
disabled={$selected_display_ids.length === 0}
|
||||||
click_function={() => {
|
click_function={async () => {
|
||||||
run_on_all_selected_displays(send_keyboard_input, true, 'VK_LEFT');
|
await run_on_all_selected_displays(send_keyboard_input, true, 'VK_LEFT');
|
||||||
}}><ArrowBigLeft /></Button
|
}}><ArrowBigLeft /></Button
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
title="Nächste Folie (Pfeil nach Rechts)"
|
title="Nächste Folie (Pfeil nach Rechts)"
|
||||||
className="px-9"
|
className="px-9"
|
||||||
disabled={$selected_display_ids.length === 0}
|
disabled={$selected_display_ids.length === 0}
|
||||||
click_function={() => {
|
click_function={async () => {
|
||||||
run_on_all_selected_displays(send_keyboard_input, true, 'VK_RIGHT');
|
await run_on_all_selected_displays(send_keyboard_input, true, 'VK_RIGHT');
|
||||||
}}><ArrowBigRight /></Button
|
}}><ArrowBigRight /></Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -102,8 +102,8 @@
|
|||||||
<Button
|
<Button
|
||||||
className="px-3 flex gap-3 w-75 justify-normal"
|
className="px-3 flex gap-3 w-75 justify-normal"
|
||||||
disabled={$selected_display_ids.length === 0}
|
disabled={$selected_display_ids.length === 0}
|
||||||
click_function={() => {
|
click_function={async () => {
|
||||||
run_on_all_selected_displays(show_blackscreen, true);
|
await run_on_all_selected_displays(show_blackscreen, true);
|
||||||
}}><Presentation />Blackout</Button
|
}}><Presentation />Blackout</Button
|
||||||
>
|
>
|
||||||
<div class="flex flex-row justify-normal">
|
<div class="flex flex-row justify-normal">
|
||||||
|
|||||||
@@ -2,10 +2,10 @@
|
|||||||
import { GripHorizontal } from 'lucide-svelte';
|
import { GripHorizontal } from 'lucide-svelte';
|
||||||
import { dragHandle } from 'svelte-dnd-action';
|
import { dragHandle } from 'svelte-dnd-action';
|
||||||
|
|
||||||
let { bg, className = "" } = $props<{
|
let { bg, className = "" }: {
|
||||||
bg: string;
|
bg: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
}>();
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -10,51 +10,61 @@
|
|||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import DisplayObject from './DisplayObject.svelte';
|
import DisplayObject from './DisplayObject.svelte';
|
||||||
import {
|
import {
|
||||||
add_empty_display_group,
|
|
||||||
all_displays_of_group_selected,
|
all_displays_of_group_selected,
|
||||||
remove_empty_display_groups,
|
get_display_ids_in_group,
|
||||||
select_all_of_group,
|
select_all_of_group,
|
||||||
set_new_display_group_data
|
set_new_display_order
|
||||||
} from '../ts/stores/displays';
|
} from '../ts/stores/displays';
|
||||||
import DNDGrip from './DNDGrip.svelte';
|
import DNDGrip from './DNDGrip.svelte';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import type { DisplayGroup, MenuOption } from '../ts/types';
|
import type { DisplayGroup, MenuOption } from '../ts/types';
|
||||||
import { selected_display_ids } from '../ts/stores/select';
|
import { selected_display_ids } from '../ts/stores/select';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
let { display_group, get_display_menu_options, close_pinned_display } = $props<{
|
let { display_group_id, get_display_menu_options, close_pinned_display }: {
|
||||||
display_group: DisplayGroup;
|
|
||||||
|
display_group_id: string;
|
||||||
get_display_menu_options: (display_id: string) => MenuOption[];
|
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||||
close_pinned_display: () => void;
|
close_pinned_display: () => void;
|
||||||
}>();
|
} = $props
|
||||||
|
();
|
||||||
|
|
||||||
|
let all_selected = liveQuery(() =>
|
||||||
|
all_displays_of_group_selected(display_group_id, $selected_display_ids)
|
||||||
|
);
|
||||||
|
let display_ids_in_group = liveQuery(() => get_display_ids_in_group(display_group_id));
|
||||||
let hovering_selectable = $state(false);
|
let hovering_selectable = $state(false);
|
||||||
|
|
||||||
function select_all_of_this_group() {
|
|
||||||
const new_value = !all_displays_of_group_selected(display_group, $selected_display_ids);
|
async function select_all_of_this_group() {
|
||||||
select_all_of_group(display_group, new_value);
|
const new_value = !(await all_displays_of_group_selected(
|
||||||
|
display_group_id,
|
||||||
|
$selected_display_ids
|
||||||
|
));
|
||||||
|
await select_all_of_group(display_group_id, new_value);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_consider(e: CustomEvent) {
|
async function handle_consider(e: CustomEvent) {
|
||||||
const { items, info } = e.detail;
|
const { items, info } = e.detail;
|
||||||
|
|
||||||
if (items.length !== 1 && info.trigger === TRIGGERS.DRAG_STARTED) {
|
if (items.length !== 1 && info.trigger === TRIGGERS.DRAG_STARTED) {
|
||||||
$is_display_drag = true;
|
$is_display_drag = true;
|
||||||
add_empty_display_group();
|
// add_empty_display_group();
|
||||||
}
|
}
|
||||||
set_new_display_group_data(display_group.id, items);
|
await set_new_display_order(items);
|
||||||
}
|
}
|
||||||
|
|
||||||
function handle_finalize(e: CustomEvent) {
|
async function handle_finalize(e: CustomEvent) {
|
||||||
remove_empty_display_groups();
|
|
||||||
$is_display_drag = false;
|
$is_display_drag = false;
|
||||||
set_new_display_group_data(display_group.id, e.detail.items);
|
await set_new_display_order(e.detail.items);
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
transition:fade={{ duration: 100 }}
|
transition:fade={{ duration: 100 }}
|
||||||
class="{get_selectable_color_classes(
|
class="{get_selectable_color_classes(
|
||||||
all_displays_of_group_selected(display_group, $selected_display_ids),
|
$all_selected || false,
|
||||||
{
|
{
|
||||||
bg: true,
|
bg: true,
|
||||||
hover: hovering_selectable,
|
hover: hovering_selectable,
|
||||||
@@ -68,7 +78,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex flex-col min-w-0 pl-2 py-2 gap-2 w-full"
|
class="flex flex-col min-w-0 pl-2 py-2 gap-2 w-full"
|
||||||
use:dragHandleZone={{
|
use:dragHandleZone={{
|
||||||
items: display_group.data,
|
items: $display_ids_in_group || [],
|
||||||
type: 'item',
|
type: 'item',
|
||||||
flipDurationMs: dnd_flip_duration_ms,
|
flipDurationMs: dnd_flip_duration_ms,
|
||||||
dropTargetStyle: { outline: 'none' }
|
dropTargetStyle: { outline: 'none' }
|
||||||
@@ -76,7 +86,7 @@
|
|||||||
onconsider={handle_consider}
|
onconsider={handle_consider}
|
||||||
onfinalize={handle_finalize}
|
onfinalize={handle_finalize}
|
||||||
>
|
>
|
||||||
{#each display_group.data as display (display.id)}
|
{#each $display_ids_in_group || [] as display (display.id)}
|
||||||
<!-- Each Group -->
|
<!-- Each Group -->
|
||||||
<section
|
<section
|
||||||
animate:flip={{ duration: $is_group_drag ? 0 : dnd_flip_duration_ms, easing: cubicOut }}
|
animate:flip={{ duration: $is_group_drag ? 0 : dnd_flip_duration_ms, easing: cubicOut }}
|
||||||
@@ -87,7 +97,7 @@
|
|||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
{#if display_group.data.length === 0}
|
{#if ($display_ids_in_group || []).length === 0}
|
||||||
<div class="min-h-10 h-full w-full pl-2 py-2 flex justify-center items-center">
|
<div class="min-h-10 h-full w-full pl-2 py-2 flex justify-center items-center">
|
||||||
Hier in leere neue Gruppe ablegen
|
Hier in leere neue Gruppe ablegen
|
||||||
</div>
|
</div>
|
||||||
@@ -95,18 +105,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="flex items-center justify-center px-2"
|
class="flex items-center justify-center px-2"
|
||||||
onclick={(e) => select_all_of_this_group()}
|
onclick={select_all_of_this_group}
|
||||||
role="button"
|
role="button"
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
onkeydown={(e) => {
|
onkeydown={async (e) => {
|
||||||
if (e.key === 'Enter' || e.key === ' ') select_all_of_this_group();
|
if (e.key === 'Enter' || e.key === ' ') await select_all_of_this_group();
|
||||||
}}
|
}}
|
||||||
onmouseenter={() => (hovering_selectable = true)}
|
onmouseenter={() => (hovering_selectable = true)}
|
||||||
onmouseleave={() => (hovering_selectable = false)}
|
onmouseleave={() => (hovering_selectable = false)}
|
||||||
>
|
>
|
||||||
<DNDGrip
|
<DNDGrip
|
||||||
bg={get_selectable_color_classes(
|
bg={get_selectable_color_classes(
|
||||||
all_displays_of_group_selected(display_group, $selected_display_ids),
|
$all_selected || false,
|
||||||
{
|
{
|
||||||
bg: true
|
bg: true
|
||||||
},
|
},
|
||||||
|
|||||||
Regular → Executable
+17
-11
@@ -10,30 +10,36 @@
|
|||||||
import OnlineState from './OnlineState.svelte';
|
import OnlineState from './OnlineState.svelte';
|
||||||
import type { Display, MenuOption } from '../ts/types';
|
import type { Display, MenuOption } from '../ts/types';
|
||||||
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
import { is_selected, select, selected_display_ids } from '../ts/stores/select';
|
||||||
import { update_screenshot } from '../ts/stores/displays';
|
import { screenshot_loop } from '../ts/stores/displays';
|
||||||
import { filter_file_selection_for_current_selected_displays } from '../ts/stores/files';
|
import { change_file_path, current_file_path } from '../ts/stores/files';
|
||||||
|
|
||||||
let { display, get_display_menu_options, close_pinned_display } = $props<{
|
let {
|
||||||
|
display,
|
||||||
|
get_display_menu_options,
|
||||||
|
close_pinned_display
|
||||||
|
}: {
|
||||||
display: Display;
|
display: Display;
|
||||||
get_display_menu_options: (display_id: string) => MenuOption[];
|
get_display_menu_options: (display_id: string) => MenuOption[];
|
||||||
close_pinned_display: () => void;
|
close_pinned_display: () => void;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
let hovering_unselectable = $state(false);
|
let hovering_unselectable = $state(false);
|
||||||
|
|
||||||
function onclick(e: Event) {
|
async function onclick(e: Event) {
|
||||||
select(selected_display_ids, display.id);
|
|
||||||
filter_file_selection_for_current_selected_displays();
|
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
|
select(selected_display_ids, display.id, 'toggle');
|
||||||
|
|
||||||
|
// force file view update
|
||||||
|
await change_file_path($current_file_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
function on_preview_click(e: MouseEvent) {
|
async function on_preview_click(e: MouseEvent) {
|
||||||
if ($pinned_display_id === display.id) {
|
if ($pinned_display_id === display.id) {
|
||||||
close_pinned_display();
|
close_pinned_display();
|
||||||
} else {
|
} else {
|
||||||
$pinned_display_id = display.id;
|
$pinned_display_id = display.id;
|
||||||
}
|
}
|
||||||
update_screenshot(display.id);
|
await screenshot_loop(display.id);
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
@@ -65,8 +71,8 @@
|
|||||||
<div class="size-[50%]">
|
<div class="size-[50%]">
|
||||||
<Pin class="size-full" />
|
<Pin class="size-full" />
|
||||||
</div>
|
</div>
|
||||||
{:else if display.preview_url}
|
{:else if display.preview.url}
|
||||||
<img src={display.preview_url} alt="preview" class="w-full object-cover bg-black" />
|
<img src={display.preview.url} alt="preview" class="w-full object-cover bg-black" />
|
||||||
{:else}
|
{:else}
|
||||||
<!-- No Signal -->
|
<!-- No Signal -->
|
||||||
<div class="size-full bg-black flex justify-center items-center">
|
<div class="size-full bg-black flex justify-center items-center">
|
||||||
|
|||||||
Regular → Executable
+45
-32
@@ -2,9 +2,10 @@
|
|||||||
import { fade, scale } from 'svelte/transition';
|
import { fade, scale } from 'svelte/transition';
|
||||||
import {
|
import {
|
||||||
all_displays_of_group_selected,
|
all_displays_of_group_selected,
|
||||||
displays,
|
|
||||||
get_display_by_id,
|
get_display_by_id,
|
||||||
select_all_of_group
|
get_display_groups,
|
||||||
|
select_all_of_group,
|
||||||
|
set_new_display_group_order
|
||||||
} from '../ts/stores/displays';
|
} from '../ts/stores/displays';
|
||||||
import {
|
import {
|
||||||
change_height,
|
change_height,
|
||||||
@@ -27,15 +28,23 @@
|
|||||||
import DisplayGroupObject from './DisplayGroupObject.svelte';
|
import DisplayGroupObject from './DisplayGroupObject.svelte';
|
||||||
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
import { Pane, Splitpanes } from 'svelte-splitpanes';
|
||||||
import HighlightedText from './HighlightedText.svelte';
|
import HighlightedText from './HighlightedText.svelte';
|
||||||
|
import { liveQuery, type Observable } from 'dexie';
|
||||||
|
|
||||||
let { handle_display_deletion, handle_display_editing } = $props<{
|
let {
|
||||||
|
handle_display_deletion,
|
||||||
|
handle_display_editing
|
||||||
|
}: {
|
||||||
handle_display_deletion: (display_id: string) => void;
|
handle_display_deletion: (display_id: string) => void;
|
||||||
handle_display_editing: (display_id: string) => void;
|
handle_display_editing: (display_id: string) => void;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
let displays_scroll_box: HTMLElement;
|
let displays_scroll_box: HTMLElement;
|
||||||
let pinned_display: Display | null = $derived(
|
let pinned_display: Observable<Display | null> = liveQuery(() =>
|
||||||
get_display_by_id($pinned_display_id || '', $displays)
|
get_display_by_id($pinned_display_id || '')
|
||||||
|
);
|
||||||
|
let display_groups = liveQuery(() => get_display_groups());
|
||||||
|
let all_groups_selected = liveQuery(() =>
|
||||||
|
all_selected($display_groups || [], $selected_display_ids)
|
||||||
);
|
);
|
||||||
|
|
||||||
let last_pinned_pane_size: number = 45;
|
let last_pinned_pane_size: number = 45;
|
||||||
@@ -66,16 +75,22 @@
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
function select_all(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
async function select_all(
|
||||||
const new_value = !all_selected(current_displays, current_selected_display_ids);
|
current_displays: DisplayGroup[],
|
||||||
|
current_selected_display_ids: string[]
|
||||||
|
) {
|
||||||
|
const new_value = !(await all_selected(current_displays, current_selected_display_ids));
|
||||||
for (const display_group of current_displays) {
|
for (const display_group of current_displays) {
|
||||||
select_all_of_group(display_group, new_value);
|
await select_all_of_group(display_group.id, new_value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function all_selected(current_displays: DisplayGroup[], current_selected_display_ids: string[]) {
|
async function all_selected(
|
||||||
|
current_displays: DisplayGroup[],
|
||||||
|
current_selected_display_ids: string[]
|
||||||
|
) {
|
||||||
for (const display_group of current_displays) {
|
for (const display_group of current_displays) {
|
||||||
if (!all_displays_of_group_selected(display_group, current_selected_display_ids)) {
|
if (!(await all_displays_of_group_selected(display_group.id, current_selected_display_ids))) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -121,16 +136,16 @@
|
|||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="text-xl font-bold pl-2 content-center truncate min-w-0"
|
class="text-xl font-bold pl-2 content-center truncate min-w-0"
|
||||||
title={pinned_display?.name}
|
title={$pinned_display?.name || '...'}
|
||||||
>
|
>
|
||||||
{pinned_display?.name}
|
{$pinned_display?.name || '...'}
|
||||||
</span>
|
</span>
|
||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
<div class="flex flex-row items-center mr-1">
|
<div class="flex flex-row items-center mr-1">
|
||||||
<span class="text-stone-400"> Aktueller Status: </span>
|
<span class="text-stone-400"> Aktueller Status: </span>
|
||||||
<OnlineState
|
<OnlineState
|
||||||
selected={false}
|
selected={false}
|
||||||
status={pinned_display?.status ?? null}
|
status={$pinned_display?.status ?? null}
|
||||||
className="flex items-center px-2"
|
className="flex items-center px-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -159,9 +174,9 @@
|
|||||||
<div
|
<div
|
||||||
class="h-full bg-stone-800 rounded-b-2xl overflow-hidden flex justify-center items-center"
|
class="h-full bg-stone-800 rounded-b-2xl overflow-hidden flex justify-center items-center"
|
||||||
>
|
>
|
||||||
{#if pinned_display?.preview_url}
|
{#if $pinned_display?.preview.url}
|
||||||
<img
|
<img
|
||||||
src={pinned_display.preview_url}
|
src={$pinned_display.preview.url}
|
||||||
alt="preview"
|
alt="preview"
|
||||||
class="max-h-full max-w-full object-cover bg-black"
|
class="max-h-full max-w-full object-cover bg-black"
|
||||||
/>
|
/>
|
||||||
@@ -188,7 +203,7 @@
|
|||||||
<div class="flex flex-row gap-1">
|
<div class="flex flex-row gap-1">
|
||||||
<button
|
<button
|
||||||
class="min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
|
class="min-w-40 px-4 rounded-xl cursor-pointer duration-200 transition-colors {get_selectable_color_classes(
|
||||||
all_selected($displays, $selected_display_ids),
|
$all_groups_selected || false,
|
||||||
{
|
{
|
||||||
bg: true,
|
bg: true,
|
||||||
hover: true,
|
hover: true,
|
||||||
@@ -196,13 +211,9 @@
|
|||||||
text: true
|
text: true
|
||||||
}
|
}
|
||||||
)}"
|
)}"
|
||||||
onclick={() => select_all($displays, $selected_display_ids)}
|
onclick={async () => await select_all($display_groups || [], $selected_display_ids)}
|
||||||
>
|
|
||||||
<span
|
|
||||||
>{all_selected($displays, $selected_display_ids)
|
|
||||||
? 'Alle abwählen'
|
|
||||||
: 'Alle auswählen'}</span
|
|
||||||
>
|
>
|
||||||
|
<span>{$all_groups_selected || false ? 'Alle abwählen' : 'Alle auswählen'}</span>
|
||||||
</button>
|
</button>
|
||||||
<div class="flex flex-row">
|
<div class="flex flex-row">
|
||||||
<Button
|
<Button
|
||||||
@@ -234,30 +245,32 @@
|
|||||||
<div
|
<div
|
||||||
class="min-h-full p-2 flex flex-col gap-4"
|
class="min-h-full p-2 flex flex-col gap-4"
|
||||||
use:dragHandleZone={{
|
use:dragHandleZone={{
|
||||||
items: $displays,
|
items: $display_groups || [],
|
||||||
type: 'group',
|
type: 'group',
|
||||||
flipDurationMs: dnd_flip_duration_ms,
|
flipDurationMs: dnd_flip_duration_ms,
|
||||||
dropFromOthersDisabled: true,
|
dropFromOthersDisabled: true,
|
||||||
dropTargetStyle: { outline: 'none' }
|
dropTargetStyle: { outline: 'none' }
|
||||||
}}
|
}}
|
||||||
onconsider={(e: CustomEvent) => {
|
onconsider={async (e: CustomEvent) => {
|
||||||
$is_group_drag = true;
|
$is_group_drag = true;
|
||||||
$displays = e.detail.items;
|
await set_new_display_group_order(e.detail.items);
|
||||||
}}
|
}}
|
||||||
onfinalize={(e: CustomEvent) => {
|
onfinalize={async (e: CustomEvent) => {
|
||||||
$displays = e.detail.items;
|
await set_new_display_group_order(e.detail.items);
|
||||||
$is_group_drag = false;
|
$is_group_drag = false;
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if $displays.length === 1 && $displays[0].data.length === 0}
|
{#if ($display_groups || []).length === 0}
|
||||||
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
|
<div class="text-stone-500 px-10 py-6 leading-relaxed text-center">
|
||||||
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf
|
Es wurden noch keine Bildschirme hinzugefügt. Klicke oben rechts auf
|
||||||
<HighlightedText fg="text-stone-450" className="!p-1"><Settings class="inline pb-1" /></HighlightedText>
|
<HighlightedText fg="text-stone-450" className="!p-1"
|
||||||
|
><Settings class="inline pb-1" /></HighlightedText
|
||||||
|
>
|
||||||
und
|
und
|
||||||
<HighlightedText fg="text-stone-450">Neuen Bildschirm hinzufügen</HighlightedText>.
|
<HighlightedText fg="text-stone-450">Neuen Bildschirm hinzufügen</HighlightedText>.
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{:else}
|
||||||
{#each $displays as display_group (display_group.id)}
|
{#each $display_groups || [] as display_group (display_group.id)}
|
||||||
<!-- Each Group -->
|
<!-- Each Group -->
|
||||||
<section
|
<section
|
||||||
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
out:scale={{ duration: dnd_flip_duration_ms, easing: cubicOut }}
|
||||||
@@ -265,7 +278,7 @@
|
|||||||
class="outline-none"
|
class="outline-none"
|
||||||
>
|
>
|
||||||
<DisplayGroupObject
|
<DisplayGroupObject
|
||||||
{display_group}
|
display_group_id={display_group.id}
|
||||||
{get_display_menu_options}
|
{get_display_menu_options}
|
||||||
{close_pinned_display}
|
{close_pinned_display}
|
||||||
/>
|
/>
|
||||||
|
|||||||
Regular → Executable
+50
-53
@@ -3,6 +3,7 @@
|
|||||||
ClipboardPaste,
|
ClipboardPaste,
|
||||||
Download,
|
Download,
|
||||||
FolderPlus,
|
FolderPlus,
|
||||||
|
Minus,
|
||||||
Info,
|
Info,
|
||||||
Pen,
|
Pen,
|
||||||
RefreshCcw,
|
RefreshCcw,
|
||||||
@@ -17,30 +18,33 @@
|
|||||||
import PathBar from './PathBar.svelte';
|
import PathBar from './PathBar.svelte';
|
||||||
import { selected_display_ids, selected_file_ids } from '../ts/stores/select';
|
import { selected_display_ids, selected_file_ids } from '../ts/stores/select';
|
||||||
import {
|
import {
|
||||||
all_files,
|
|
||||||
current_file_path,
|
current_file_path,
|
||||||
get_current_folder_elements,
|
get_current_folder_elements,
|
||||||
get_display_ids_where_file_is_missing,
|
|
||||||
get_display_ids_where_path_does_not_exist,
|
|
||||||
get_file_by_id,
|
get_file_by_id,
|
||||||
get_longest_existing_path_and_needed_parts,
|
|
||||||
run_for_selected_files_on_selected_displays,
|
run_for_selected_files_on_selected_displays,
|
||||||
update_current_folder_on_selected_displays
|
update_current_folder_on_selected_displays,
|
||||||
|
get_displays_where_path_exists,
|
||||||
|
create_folder_on_all_selected_displays
|
||||||
} from '../ts/stores/files';
|
} from '../ts/stores/files';
|
||||||
import { slide } from 'svelte/transition';
|
import { slide } from 'svelte/transition';
|
||||||
import FolderElementObject from './FolderElementObject.svelte';
|
import InodeElement from './InodeElement.svelte';
|
||||||
import PopUp from './PopUp.svelte';
|
import PopUp from './PopUp.svelte';
|
||||||
import type { PopupContent } from '../ts/types';
|
import { get_file_primary_key, type Inode, type PopupContent } from '../ts/types';
|
||||||
import TextInput from './TextInput.svelte';
|
import TextInput from './TextInput.svelte';
|
||||||
import { is_valid_name } from '../ts/utils';
|
import { is_valid_name } from '../ts/utils';
|
||||||
import { displays, get_display_by_id, run_on_all_selected_displays } from '../ts/stores/displays';
|
import { delete_files, rename_file } from '../ts/api_handler';
|
||||||
import { create_folders, delete_files, rename_file } from '../ts/api_handler';
|
|
||||||
import { get } from 'svelte/store';
|
|
||||||
import HighlightedText from './HighlightedText.svelte';
|
import HighlightedText from './HighlightedText.svelte';
|
||||||
|
import { liveQuery } from 'dexie';
|
||||||
|
|
||||||
let current_name: string = $state('');
|
let current_name: string = $state('');
|
||||||
let current_valid: boolean = $state(false);
|
let current_valid: boolean = $state(false);
|
||||||
|
|
||||||
|
let display_names_where_path_does_not_exist: string[] = $state([]);
|
||||||
|
let selected_files = liveQuery(() => get_selected_files($selected_display_ids));
|
||||||
|
let current_folder_elements = liveQuery(() =>
|
||||||
|
get_current_folder_elements($current_file_path, $selected_display_ids)
|
||||||
|
);
|
||||||
|
|
||||||
let popup_content: PopupContent = $state({
|
let popup_content: PopupContent = $state({
|
||||||
open: false,
|
open: false,
|
||||||
snippet: null,
|
snippet: null,
|
||||||
@@ -48,24 +52,26 @@
|
|||||||
closable: true
|
closable: true
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function get_selected_files(selected_file_ids: string[]): Promise<Inode[]> {
|
||||||
|
try {
|
||||||
|
const results = await Promise.all(selected_file_ids.map((id) => get_file_by_id(id)));
|
||||||
|
return results.filter((element) => element !== null);
|
||||||
|
} catch (e: unknown) {
|
||||||
|
console.error('Error on generating selected_files');
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function popup_close_function() {
|
function popup_close_function() {
|
||||||
popup_content.open = false;
|
popup_content.open = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function create_new_folder() {
|
async function create_new_folder() {
|
||||||
for (const display_id of $selected_display_ids) {
|
await create_folder_on_all_selected_displays(
|
||||||
const display = get_display_by_id(display_id, $displays);
|
current_name.trim(),
|
||||||
if (!display) continue;
|
|
||||||
const path_data = get_longest_existing_path_and_needed_parts(
|
|
||||||
$current_file_path,
|
$current_file_path,
|
||||||
display_id,
|
$selected_display_ids
|
||||||
$all_files
|
|
||||||
);
|
);
|
||||||
await create_folders(display.ip, path_data.existing, [
|
|
||||||
...path_data.needed,
|
|
||||||
current_name.trim()
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
await update_current_folder_on_selected_displays();
|
await update_current_folder_on_selected_displays();
|
||||||
popup_close_function();
|
popup_close_function();
|
||||||
}
|
}
|
||||||
@@ -82,8 +88,8 @@
|
|||||||
popup_close_function();
|
popup_close_function();
|
||||||
}
|
}
|
||||||
|
|
||||||
const show_edit_file_popup = () => {
|
const show_edit_file_popup = async () => {
|
||||||
const file = get_file_by_id($selected_file_ids[0], $all_files, $current_file_path);
|
const file = await get_file_by_id($selected_file_ids[0]);
|
||||||
if (!file) return;
|
if (!file) return;
|
||||||
const is_folder = file.type === 'inode/directory';
|
const is_folder = file.type === 'inode/directory';
|
||||||
const extension = is_folder ? '' : '.' + file.name.split('.').at(-1) || '';
|
const extension = is_folder ? '' : '.' + file.name.split('.').at(-1) || '';
|
||||||
@@ -99,9 +105,12 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const show_new_folder_popup = () => {
|
const show_new_folder_popup = async () => {
|
||||||
current_name = '';
|
current_name = '';
|
||||||
current_valid = false;
|
current_valid = false;
|
||||||
|
display_names_where_path_does_not_exist = (
|
||||||
|
await get_displays_where_path_exists($current_file_path, $selected_display_ids, true)
|
||||||
|
).map((display) => display.name);
|
||||||
popup_content = {
|
popup_content = {
|
||||||
open: true,
|
open: true,
|
||||||
snippet: new_folder_popup,
|
snippet: new_folder_popup,
|
||||||
@@ -123,22 +132,18 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet new_folder_popup()}
|
{#snippet new_folder_popup()}
|
||||||
{#if get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files).length > 0}
|
{#if display_names_where_path_does_not_exist.length > 0}
|
||||||
<span class="leading-relaxed"
|
<span class="leading-relaxed"
|
||||||
>Der aktuelle Pfad <HighlightedText
|
>Der aktuelle Pfad <HighlightedText
|
||||||
>{$current_file_path.slice(0, $current_file_path.length - 1)}</HighlightedText
|
>{$current_file_path.slice(0, $current_file_path.length - 1)}</HighlightedText
|
||||||
> existiert nicht auf {get_display_ids_where_path_does_not_exist(
|
> existiert nicht auf {display_names_where_path_does_not_exist.length === 1
|
||||||
$current_file_path,
|
|
||||||
$selected_display_ids,
|
|
||||||
$all_files
|
|
||||||
).length === 1
|
|
||||||
? 'dem Bildschirm'
|
? 'dem Bildschirm'
|
||||||
: 'den Bildschirmen'}
|
: 'den Bildschirmen'}
|
||||||
{#each get_display_ids_where_path_does_not_exist($current_file_path, $selected_display_ids, $all_files) as display_id, i}
|
{#each display_names_where_path_does_not_exist as display_name, i}
|
||||||
{#if i !== 0}
|
{#if i !== 0}
|
||||||
,
|
,
|
||||||
{/if}
|
{/if}
|
||||||
<HighlightedText>{get_display_by_id(display_id, $displays)?.name}</HighlightedText>
|
<HighlightedText>{display_name}</HighlightedText>
|
||||||
{/each}. Mit der Erstellung dieses Ordners wird der Pfad automatisch mit leeren Ordnern bis
|
{/each}. Mit der Erstellung dieses Ordners wird der Pfad automatisch mit leeren Ordnern bis
|
||||||
zum aktuellen Pfad aufgefüllt.
|
zum aktuellen Pfad aufgefüllt.
|
||||||
</span>
|
</span>
|
||||||
@@ -148,14 +153,14 @@
|
|||||||
bind:current_value={current_name}
|
bind:current_value={current_name}
|
||||||
bind:current_valid
|
bind:current_valid
|
||||||
title="Ordnername"
|
title="Ordnername"
|
||||||
is_valid_function={(input: string) => {
|
is_valid_function={async (input: string) => {
|
||||||
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
||||||
const trimmed_input = input.trim();
|
const trimmed_input = input.trim();
|
||||||
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
||||||
return [false, 'Ungültige Länge'];
|
return [false, 'Ungültige Länge'];
|
||||||
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
||||||
if (
|
if (
|
||||||
get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some(
|
(await get_current_folder_elements($current_file_path, $selected_display_ids)).some(
|
||||||
(e) => e.name === trimmed_input
|
(e) => e.name === trimmed_input
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -178,30 +183,28 @@
|
|||||||
bind:current_value={current_name}
|
bind:current_value={current_name}
|
||||||
bind:current_valid
|
bind:current_valid
|
||||||
title="Neuer {extension === '' ? 'Ordner' : 'Datei'}name"
|
title="Neuer {extension === '' ? 'Ordner' : 'Datei'}name"
|
||||||
is_valid_function={(input: string) => {
|
is_valid_function={async (input: string) => {
|
||||||
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
if (input.startsWith('.')) return [false, 'Name darf nicht mit . beginnen'];
|
||||||
const trimmed_input = input.trim() + extension;
|
const trimmed_input = input.trim() + extension;
|
||||||
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
if (trimmed_input.length === 0 || trimmed_input.length > 50)
|
||||||
return [false, 'Ungültige Länge'];
|
return [false, 'Ungültige Länge'];
|
||||||
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
if (!is_valid_name(trimmed_input)) return [false, 'Name enthält ungültige Zeichen'];
|
||||||
if (
|
if (
|
||||||
get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).some(
|
(await get_current_folder_elements($current_file_path, $selected_display_ids)).some(
|
||||||
(e) =>
|
async (e) => e.name === trimmed_input && get_file_primary_key(e) !== $selected_file_ids[0]
|
||||||
e.name === trimmed_input &&
|
|
||||||
e.id !== get_file_by_id($selected_file_ids[0], $all_files, $current_file_path)?.id
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return [false, 'Name bereits verwendet'];
|
return [false, 'Name bereits verwendet'];
|
||||||
return [true, 'Gültiger Name'];
|
return [true, 'Gültiger Name'];
|
||||||
}}
|
}}
|
||||||
enter_mode="submit"
|
enter_mode="submit"
|
||||||
enter_function={() => edit_file_name(current_name.trim() + extension)}
|
enter_function={async () => await edit_file_name(current_name.trim() + extension)}
|
||||||
{extension}
|
{extension}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row justify-end gap-2">
|
<div class="flex flex-row justify-end gap-2">
|
||||||
<Button
|
<Button
|
||||||
className="px-4 font-bold"
|
className="px-4 font-bold"
|
||||||
click_function={() => edit_file_name(current_name.trim() + extension)}
|
click_function={async () => await edit_file_name(current_name.trim() + extension)}
|
||||||
disabled={!current_valid}>{extension === '' ? 'Ordner' : 'Datei'} umbenennen</Button
|
disabled={!current_valid}>{extension === '' ? 'Ordner' : 'Datei'} umbenennen</Button
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
@@ -213,10 +216,8 @@
|
|||||||
>{`${$selected_file_ids.length === 1 ? 'Folgendes Objekt' : `Folgende ${$selected_file_ids.length} Objekte`} löschen? (Wiederherstellung nicht möglich)`}</span
|
>{`${$selected_file_ids.length === 1 ? 'Folgendes Objekt' : `Folgende ${$selected_file_ids.length} Objekte`} löschen? (Wiederherstellung nicht möglich)`}</span
|
||||||
>
|
>
|
||||||
<div class="flex flex-col gap-2 overflow-auto h-full min-h-0 grow-0">
|
<div class="flex flex-col gap-2 overflow-auto h-full min-h-0 grow-0">
|
||||||
{#each $selected_file_ids
|
{#each $selected_files || [] as file}
|
||||||
.map((file_id) => get_file_by_id(file_id, $all_files, $current_file_path))
|
<InodeElement {file} not_interactable />
|
||||||
.filter((element) => element !== null) as file}
|
|
||||||
<FolderElementObject {file} not_interactable />
|
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,10 +240,6 @@
|
|||||||
</div>
|
</div>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet clipboard_hover_snippet()}
|
|
||||||
<div></div>
|
|
||||||
{/snippet}
|
|
||||||
|
|
||||||
<div class="bg-stone-800 h-full rounded-2xl grid grid-rows-[2.5rem_1fr] min-h-0">
|
<div class="bg-stone-800 h-full rounded-2xl grid grid-rows-[2.5rem_1fr] min-h-0">
|
||||||
<div class="bg-stone-700 flex justify-between w-full p-1 rounded-t-2xl min-w-0 gap-2">
|
<div class="bg-stone-700 flex justify-between w-full p-1 rounded-t-2xl min-w-0 gap-2">
|
||||||
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
|
<span class="text-xl font-bold pl-2 content-center truncate min-w-0">
|
||||||
@@ -312,7 +309,7 @@
|
|||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
title="Ausgewählte Datei(en) einfügen"
|
title="Ausgewählte Datei(en) einfügen"
|
||||||
className="!p-0 flex relative"
|
className="px-3 flex"
|
||||||
disabled={$selected_display_ids.length === 0}
|
disabled={$selected_display_ids.length === 0}
|
||||||
>
|
>
|
||||||
<ClipboardPaste />
|
<ClipboardPaste />
|
||||||
@@ -342,12 +339,12 @@
|
|||||||
Es sind keine Bildschirme ausgewählt.
|
Es sind keine Bildschirme ausgewählt.
|
||||||
</span>
|
</span>
|
||||||
{:else}
|
{:else}
|
||||||
{#each get_current_folder_elements($all_files, $current_file_path, $selected_display_ids) as folder_element (folder_element.id)}
|
{#each $current_folder_elements || [] as folder_element (get_file_primary_key(folder_element))}
|
||||||
<section in:slide={{ duration: 100 }} class="outline-none">
|
<section in:slide={{ duration: 100 }} class="outline-none">
|
||||||
<FolderElementObject file={folder_element} />
|
<InodeElement file={folder_element} />
|
||||||
</section>
|
</section>
|
||||||
{/each}
|
{/each}
|
||||||
{#if get_current_folder_elements($all_files, $current_file_path, $selected_display_ids).length === 0}
|
{#if ($current_folder_elements || []).length === 0}
|
||||||
<span class="text-stone-450 px-10 py-6 leading-relaxed text-center max-w-full">
|
<span class="text-stone-450 px-10 py-6 leading-relaxed text-center max-w-full">
|
||||||
Es existieren keine Dateien auf {$selected_display_ids.length === 1
|
Es existieren keine Dateien auf {$selected_display_ids.length === 1
|
||||||
? 'dem ausgewähltem Bildchirm'
|
? 'dem ausgewähltem Bildchirm'
|
||||||
|
|||||||
@@ -4,9 +4,16 @@
|
|||||||
bg = 'bg-stone-750',
|
bg = 'bg-stone-750',
|
||||||
fg = 'text-stone-200',
|
fg = 'text-stone-200',
|
||||||
className = ''
|
className = ''
|
||||||
} = $props<{ children: any; bg?: string; fg?: string; className?: string }>();
|
}: {
|
||||||
|
children: any;
|
||||||
|
bg?: string;
|
||||||
|
fg?: string;
|
||||||
|
className?: string;
|
||||||
|
} = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="{bg} {fg} {className} rounded-lg py-0.5 px-1 inline-block truncate min-w-0 max-w-full align-middle my-[1.5px]">
|
<div
|
||||||
|
class="{bg} {fg} {className} rounded-lg py-0.5 px-1 inline-block truncate min-w-0 max-w-full align-middle my-[1.5px]"
|
||||||
|
>
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Regular → Executable
+18
-46
@@ -1,24 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import {
|
import { ArrowRight, Ban, FileIcon, Folder, Play } from 'lucide-svelte';
|
||||||
ArrowRight,
|
|
||||||
Ban,
|
|
||||||
FileIcon,
|
|
||||||
Folder,
|
|
||||||
Play,
|
|
||||||
RefreshCcwDot,
|
|
||||||
TriangleAlert
|
|
||||||
} from 'lucide-svelte';
|
|
||||||
import {
|
import {
|
||||||
current_height,
|
current_height,
|
||||||
get_selectable_color_classes,
|
get_selectable_color_classes,
|
||||||
get_shifted_color
|
get_shifted_color
|
||||||
} from '../ts/stores/ui_behavior';
|
} from '../ts/stores/ui_behavior';
|
||||||
import Button from './Button.svelte';
|
import Button from './Button.svelte';
|
||||||
import {
|
import { supported_file_type_icon, type Inode, get_file_primary_key } from '../ts/types';
|
||||||
supported_file_type_icon,
|
|
||||||
type FolderElement,
|
|
||||||
type SupportedFileType
|
|
||||||
} from '../ts/types';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
is_selected,
|
is_selected,
|
||||||
@@ -27,39 +15,23 @@
|
|||||||
selected_file_ids
|
selected_file_ids
|
||||||
} from '../ts/stores/select';
|
} from '../ts/stores/select';
|
||||||
import {
|
import {
|
||||||
all_files,
|
|
||||||
change_file_path,
|
change_file_path,
|
||||||
current_file_path,
|
current_file_path,
|
||||||
get_display_ids_where_file_is_missing
|
get_missing_colliding_display_ids
|
||||||
} from '../ts/stores/files';
|
} from '../ts/stores/files';
|
||||||
import RefreshPlay from './RefreshPlay.svelte';
|
import RefreshPlay from './RefreshPlay.svelte';
|
||||||
import { get_file_size_display_string, get_file_type } from '../ts/utils';
|
import { get_file_size_display_string, get_file_type } from '../ts/utils';
|
||||||
import { open_file } from '../ts/api_handler';
|
import { open_file } from '../ts/api_handler';
|
||||||
import {
|
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||||
displays,
|
|
||||||
get_display_by_id,
|
|
||||||
run_on_all_selected_displays,
|
|
||||||
update_screenshot
|
|
||||||
} from '../ts/stores/displays';
|
|
||||||
import { get_thumbnail_url } from '../ts/stores/thumbnails';
|
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';
|
import { liveQuery } from 'dexie';
|
||||||
|
|
||||||
let { file, not_interactable = false } = $props<{
|
let { file, not_interactable = false }: { file: Inode; not_interactable?: boolean } = $props();
|
||||||
file: FolderElement;
|
|
||||||
not_interactable?: boolean;
|
|
||||||
}>();
|
|
||||||
|
|
||||||
let thumbnail_url: string | null = $state(null);
|
let missing_colliding_displays_ids = liveQuery(() =>
|
||||||
// Update thumbnail_url automatically if data is available
|
get_missing_colliding_display_ids(file, $selected_display_ids)
|
||||||
const subscription = liveQuery(() => db.thumbnail_blobs.get(file.hash)).subscribe({
|
);
|
||||||
next: async () => {
|
let thumbnail_url = liveQuery(() => get_thumbnail_url(file));
|
||||||
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';
|
||||||
|
|
||||||
@@ -106,13 +78,13 @@
|
|||||||
|
|
||||||
function onclick(e: Event) {
|
function onclick(e: Event) {
|
||||||
if (not_interactable) return;
|
if (not_interactable) return;
|
||||||
select(selected_file_ids, file.id);
|
select(selected_file_ids, get_file_primary_key(file), 'toggle');
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function open() {
|
async function open() {
|
||||||
if (is_folder) {
|
if (is_folder) {
|
||||||
change_file_path($current_file_path + file.name + '/');
|
await change_file_path($current_file_path + file.name + '/');
|
||||||
} else {
|
} else {
|
||||||
const path_to_file = $current_file_path + file.name;
|
const path_to_file = $current_file_path + file.name;
|
||||||
await run_on_all_selected_displays(open_file, true, path_to_file);
|
await run_on_all_selected_displays(open_file, true, path_to_file);
|
||||||
@@ -158,7 +130,7 @@
|
|||||||
>
|
>
|
||||||
{#if is_folder}
|
{#if is_folder}
|
||||||
<ArrowRight class="size-full" strokeWidth="3" />
|
<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}
|
{:else if $missing_colliding_displays_ids && $missing_colliding_displays_ids.missing.length !== 0}
|
||||||
<RefreshPlay className="size-full" />
|
<RefreshPlay className="size-full" />
|
||||||
{:else if get_file_type(file) !== null}
|
{:else if get_file_type(file) !== null}
|
||||||
<Play class="size-full" strokeWidth="3" />
|
<Play class="size-full" strokeWidth="3" />
|
||||||
@@ -176,7 +148,7 @@
|
|||||||
}}
|
}}
|
||||||
{onclick}
|
{onclick}
|
||||||
class="{get_selectable_color_classes(
|
class="{get_selectable_color_classes(
|
||||||
!not_interactable && is_selected(file.id, $selected_file_ids),
|
!not_interactable && is_selected(get_file_primary_key(file), $selected_file_ids),
|
||||||
{
|
{
|
||||||
bg: true,
|
bg: true,
|
||||||
hover: !not_interactable,
|
hover: !not_interactable,
|
||||||
@@ -191,9 +163,9 @@
|
|||||||
<div class="aspect-square rounded-md flex justify-center items-center">
|
<div class="aspect-square rounded-md flex justify-center items-center">
|
||||||
{#if is_folder}
|
{#if is_folder}
|
||||||
<Folder class="size-full p-2" />
|
<Folder class="size-full p-2" />
|
||||||
{:else if thumbnail_url}
|
{:else if $thumbnail_url || null}
|
||||||
<img
|
<img
|
||||||
src={thumbnail_url}
|
src={$thumbnail_url || null}
|
||||||
alt="file_thumbnail"
|
alt="file_thumbnail"
|
||||||
class="object-contain size-full select-none block p-1 rounded-lg"
|
class="object-contain size-full select-none block p-1 rounded-lg"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
@@ -213,7 +185,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class=" p-1 flex flex-row items-center gap-1 pr-1 {get_grayed_out_text_color_strings(
|
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)
|
is_selected(get_file_primary_key(file), $selected_file_ids)
|
||||||
)} duration-200 transition-colors"
|
)} duration-200 transition-colors"
|
||||||
>
|
>
|
||||||
<!-- {#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0}
|
<!-- {#if get_display_ids_where_file_is_missing($current_file_path, file, $selected_display_ids, $all_files)[1].length !== 0}
|
||||||
@@ -251,7 +223,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||||
is_selected(file.id, $selected_file_ids)
|
is_selected(get_file_primary_key(file), $selected_file_ids)
|
||||||
)} duration-200 transition-colors my-1"
|
)} duration-200 transition-colors my-1"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
@@ -262,7 +234,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="h-[70%] border {get_grayed_out_border_color_strings(
|
class="h-[70%] border {get_grayed_out_border_color_strings(
|
||||||
is_selected(file.id, $selected_file_ids)
|
is_selected(get_file_primary_key(file), $selected_file_ids)
|
||||||
)} duration-200 transition-colors"
|
)} duration-200 transition-colors"
|
||||||
></div>
|
></div>
|
||||||
<div
|
<div
|
||||||
@@ -3,8 +3,7 @@
|
|||||||
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
|
import { get_selectable_color_classes } from '../ts/stores/ui_behavior';
|
||||||
import key_map_json from './../../../../shared/keys.json';
|
import key_map_json from './../../../../shared/keys.json';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import { selected_display_ids } from '../ts/stores/select';
|
import { run_on_all_selected_displays } from '../ts/stores/displays';
|
||||||
import { displays, get_display_by_id, run_on_all_selected_displays } from '../ts/stores/displays';
|
|
||||||
import { send_keyboard_input } from '../ts/api_handler';
|
import { send_keyboard_input } from '../ts/api_handler';
|
||||||
|
|
||||||
const bg = 'bg-stone-700';
|
const bg = 'bg-stone-700';
|
||||||
@@ -64,7 +63,11 @@
|
|||||||
{active ? 'Erfassung aktiv' : 'Hier für Erfassung klicken'}
|
{active ? 'Erfassung aktiv' : 'Hier für Erfassung klicken'}
|
||||||
<div class="absolute top-full left-0 ml-1 mt-0.5 flex flex-col-reverse text-sm text-stone-400">
|
<div class="absolute top-full left-0 ml-1 mt-0.5 flex flex-col-reverse text-sm text-stone-400">
|
||||||
{#each last_keys as key (key.id)}
|
{#each last_keys as key (key.id)}
|
||||||
<span animate:flip={{ duration: 200 }} in:fade={{ duration: 200 }} out:fade={{ duration: 500 }} >{key.key}</span>
|
<span
|
||||||
|
animate:flip={{ duration: 200 }}
|
||||||
|
in:fade={{ duration: 200 }}
|
||||||
|
out:fade={{ duration: 500 }}>{key.key}</span
|
||||||
|
>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,16 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { type DisplayStatus } from "../ts/types";
|
import { type DisplayStatus } from '../ts/types';
|
||||||
import { display_status_to_info } from "../ts/utils";
|
import { display_status_to_info } from '../ts/utils';
|
||||||
|
|
||||||
let { selected, status, className = "" } = $props<{
|
let {
|
||||||
|
selected,
|
||||||
|
status,
|
||||||
|
className = ''
|
||||||
|
}: {
|
||||||
selected: boolean;
|
selected: boolean;
|
||||||
status: DisplayStatus;
|
status: DisplayStatus;
|
||||||
className?: string;
|
className?: string;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
function get_text_color(selected: boolean, status: DisplayStatus) {
|
function get_text_color(selected: boolean, status: DisplayStatus) {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
|
|||||||
@@ -8,9 +8,11 @@
|
|||||||
import { flip } from 'svelte/animate';
|
import { flip } from 'svelte/animate';
|
||||||
import { cubicOut } from 'svelte/easing';
|
import { cubicOut } from 'svelte/easing';
|
||||||
|
|
||||||
let { bg = 'bg-stone-700' } = $props<{
|
let {
|
||||||
|
bg = 'bg-stone-700'
|
||||||
|
}: {
|
||||||
bg?: string;
|
bg?: string;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
let outside_container: HTMLDivElement;
|
let outside_container: HTMLDivElement;
|
||||||
let inside_container: HTMLDivElement;
|
let inside_container: HTMLDivElement;
|
||||||
@@ -79,21 +81,21 @@
|
|||||||
out.push({
|
out.push({
|
||||||
name: ' '.repeat(i) + hidden_folders[i],
|
name: ' '.repeat(i) + hidden_folders[i],
|
||||||
class: 'truncate max-w-80',
|
class: 'truncate max-w-80',
|
||||||
on_select: () => {
|
on_select: async () => {
|
||||||
open_path(i + 1, path);
|
await open_path(i + 1, path);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
function open_path(index_of_all_folders: number, path: string) {
|
async function open_path(index_of_all_folders: number, path: string) {
|
||||||
let new_path = '/';
|
let new_path = '/';
|
||||||
const all_folders = get_folders(path);
|
const all_folders = get_folders(path);
|
||||||
for (let i = 0; i < index_of_all_folders; i++) {
|
for (let i = 0; i < index_of_all_folders; i++) {
|
||||||
new_path += all_folders[i] + '/';
|
new_path += all_folders[i] + '/';
|
||||||
}
|
}
|
||||||
change_file_path(new_path);
|
await change_file_path(new_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
@@ -113,11 +115,14 @@
|
|||||||
<Button
|
<Button
|
||||||
className="py-1 shrink-0 grow-0 w-10"
|
className="py-1 shrink-0 grow-0 w-10"
|
||||||
{bg}
|
{bg}
|
||||||
click_function={(e) => {
|
click_function={async () => {
|
||||||
open_path(0, $current_file_path);
|
await open_path(0, $current_file_path);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<House class="size-full transition-all duration-100" strokeWidth={$current_file_path === '/' ? 2.7 : 2}/>
|
<House
|
||||||
|
class="size-full transition-all duration-100"
|
||||||
|
strokeWidth={$current_file_path === '/' ? 2.7 : 2}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
{#if cut_folders !== 0}
|
{#if cut_folders !== 0}
|
||||||
@@ -144,8 +149,8 @@
|
|||||||
? 'max-w-80 font-bold'
|
? 'max-w-80 font-bold'
|
||||||
: 'max-w-30'}"
|
: 'max-w-30'}"
|
||||||
{bg}
|
{bg}
|
||||||
click_function={(e) => {
|
click_function={async () => {
|
||||||
open_path(cut_folders + i + 1, $current_file_path);
|
await open_path(cut_folders + i + 1, $current_file_path);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ChevronRight class="shrink-0 text-stone-500 h-full" />
|
<ChevronRight class="shrink-0 text-stone-500 h-full" />
|
||||||
|
|||||||
@@ -10,12 +10,12 @@
|
|||||||
close_function,
|
close_function,
|
||||||
className = '',
|
className = '',
|
||||||
snippet_container_class = ''
|
snippet_container_class = ''
|
||||||
} = $props<{
|
}: {
|
||||||
content: PopupContent;
|
content: PopupContent;
|
||||||
close_function: () => void;
|
close_function: () => void;
|
||||||
className?: string;
|
className?: string;
|
||||||
snippet_container_class?: string;
|
snippet_container_class?: string;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
function try_to_close() {
|
function try_to_close() {
|
||||||
if (!content.closable || !content.open) return;
|
if (!content.closable || !content.open) return;
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
let {className = ''} = $props<{className?: string}>();
|
let { className = '' }: { className?: string } = $props();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svg
|
<svg
|
||||||
|
|||||||
@@ -16,28 +16,28 @@
|
|||||||
extension = null,
|
extension = null,
|
||||||
enter_mode = 'none',
|
enter_mode = 'none',
|
||||||
enter_function = null
|
enter_function = null
|
||||||
} = $props<{
|
}: {
|
||||||
current_value: string;
|
current_value: string;
|
||||||
current_valid: boolean;
|
current_valid: boolean;
|
||||||
className?: string;
|
className?: string;
|
||||||
bg?: string;
|
bg?: string;
|
||||||
title: string;
|
title: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
is_valid_function?: ((input: string) => [boolean, string]) | null;
|
is_valid_function?: ((input: string) => [boolean, string] | Promise<[boolean, string]>) | null;
|
||||||
focused_on_start?: boolean;
|
focused_on_start?: boolean;
|
||||||
extension?: string | null;
|
extension?: string | null;
|
||||||
enter_mode?: 'none' | 'focus_next' | 'submit';
|
enter_mode?: 'none' | 'focus_next' | 'submit';
|
||||||
enter_function?: (() => void) | null;
|
enter_function?: (() => void) | null;
|
||||||
}>();
|
} = $props();
|
||||||
|
|
||||||
let focus_bg = get_shifted_color(bg, 100);
|
let focus_bg = get_shifted_color(bg, 100);
|
||||||
let focused: boolean = $state(false);
|
let focused: boolean = $state(false);
|
||||||
let current_info = $state('');
|
let current_info = $state('');
|
||||||
let input_element: HTMLInputElement;
|
let input_element: HTMLInputElement;
|
||||||
|
|
||||||
function validate_input() {
|
async function validate_input() {
|
||||||
if (!is_valid_function) return;
|
if (!is_valid_function) return;
|
||||||
[current_valid, current_info] = is_valid_function(current_value.trim());
|
[current_valid, current_info] = await is_valid_function(current_value.trim());
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_highlighting_string(): string {
|
function get_highlighting_string(): string {
|
||||||
@@ -72,12 +72,12 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onMount(() => {
|
onMount(async () => {
|
||||||
validate_input();
|
await validate_input();
|
||||||
if (focused_on_start && input_element) input_element.focus();
|
if (focused_on_start && input_element) input_element.focus();
|
||||||
|
|
||||||
selected_display_ids.subscribe(() => {
|
selected_display_ids.subscribe(async () => {
|
||||||
validate_input();
|
await validate_input();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -114,11 +114,11 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function show_text() {
|
async function show_text() {
|
||||||
const html =
|
const html =
|
||||||
editor_state.editor?.getHTML() +
|
editor_state.editor?.getHTML() +
|
||||||
`<style>:root {--background-color: ${color_states.bg.value} !important;}
|
`<style>:root {--background-color: ${color_states.bg.value} !important;}
|
||||||
</style>`;
|
</style>`;
|
||||||
await run_on_all_selected_displays(show_html, true, html);
|
await run_on_all_selected_displays(show_html, true, html);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1,2 @@
|
|||||||
export const prerender = true;
|
export const prerender = true;
|
||||||
|
export const ssr = false;
|
||||||
@@ -10,7 +10,6 @@
|
|||||||
import TextInput from '../components/TextInput.svelte';
|
import TextInput from '../components/TextInput.svelte';
|
||||||
import {
|
import {
|
||||||
add_display,
|
add_display,
|
||||||
displays,
|
|
||||||
edit_display_data,
|
edit_display_data,
|
||||||
get_display_by_id,
|
get_display_by_id,
|
||||||
is_display_name_taken,
|
is_display_name_taken,
|
||||||
@@ -35,6 +34,7 @@
|
|||||||
title_class: '!text-xl',
|
title_class: '!text-xl',
|
||||||
closable: true
|
closable: true
|
||||||
});
|
});
|
||||||
|
let remove_display_name = $state('');
|
||||||
|
|
||||||
const text_inputs_valid_null_values = {
|
const text_inputs_valid_null_values = {
|
||||||
name: { valid: false, value: '' },
|
name: { valid: false, value: '' },
|
||||||
@@ -81,7 +81,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const show_remove_display_popup = (display_id: string) => {
|
const show_remove_display_popup = async (display_id: string) => {
|
||||||
|
remove_display_name = (await get_display_by_id(display_id))?.name || '?';
|
||||||
popup_content = {
|
popup_content = {
|
||||||
open: true,
|
open: true,
|
||||||
snippet: remove_display_popup,
|
snippet: remove_display_popup,
|
||||||
@@ -93,8 +94,8 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const show_edit_display_popup = (display_id: string) => {
|
const show_edit_display_popup = async (display_id: string) => {
|
||||||
const display = get_display_by_id(display_id, $displays);
|
const display = await get_display_by_id(display_id);
|
||||||
if (!display) return;
|
if (!display) return;
|
||||||
// insert existing values in text_inputs_valid
|
// insert existing values in text_inputs_valid
|
||||||
for (const key of Object.keys(text_inputs_valid) as (keyof typeof text_inputs_valid)[]) {
|
for (const key of Object.keys(text_inputs_valid) as (keyof typeof text_inputs_valid)[]) {
|
||||||
@@ -112,17 +113,14 @@
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(on_start);
|
||||||
on_start();
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#snippet remove_display_popup(display_id: string)}
|
{#snippet remove_display_popup(display_id: string)}
|
||||||
<div class="max-w-prose px-2">
|
<div class="max-w-prose px-2">
|
||||||
Soll der Bildschirm <HighlightedText
|
Soll der Bildschirm <HighlightedText>{remove_display_name}</HighlightedText> wirklich gelöscht werden?
|
||||||
>{get_display_by_id(display_id, $displays)?.name || '?'}</HighlightedText
|
Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation auf dem Gerät bleibt
|
||||||
> wirklich gelöscht werden? Dadurch wird es von diesem Controller nicht mehr erreichbar. Die Installation
|
bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
|
||||||
auf dem Gerät bleibt bestehen. Mit dem erneuten Hinzufügen des Bildschirms wird er wieder steuerbar.
|
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-row justify-end gap-2">
|
<div class="flex flex-row justify-end gap-2">
|
||||||
<Button className="px-4 font-bold" click_function={popup_close_function}>Abbrechen</Button>
|
<Button className="px-4 font-bold" click_function={popup_close_function}>Abbrechen</Button>
|
||||||
@@ -130,8 +128,8 @@
|
|||||||
hover_bg="bg-red-400"
|
hover_bg="bg-red-400"
|
||||||
active_bg="bg-red-500"
|
active_bg="bg-red-500"
|
||||||
className="px-4 flex text-red-400 hover:text-stone-100"
|
className="px-4 flex text-red-400 hover:text-stone-100"
|
||||||
click_function={() => {
|
click_function={async () => {
|
||||||
remove_display(display_id);
|
await remove_display(display_id);
|
||||||
popup_close_function();
|
popup_close_function();
|
||||||
}}>Löschen</Button
|
}}>Löschen</Button
|
||||||
>
|
>
|
||||||
@@ -145,13 +143,13 @@
|
|||||||
bind:current_valid={text_inputs_valid.name.valid}
|
bind:current_valid={text_inputs_valid.name.valid}
|
||||||
title="Anzeigename"
|
title="Anzeigename"
|
||||||
placeholder="z.B. Beamer vorne links"
|
placeholder="z.B. Beamer vorne links"
|
||||||
is_valid_function={(input: string) => {
|
is_valid_function={async (input: string) => {
|
||||||
if (!!existing_display_id) {
|
if (!!existing_display_id) {
|
||||||
if (input === get_display_by_id(existing_display_id, $displays)?.name)
|
if (input === (await get_display_by_id(existing_display_id))?.name)
|
||||||
return [true, 'Gültiger Name'];
|
return [true, 'Gültiger Name'];
|
||||||
}
|
}
|
||||||
if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
|
if (input.length === 0 || input.length > 50) return [false, 'Ungültige Länge'];
|
||||||
if (is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
if (await is_display_name_taken(input)) return [false, 'Name bereits verwendet'];
|
||||||
return [true, 'Gültiger Name'];
|
return [true, 'Gültiger Name'];
|
||||||
}}
|
}}
|
||||||
enter_mode="focus_next"
|
enter_mode="focus_next"
|
||||||
@@ -199,8 +197,8 @@
|
|||||||
: [false, 'Ungültige MAC-Adresse'];
|
: [false, 'Ungültige MAC-Adresse'];
|
||||||
}}
|
}}
|
||||||
enter_mode="submit"
|
enter_mode="submit"
|
||||||
enter_function={() => {
|
enter_function={async () => {
|
||||||
finalize_add_edit_display(existing_display_id);
|
await finalize_add_edit_display(existing_display_id);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div class="flex flex-row gap-2 justify-end pt-2">
|
<div class="flex flex-row gap-2 justify-end pt-2">
|
||||||
@@ -212,8 +210,8 @@
|
|||||||
disabled={!all_text_inputs_valid()}
|
disabled={!all_text_inputs_valid()}
|
||||||
className="{!!existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
|
className="{!!existing_display_id ? 'px-4' : 'pl-3 pr-4 gap-2'} font-bold"
|
||||||
bg="bg-stone-650"
|
bg="bg-stone-650"
|
||||||
click_function={() => {
|
click_function={async () => {
|
||||||
finalize_add_edit_display(existing_display_id);
|
await finalize_add_edit_display(existing_display_id);
|
||||||
}}
|
}}
|
||||||
>{#if !!existing_display_id}
|
>{#if !!existing_display_id}
|
||||||
Speichern
|
Speichern
|
||||||
|
|||||||
Regular → Executable
+110
-53
@@ -1,6 +1,12 @@
|
|||||||
import { notifications } from "./stores/notification";
|
import { notifications } from './stores/notification';
|
||||||
import { to_display_status, type DisplayStatus, type FolderElement, type RequestResponse, type ShellCommandResponse, type TreeElement } from "./types";
|
import {
|
||||||
import { get_uuid } from "./utils";
|
to_display_status,
|
||||||
|
type DisplayStatus,
|
||||||
|
type Inode,
|
||||||
|
type RequestResponse,
|
||||||
|
type ShellCommandResponse,
|
||||||
|
type TreeElement
|
||||||
|
} from './types';
|
||||||
|
|
||||||
export async function get_screenshot(ip: string): Promise<Blob | null> {
|
export async function get_screenshot(ip: string): Promise<Blob | null> {
|
||||||
const options = { method: 'PATCH' };
|
const options = { method: 'PATCH' };
|
||||||
@@ -19,8 +25,8 @@ export async function send_keyboard_input(ip: string, key: string): Promise<void
|
|||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
key: key,
|
key: key
|
||||||
}),
|
})
|
||||||
};
|
};
|
||||||
await request_display(ip, '/keyboardInput', options);
|
await request_display(ip, '/keyboardInput', options);
|
||||||
}
|
}
|
||||||
@@ -36,7 +42,10 @@ export async function show_html(ip: string, html: string) {
|
|||||||
await request_display(ip, '/showHTML', options);
|
await request_display(ip, '/showHTML', options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_file_data(ip: string, path: string): Promise<FolderElement[] | null> {
|
export async function get_file_data(
|
||||||
|
ip: string,
|
||||||
|
path: string
|
||||||
|
): Promise<{ folder_element: Inode; date_created: Date }[] | null> {
|
||||||
interface FileInfo {
|
interface FileInfo {
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
type: string;
|
||||||
@@ -52,54 +61,54 @@ export async function get_file_data(ip: string, path: string): Promise<FolderEle
|
|||||||
'{name:$name, type:$type, size:($size|tostring), created:$created}' | tr -d '\n'
|
'{name:$name, type:$type, size:($size|tostring), created:$created}' | tr -d '\n'
|
||||||
echo
|
echo
|
||||||
done
|
done
|
||||||
`
|
`;
|
||||||
const raw_response = await run_shell_command(ip, command, true);
|
const raw_response = await run_shell_command(ip, command, true);
|
||||||
if (!raw_response.ok || !raw_response.json) return null;
|
if (!raw_response.ok || !raw_response.json) return null;
|
||||||
const json_response = raw_response.json as ShellCommandResponse;
|
const json_response = raw_response.json as ShellCommandResponse;
|
||||||
if (json_response.exitCode === 0 && json_response.stdout.trim() === '') return [];
|
if (json_response.exitCode === 0 && json_response.stdout.trim() === '') return [];
|
||||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||||
|
|
||||||
const response: FileInfo[] = json_response.stdout.trim()
|
const response: FileInfo[] = json_response.stdout
|
||||||
.split("\n")
|
.trim()
|
||||||
|
.split('\n')
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.map((line: string) => JSON.parse(line) as FileInfo);
|
.map((line: string) => JSON.parse(line) as FileInfo);
|
||||||
|
|
||||||
const folder_element_list: FolderElement[] = [];
|
const folder_element_list: { folder_element: Inode; date_created: Date }[] = [];
|
||||||
|
|
||||||
for (const response_element of response) {
|
for (const response_element of response) {
|
||||||
// filter hidden files (start with '.' -> './.config')
|
// filter hidden files (start with '.' -> './.config')
|
||||||
if (response_element.name.charAt(2) !== '.') {
|
if (response_element.name.charAt(2) === '.') continue;
|
||||||
const truncated = {
|
const folder_element: Inode = {
|
||||||
...response_element,
|
path: path,
|
||||||
created: response_element.created.slice(0, 16) // truncated to YYYY-MM-DD hh-mm -> no (milli)seconds
|
|
||||||
};
|
|
||||||
|
|
||||||
const folder_element: FolderElement = {
|
|
||||||
id: get_uuid(),
|
|
||||||
hash: JSON.stringify(truncated),
|
|
||||||
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),
|
|
||||||
size: Number(response_element.size),
|
size: Number(response_element.size),
|
||||||
|
thumbnail: null
|
||||||
};
|
};
|
||||||
folder_element_list.push(folder_element);
|
folder_element_list.push({ folder_element, date_created: new Date(response_element.created) });
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return folder_element_list;
|
return folder_element_list;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_file_tree_data(ip: string, path: string): Promise<TreeElement[] | null> {
|
export async function get_file_tree_data(ip: string, path: string): Promise<TreeElement[] | null> {
|
||||||
const command = `cd ".${path}" && tree -Js`
|
const command = `cd ".${path}" && tree -Js`;
|
||||||
const raw_response = await run_shell_command(ip, command, true);
|
const raw_response = await run_shell_command(ip, command, true);
|
||||||
|
|
||||||
if (!raw_response.ok || !raw_response.json) return null;
|
if (!raw_response.ok || !raw_response.json) return null;
|
||||||
const json_response = raw_response.json as ShellCommandResponse;
|
const json_response = raw_response.json as ShellCommandResponse;
|
||||||
if (handle_shell_error(ip, json_response, command, true)) return null;
|
if (handle_shell_error(ip, json_response, command, true)) return null;
|
||||||
|
|
||||||
return (JSON.parse(json_response.stdout.trim()) as [TreeElement, any])[0].contents || null;
|
const tree_element: TreeElement | null = JSON.parse(json_response.stdout.trim())[0] || null;
|
||||||
|
|
||||||
|
return tree_element?.contents || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function create_folders(ip: string, path: string, folder_names: string[]): Promise<void> {
|
export async function create_folders(
|
||||||
|
ip: string,
|
||||||
|
path: string,
|
||||||
|
folder_names: string[]
|
||||||
|
): Promise<void> {
|
||||||
let command = `cd ".${path}"`;
|
let command = `cd ".${path}"`;
|
||||||
|
|
||||||
for (const part of folder_names) {
|
for (const part of folder_names) {
|
||||||
@@ -112,7 +121,12 @@ export async function create_folders(ip: string, path: string, folder_names: str
|
|||||||
handle_shell_error(ip, json_response, command, true);
|
handle_shell_error(ip, json_response, command, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function rename_file(ip: string, path: string, old_file_name: string, new_file_name: string): Promise<void> {
|
export async function rename_file(
|
||||||
|
ip: string,
|
||||||
|
path: string,
|
||||||
|
old_file_name: string,
|
||||||
|
new_file_name: string
|
||||||
|
): Promise<void> {
|
||||||
const command: string = `cd ".${path}" && mv "${old_file_name}" "${new_file_name}"`;
|
const command: string = `cd ".${path}" && mv "${old_file_name}" "${new_file_name}"`;
|
||||||
|
|
||||||
const raw_response = await run_shell_command(ip, command);
|
const raw_response = await run_shell_command(ip, command);
|
||||||
@@ -121,7 +135,11 @@ export async function rename_file(ip: string, path: string, old_file_name: strin
|
|||||||
handle_shell_error(ip, json_response, command, true);
|
handle_shell_error(ip, json_response, command, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function delete_files(ip: string, current_path: string, file_names: string[]): Promise<void> {
|
export async function delete_files(
|
||||||
|
ip: string,
|
||||||
|
current_path: string,
|
||||||
|
file_names: string[]
|
||||||
|
): Promise<void> {
|
||||||
let command: string = `cd ".${current_path}"`;
|
let command: string = `cd ".${current_path}"`;
|
||||||
for (const file_name of file_names) {
|
for (const file_name of file_names) {
|
||||||
command += ` && rm -r "${file_name}"`;
|
command += ` && rm -r "${file_name}"`;
|
||||||
@@ -132,7 +150,6 @@ export async function delete_files(ip: string, current_path: string, file_names:
|
|||||||
handle_shell_error(ip, json_response, command, true);
|
handle_shell_error(ip, json_response, command, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function show_blackscreen(ip: string): Promise<void> {
|
export async function show_blackscreen(ip: string): Promise<void> {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
@@ -145,31 +162,53 @@ export async function show_blackscreen(ip: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function get_thumbnail_blob(ip: string, path_to_file: string): Promise<Blob | 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' }, true, [415]);
|
const raw_response = await request_display(
|
||||||
if (!raw_response.ok || !raw_response.blob) return null
|
ip,
|
||||||
|
`/file/preview${path_to_file}`,
|
||||||
|
{ method: 'GET' },
|
||||||
|
true,
|
||||||
|
[415]
|
||||||
|
);
|
||||||
|
if (!raw_response.ok || !raw_response.blob) return null;
|
||||||
return raw_response.blob;
|
return raw_response.blob;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function ping_ip(ip: string): Promise<DisplayStatus> {
|
export async function ping_ip(ip: string): Promise<DisplayStatus> {
|
||||||
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
const raw_response = await request_control(`/ping?ip=${ip}`, { method: 'GET' });
|
||||||
if (!raw_response.ok || !raw_response.json) return null;
|
if (!raw_response.ok || !raw_response.json) return null;
|
||||||
return raw_response.json.status ? to_display_status(raw_response.json.status) : null;
|
|
||||||
|
const status = raw_response.json.status;
|
||||||
|
if (typeof status === 'string') {
|
||||||
|
return to_display_status(status);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function request_display(
|
||||||
|
ip: string,
|
||||||
async function request_display(ip: string, api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise<RequestResponse> {
|
api_route: string,
|
||||||
|
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||||
|
log_in_debug: boolean = false,
|
||||||
|
supress_error_handling_http_codes: number[] = []
|
||||||
|
): Promise<RequestResponse> {
|
||||||
const url = `http://${ip}:1323/api${api_route}`;
|
const url = `http://${ip}:1323/api${api_route}`;
|
||||||
return await request(url, options, log_in_debug, supress_error_handling_http_codes);
|
return await request(url, options, log_in_debug, supress_error_handling_http_codes);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request_control(api_route: string, options: { method: string, headers?: Record<string, string>, body?: any }): Promise<RequestResponse> {
|
async function request_control(
|
||||||
|
api_route: string,
|
||||||
|
options: { method: string; headers?: Record<string, string>; body?: string }
|
||||||
|
): Promise<RequestResponse> {
|
||||||
const url = `${window.location.origin}/api${api_route}`;
|
const url = `${window.location.origin}/api${api_route}`;
|
||||||
return await request(url, options);
|
return await request(url, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function request(
|
||||||
async function request(url: string, options: { method: string, headers?: Record<string, string>, body?: any }, log_in_debug: boolean = false, supress_error_handling_http_codes: number[] = []): Promise<RequestResponse> {
|
url: string,
|
||||||
|
options: { method: string; headers?: Record<string, string>; body?: string },
|
||||||
|
log_in_debug: boolean = false,
|
||||||
|
supress_error_handling_http_codes: number[] = []
|
||||||
|
): Promise<RequestResponse> {
|
||||||
try {
|
try {
|
||||||
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
const cache_buster = `${url.includes('?') ? '&' : '?'}=${Date.now()}`;
|
||||||
if (log_in_debug) {
|
if (log_in_debug) {
|
||||||
@@ -179,13 +218,13 @@ async function request(url: string, options: { method: string, headers?: Record<
|
|||||||
}
|
}
|
||||||
const response = await fetch(url + cache_buster, options);
|
const response = await fetch(url + cache_buster, options);
|
||||||
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
|
if (response.ok || supress_error_handling_http_codes.includes(response.status)) {
|
||||||
const contentType = response.headers.get("content-type") || "";
|
const contentType = response.headers.get('content-type') || '';
|
||||||
let request_response: RequestResponse;
|
let request_response: RequestResponse;
|
||||||
if (!contentType.includes("application/json")) {
|
if (!contentType.includes('application/json')) {
|
||||||
const blob: Blob = await response.blob();
|
const blob: Blob = await response.blob();
|
||||||
request_response = { ok: response.ok, http_code: response.status, blob: blob };
|
request_response = { ok: response.ok, http_code: response.status, blob: blob };
|
||||||
} else {
|
} else {
|
||||||
const json: Object = await response.json();
|
const json: Record<string, unknown> = await response.json();
|
||||||
request_response = { ok: response.ok, http_code: response.status, json: json };
|
request_response = { ok: response.ok, http_code: response.status, json: json };
|
||||||
}
|
}
|
||||||
if (log_in_debug) {
|
if (log_in_debug) {
|
||||||
@@ -200,40 +239,58 @@ async function request(url: string, options: { method: string, headers?: Record<
|
|||||||
try {
|
try {
|
||||||
const json: { description: string } = await response.json();
|
const json: { description: string } = await response.json();
|
||||||
error_description = json.description;
|
error_description = json.description;
|
||||||
} catch (error_on_json_parsing: any) {
|
} catch (error_on_json_parsing: unknown) {
|
||||||
error_description = `unknown error: ${error_on_json_parsing}`;
|
error_description = `unknown error: ${error_on_json_parsing}`;
|
||||||
}
|
}
|
||||||
console.error(url, error_description);
|
console.error(url, error_description);
|
||||||
notifications.push("error", `Fehler ${response.status} bei API-Anfrage`, `${url}\nFehler: ${error_description}`);
|
notifications.push(
|
||||||
} catch (error: any) {
|
'error',
|
||||||
|
`Fehler ${response.status} bei API-Anfrage`,
|
||||||
|
`${url}\nFehler: ${error_description}`
|
||||||
|
);
|
||||||
|
} catch (error: unknown) {
|
||||||
if (error instanceof TypeError && /fetch|NetworkError/i.test(error.message)) {
|
if (error instanceof TypeError && /fetch|NetworkError/i.test(error.message)) {
|
||||||
console.log("Request failed - Is the targeted device online?")
|
console.log('Request failed - Is the targeted device online?');
|
||||||
} else {
|
} else {
|
||||||
console.error(url, error);
|
console.error(url, error);
|
||||||
notifications.push("error", `Fataler Fehler bei API-Anfrage`, `${url}\nFehler: ${error}`);
|
notifications.push('error', `Fataler Fehler bei API-Anfrage`, `${url}\nFehler: ${error}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { ok: false };
|
return { ok: false };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handle_shell_error(
|
||||||
|
ip: string,
|
||||||
|
shell_response: ShellCommandResponse,
|
||||||
function handle_shell_error(ip: string, shell_response: ShellCommandResponse, shell_command: string, command_includs_cd: boolean): boolean {
|
shell_command: string,
|
||||||
|
command_includs_cd: boolean
|
||||||
|
): boolean {
|
||||||
if (shell_response.exitCode !== 0) {
|
if (shell_response.exitCode !== 0) {
|
||||||
if (command_includs_cd && shell_response.stderr && /bash: line \d+: cd: .+: No such file or directory/.test(shell_response.stderr)) {
|
if (
|
||||||
console.log("current file_path does not exist on display:", ip);
|
command_includs_cd &&
|
||||||
|
shell_response.stderr &&
|
||||||
|
/bash: line \d+: cd: .+: No such file or directory/.test(shell_response.stderr)
|
||||||
|
) {
|
||||||
|
console.log('current file_path does not exist on display:', ip);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
console.error(shell_response);
|
console.error(shell_response);
|
||||||
notifications.push("error", `Fehler ${shell_response.exitCode} in API-Shell`, `${ip}\n${shell_command}\nFehler: ${shell_response.stderr}`);
|
notifications.push(
|
||||||
|
'error',
|
||||||
|
`Fehler ${shell_response.exitCode} in API-Shell`,
|
||||||
|
`${ip}\n${shell_command}\nFehler: ${shell_response.stderr}`
|
||||||
|
);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (shell_response.stdout.trim() === '') return true;
|
if (shell_response.stdout.trim() === '') return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function run_shell_command(ip: string, command: string, log_in_debug: boolean = false): Promise<RequestResponse> {
|
async function run_shell_command(
|
||||||
|
ip: string,
|
||||||
|
command: string,
|
||||||
|
log_in_debug: boolean = false
|
||||||
|
): Promise<RequestResponse> {
|
||||||
const options = {
|
const options = {
|
||||||
method: 'PATCH',
|
method: 'PATCH',
|
||||||
headers: { 'content-type': 'application/json' },
|
headers: { 'content-type': 'application/json' },
|
||||||
|
|||||||
Executable
+56
@@ -0,0 +1,56 @@
|
|||||||
|
import Dexie, { type Table } from 'dexie';
|
||||||
|
import type { Display, DisplayGroup, Inode } from './types';
|
||||||
|
|
||||||
|
export interface FileOnDisplay {
|
||||||
|
display_id: string;
|
||||||
|
file_primary_key: string; // JSON.stringify([string, string, number, string])
|
||||||
|
date_created: Date;
|
||||||
|
is_loading: boolean;
|
||||||
|
percentage: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class FileDatabase extends Dexie {
|
||||||
|
files!: Table<Inode, [string, string, number, string]>;
|
||||||
|
files_on_display!: Table<FileOnDisplay, [string, string]>;
|
||||||
|
displays!: Table<Display, string>;
|
||||||
|
display_groups!: Table<DisplayGroup, string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super('FileDatabase');
|
||||||
|
|
||||||
|
this.version(1).stores({
|
||||||
|
files: `
|
||||||
|
[path+name+size+type],
|
||||||
|
path,
|
||||||
|
name,
|
||||||
|
size,
|
||||||
|
type,
|
||||||
|
thumbnail
|
||||||
|
`,
|
||||||
|
files_on_display: `
|
||||||
|
[display_id+file_primary_key],
|
||||||
|
display_id,
|
||||||
|
file_primary_key,
|
||||||
|
date_created,
|
||||||
|
is_loading,
|
||||||
|
percentage
|
||||||
|
`,
|
||||||
|
displays: `
|
||||||
|
id,
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
position,
|
||||||
|
preview,
|
||||||
|
group_id,
|
||||||
|
name,
|
||||||
|
status
|
||||||
|
`,
|
||||||
|
display_groups: `
|
||||||
|
id,
|
||||||
|
position
|
||||||
|
`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const db = new FileDatabase();
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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();
|
|
||||||
Regular → Executable
+18
-18
@@ -1,34 +1,34 @@
|
|||||||
import { get } from "svelte/store";
|
import { screenshot_loop } from './stores/displays';
|
||||||
import { displays, run_on_all_selected_displays, update_displays_with_map, update_screenshot } from "./stores/displays"
|
import { ping_ip } from './api_handler';
|
||||||
import { ping_ip } from "./api_handler";
|
import type { Display } from './types';
|
||||||
import type { Display } from "./types";
|
import { update_folder_elements_recursively } from './stores/files';
|
||||||
import { change_file_path, update_folder_elements_recursively } from "./stores/files";
|
import { db } from './files_display.db';
|
||||||
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 db.files.clear();
|
||||||
|
await db.files_on_display.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);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
async function update_all_display_status() {
|
async function update_all_display_status() {
|
||||||
await update_displays_with_map(async (display: Display) => {
|
const all_displays: Display[] = await db.displays.toArray();
|
||||||
|
for (const display of all_displays) {
|
||||||
const new_status = await ping_ip(display.ip);
|
const new_status = await ping_ip(display.ip);
|
||||||
if (new_status === null && display.status !== null) return display;
|
if (new_status === null && display.status !== null) continue;
|
||||||
if (new_status === "app_online" && display.status !== "app_online") {
|
if (new_status === 'app_online' && display.status !== 'app_online') {
|
||||||
await on_display_start(display);
|
on_display_start(display);
|
||||||
|
}
|
||||||
|
if (new_status !== display.status) {
|
||||||
|
display.status = new_status;
|
||||||
|
await db.displays.put(display); // save
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return { ...display, status: new_status, };
|
|
||||||
});
|
|
||||||
console.log("Display Status updated")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
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 screenshot_loop(display.id);
|
||||||
}
|
}
|
||||||
Regular → Executable
+146
-141
@@ -1,54 +1,82 @@
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get } from 'svelte/store';
|
||||||
import type { Display, DisplayGroup, DisplayStatus } from "../types";
|
import type { Display, DisplayGroup, DisplayStatus } from '../types';
|
||||||
import { is_selected, select, selected_display_ids } from "./select";
|
import { is_selected, select, selected_display_ids } from './select';
|
||||||
import { get_uuid, image_content_hash } from "../utils";
|
import { get_uuid, image_content_hash } from '../utils';
|
||||||
import { get_screenshot } from "../api_handler";
|
import { get_screenshot } from '../api_handler';
|
||||||
import { filter_file_selection_for_current_selected_displays } from "./files";
|
import { delete_and_deselect_unique_files_from_display } from './files';
|
||||||
|
import { db } from '../files_display.db';
|
||||||
|
|
||||||
export const displays: Writable<DisplayGroup[]> = writable<DisplayGroup[]>([{
|
export async function is_display_name_taken(name: string): Promise<boolean> {
|
||||||
id: get_uuid(),
|
const exists = await db.displays.where('name').equals(name).first();
|
||||||
data: []
|
return !!exists;
|
||||||
}]);
|
|
||||||
|
|
||||||
|
|
||||||
export function is_display_name_taken(name: string): boolean {
|
|
||||||
const display_groups = get(displays);
|
|
||||||
return display_groups.some(group =>
|
|
||||||
group.data.some(display => display.name.trim().toLowerCase() === name.trim().toLowerCase())
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function add_display(ip: string, mac: string | null, name: string, status: DisplayStatus) {
|
export async function add_display(
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
ip: string,
|
||||||
displays[0].data.push({ id: get_uuid(), ip, preview_url: null, preview_timeout_id: null, mac, name, status });
|
mac: string | null,
|
||||||
return displays;
|
name: string,
|
||||||
|
status: DisplayStatus
|
||||||
|
) {
|
||||||
|
if (await is_display_name_taken(name)) return;
|
||||||
|
const new_id = get_uuid();
|
||||||
|
const group = await db.display_groups.toCollection().first();
|
||||||
|
let group_id: string;
|
||||||
|
if (group) {
|
||||||
|
group_id = group.id;
|
||||||
|
console.log('DISPLAYGROUP WURDE NICHT ERSTELLT');
|
||||||
|
} else {
|
||||||
|
group_id = get_uuid();
|
||||||
|
await db.display_groups.put({ id: group_id, position: 0 });
|
||||||
|
console.log('DISPLAYGROUP WURDE ERSTELLT');
|
||||||
|
}
|
||||||
|
const element_count_in_group = (await db.displays.where('group_id').equals(group_id).toArray())
|
||||||
|
.length;
|
||||||
|
await db.displays.put({
|
||||||
|
id: new_id,
|
||||||
|
ip,
|
||||||
|
mac,
|
||||||
|
position: element_count_in_group,
|
||||||
|
preview: { currently_updating: false, url: null },
|
||||||
|
group_id: group_id,
|
||||||
|
name,
|
||||||
|
status
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function edit_display_data(display_id: string, ip: string, mac: string | null, name: string) {
|
export async function edit_display_data(
|
||||||
await update_displays_with_map((display: Display) => {
|
display_id: string,
|
||||||
if (display.id !== display_id) return display;
|
ip: string,
|
||||||
return { ...display, ip: ip, mac: mac, name: name };
|
mac: string | null,
|
||||||
})
|
name: string
|
||||||
|
) {
|
||||||
|
let display = await db.displays.get(display_id);
|
||||||
|
if (!display) return;
|
||||||
|
display = { ...display, ip: ip, mac: mac, name: name };
|
||||||
|
await db.displays.put(display); // save
|
||||||
}
|
}
|
||||||
|
|
||||||
export function remove_display(display_id: string) {
|
export async function remove_display(display_id: string) {
|
||||||
select(selected_display_ids, display_id, false);
|
select(selected_display_ids, display_id, 'deselect');
|
||||||
filter_file_selection_for_current_selected_displays();
|
await delete_and_deselect_unique_files_from_display(display_id);
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
|
||||||
displays = displays.map(display_group => ({
|
|
||||||
...display_group,
|
|
||||||
data: display_group.data.filter(display => display.id !== display_id)
|
|
||||||
}));
|
|
||||||
return displays;
|
|
||||||
});
|
|
||||||
|
|
||||||
// TODO remove ID from Files usw.
|
const group_id = (await db.displays.get(display_id))?.group_id;
|
||||||
|
await db.displays.delete(display_id);
|
||||||
|
if (group_id && (await db.displays.where('group_id').equals(group_id).toArray()).length === 0) {
|
||||||
|
await db.display_groups.delete(group_id); // delete empty group
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function all_displays_of_group_selected(display_group: DisplayGroup, current_selected_displays: string[]) {
|
export async function all_displays_of_group_selected(
|
||||||
if (display_group.data.length === 0) return false;
|
display_group_id: string,
|
||||||
for (const display of display_group.data) {
|
current_selected_displays: string[]
|
||||||
|
): Promise<boolean> {
|
||||||
|
const displays_of_group: Display[] = await db.displays
|
||||||
|
.where('group_id')
|
||||||
|
.equals(display_group_id)
|
||||||
|
.toArray();
|
||||||
|
if (displays_of_group.length === 0) return false;
|
||||||
|
|
||||||
|
for (const display of displays_of_group) {
|
||||||
if (!is_selected(display.id, current_selected_displays)) {
|
if (!is_selected(display.id, current_selected_displays)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -56,136 +84,113 @@ export function all_displays_of_group_selected(display_group: DisplayGroup, curr
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function select_all_of_group(display_group: DisplayGroup, new_value: boolean | null = null) {
|
export async function select_all_of_group(
|
||||||
for (const display of display_group.data) {
|
display_group_id: string,
|
||||||
select(selected_display_ids, display.id, new_value);
|
new_value: boolean | null = null
|
||||||
}
|
) {
|
||||||
filter_file_selection_for_current_selected_displays();
|
const displays_of_group: Display[] = await db.displays
|
||||||
|
.where('group_id')
|
||||||
|
.equals(display_group_id)
|
||||||
|
.toArray();
|
||||||
|
for (const display of displays_of_group) {
|
||||||
|
let action: string;
|
||||||
|
if (new_value === true) {
|
||||||
|
action = 'select';
|
||||||
|
} else {
|
||||||
|
action = 'deselect';
|
||||||
}
|
}
|
||||||
|
|
||||||
export function set_new_display_group_data(display_group_id: string, new_data: Display[]) {
|
select(selected_display_ids, display.id, action as 'toggle' | 'select' | 'deselect');
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
|
||||||
for (const display_group of displays) {
|
|
||||||
if (display_group.id === display_group_id) {
|
|
||||||
display_group.data = new_data;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return displays;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function get_display_by_id(display_id: string, display_group_array: DisplayGroup[]) {
|
export async function get_display_by_id(display_id: string): Promise<Display | null> {
|
||||||
const displays_array = display_group_array;
|
return (await db.displays.get(display_id)) ?? null;
|
||||||
for (const display_group of displays_array) {
|
|
||||||
for (const display of display_group.data) {
|
|
||||||
if (display.id === display_id) {
|
|
||||||
return display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function screenshot_loop(display_id: string, initial_retry_count: number = 5) {
|
||||||
|
const display = await db.displays.get(display_id);
|
||||||
|
if (!display || display.preview.currently_updating) return;
|
||||||
|
|
||||||
export function add_empty_display_group() {
|
display.preview.currently_updating = true;
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
await db.displays.update(display.id, { preview: display.preview });
|
||||||
displays.push({
|
|
||||||
id: get_uuid(),
|
|
||||||
data: [],
|
|
||||||
});
|
|
||||||
return displays;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
|
let last_hash: number | null = null;
|
||||||
|
|
||||||
export function remove_empty_display_groups() {
|
let retry_count = initial_retry_count;
|
||||||
displays.update((displays: DisplayGroup[]) => {
|
while (retry_count > 0) {
|
||||||
for (let i = displays.length - 1; i >= 0; i--) {
|
retry_count -= 1;
|
||||||
if (displays[i].data.length === 0) {
|
|
||||||
displays.splice(i, 1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return displays;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function update_screenshot(display_id: string, check_type: "first_check" | "last_check_different" | "last_check_same" = "first_check") {
|
const new_blob = await get_screenshot(display.ip);
|
||||||
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) {
|
if (!new_blob) {
|
||||||
update_displays_with_map((display: Display) => {
|
display.preview = { currently_updating: false, url: null };
|
||||||
if (display.id !== display_id) return display;
|
await db.displays.update(display.id, { preview: display.preview });
|
||||||
return { ...display, preview_url: null, preview_timeout_id: null };
|
|
||||||
})
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const display = get_display_by_id(display_id, get(displays));
|
|
||||||
|
|
||||||
let update_needed = check_type === "first_check";
|
|
||||||
if (check_type !== "first_check") {
|
|
||||||
if (display && display.preview_url) {
|
|
||||||
const old_blob = await fetch(display.preview_url).then(r => r.blob());
|
|
||||||
const new_hash = await image_content_hash(new_blob);
|
const new_hash = await image_content_hash(new_blob);
|
||||||
const old_hash = await image_content_hash(old_blob);
|
if (last_hash !== new_hash) {
|
||||||
update_needed = old_hash !== new_hash; // if different -> update
|
if (display.preview.url) {
|
||||||
}
|
URL.revokeObjectURL(display.preview.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
let new_preview_timeout_id: number | null = null;
|
last_hash = new_hash;
|
||||||
if (update_needed || check_type === "last_check_different") {
|
display.preview.url = URL.createObjectURL(new_blob);
|
||||||
new_preview_timeout_id = setTimeout(async () => { await update_screenshot(display_id, update_needed ? "last_check_different" : "last_check_same") }, 2 * 1000);
|
await db.displays.update(display.id, { preview: display.preview });
|
||||||
}
|
|
||||||
if (display?.preview_timeout_id) {
|
retry_count = initial_retry_count;
|
||||||
clearInterval(display.preview_timeout_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (update_needed) {
|
await new Promise((resolve) => setTimeout(resolve, 2000)); // sleep 2s
|
||||||
update_displays_with_map((display: Display) => {
|
|
||||||
if (display.id !== display_id) return display;
|
|
||||||
if (display.preview_url) {
|
|
||||||
URL.revokeObjectURL(display.preview_url);
|
|
||||||
}
|
|
||||||
const new_url = URL.createObjectURL(new_blob);
|
|
||||||
return { ...display, preview_url: new_url, preview_timeout_id: new_preview_timeout_id };
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
display.preview.currently_updating = false;
|
||||||
export async function update_displays_with_map(update_function: (display: Display) => Display | Promise<Display>) {
|
await db.displays.update(display.id, { preview: display.preview });
|
||||||
const display_groups = get(displays);
|
|
||||||
const updated_groups = await Promise.all(
|
|
||||||
display_groups.map(async (group: DisplayGroup) => ({
|
|
||||||
...group,
|
|
||||||
data: await Promise.all(group.data.map(update_function)),
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
displays.set(updated_groups);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run_on_all_selected_displays(run_function: ((ip: string, ...args: any[]) => void | Promise<void>), update_screenshot_afterwards: boolean, ...args: any[]) {
|
export async function run_on_all_selected_displays<T extends unknown[]>(
|
||||||
|
run_function: (ip: string, ...args: T) => void | Promise<void>,
|
||||||
|
update_screenshot_afterwards: boolean,
|
||||||
|
...args: T
|
||||||
|
) {
|
||||||
for (const display_id of get(selected_display_ids)) {
|
for (const display_id of get(selected_display_ids)) {
|
||||||
const display_ip = get_display_by_id(display_id, get(displays))?.ip;
|
const display_ip = (await get_display_by_id(display_id))?.ip;
|
||||||
if (display_ip) {
|
if (display_ip) {
|
||||||
await run_function(display_ip, ...args)
|
await run_function(display_ip, ...args);
|
||||||
if (update_screenshot_afterwards) {
|
if (update_screenshot_afterwards) {
|
||||||
await update_screenshot(display_id);
|
await screenshot_loop(display_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function get_display_groups(): Promise<DisplayGroup[]> {
|
||||||
|
return await db.display_groups.orderBy('position').toArray();
|
||||||
|
}
|
||||||
add_testing_displays();
|
|
||||||
function add_testing_displays() {
|
export async function get_display_ids_in_group(display_group_id: string): Promise<Display[]> {
|
||||||
// const names = ["Vorne Rechts", "Vorne Links", "Vorne Mitte", "Fernseher Rechts", "Fernseher Bühne", "UIUIUIUIUIUIUISEHRLANGERTEXT DER IST WIRKLICH LANG, DER TEXT, so lang, dass er wirklich nirgendswo hinpasst, nichtmal da oben /\\"];
|
const displays: Display[] = await db.displays
|
||||||
// for (const name of names) {
|
.where('group_id')
|
||||||
// add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", name, "Offline");
|
.equals(display_group_id)
|
||||||
// }
|
.sortBy('position');
|
||||||
|
return displays;
|
||||||
add_display("127.0.0.1", "00:1A:2B:3C:4D:5E", "PC", "host_offline");
|
}
|
||||||
// add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "host_offline");
|
|
||||||
|
export async function set_new_display_order(new_ordered_items: Display[]) {
|
||||||
|
for (let i = 0; i < new_ordered_items.length; i++) {
|
||||||
|
new_ordered_items[i].position = i;
|
||||||
|
await db.displays.put(new_ordered_items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function set_new_display_group_order(new_ordered_items: DisplayGroup[]) {
|
||||||
|
for (let i = 0; i < new_ordered_items.length; i++) {
|
||||||
|
new_ordered_items[i].position = i;
|
||||||
|
await db.display_groups.put(new_ordered_items[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setTimeout(add_testing_displays, 0);
|
||||||
|
async function add_testing_displays() {
|
||||||
|
await add_display('127.0.0.1', '00:1A:2B:3C:4D:5E', 'PC', 'host_offline');
|
||||||
|
// await add_display("192.168.178.111", "D4:81:D7:C0:DF:3C", "Laptop", "host_offline");
|
||||||
}
|
}
|
||||||
Regular → Executable
+268
-212
@@ -1,57 +1,61 @@
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import type { Display, FolderElement, TreeElement } from "../types";
|
import { get_file_primary_key, type Display, type Inode, type TreeElement } from '../types';
|
||||||
import { displays, get_display_by_id } from "./displays";
|
import { get_display_by_id } from './displays';
|
||||||
import { select, selected_display_ids, selected_file_ids } from "./select";
|
import { select, selected_display_ids, selected_file_ids } from './select';
|
||||||
import { get_file_data, get_file_tree_data } from "../api_handler";
|
import { create_folders, get_file_data, get_file_tree_data } from '../api_handler';
|
||||||
import { notifications } from "./notification";
|
import { deactivate_old_thumbnail_urls, generate_thumbnail } from './thumbnails';
|
||||||
import { CirclePoundSterling } from "lucide-svelte";
|
import { db, type FileOnDisplay } from '../files_display.db';
|
||||||
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[]>>>({});
|
|
||||||
// {
|
|
||||||
// path: {
|
|
||||||
// display_id: FolderElement[]
|
|
||||||
// ...
|
|
||||||
// },
|
|
||||||
// path2: {
|
|
||||||
// display_id: FolderElement[]
|
|
||||||
// ...
|
|
||||||
// },
|
|
||||||
// ...
|
|
||||||
// }
|
|
||||||
|
|
||||||
export const current_file_path: Writable<string> = writable<string>('/');
|
export const current_file_path: Writable<string> = writable<string>('/');
|
||||||
|
|
||||||
|
|
||||||
export async function change_file_path(new_path: string) {
|
export async function change_file_path(new_path: string) {
|
||||||
current_file_path.update(() => {
|
current_file_path.update(() => {
|
||||||
return new_path;
|
return new_path;
|
||||||
});
|
});
|
||||||
selected_file_ids.update(() => {
|
selected_file_ids.update(() => {
|
||||||
return [];
|
return [];
|
||||||
})
|
});
|
||||||
|
|
||||||
deactivate_old_thumbnail_urls();
|
deactivate_old_thumbnail_urls();
|
||||||
|
|
||||||
for (const display_group of get(displays)) {
|
const displays = await db.displays.toArray();
|
||||||
for (const display of display_group.data) {
|
|
||||||
|
for (const display of displays) {
|
||||||
const changed_paths = await get_changed_directory_paths(display, new_path);
|
const changed_paths = await get_changed_directory_paths(display, new_path);
|
||||||
if (!changed_paths) continue;
|
if (!changed_paths) continue;
|
||||||
console.log("Update file system from", display.name, ":", changed_paths);
|
console.log('Update file system from', display.name, ':', changed_paths);
|
||||||
for (const path of changed_paths) {
|
for (const path of changed_paths) {
|
||||||
update_folder_elements_recursively(display, path);
|
await update_folder_elements_recursively(display, path);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function filter_file_selection_for_current_selected_displays() {
|
export async function delete_and_deselect_unique_files_from_display(display_id: string) {
|
||||||
for (const selected_file_id of get(selected_file_ids)) {
|
const files_on_display = await db.files_on_display
|
||||||
if (!get_file_by_id(selected_file_id, get(all_files), get(current_file_path), true)) {
|
.where('display_id')
|
||||||
// file not found in selected displays
|
.equals(display_id)
|
||||||
select(selected_file_ids, selected_file_id, false);
|
.toArray();
|
||||||
|
for (const file of files_on_display) {
|
||||||
|
await remove_file_from_display(display_id, file.file_primary_key);
|
||||||
}
|
}
|
||||||
|
await remove_all_files_without_display();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function remove_file_from_display(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]);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
export async function update_current_folder_on_selected_displays() {
|
||||||
@@ -59,73 +63,134 @@ export async function update_current_folder_on_selected_displays() {
|
|||||||
return [];
|
return [];
|
||||||
});
|
});
|
||||||
const current_path = get(current_file_path);
|
const current_path = get(current_file_path);
|
||||||
for (const display_id of get(selected_display_ids)) {
|
|
||||||
const display = get_display_by_id(display_id, get(displays));
|
for (const display of await db.displays.where('id').anyOf(get(selected_display_ids)).toArray()) {
|
||||||
if (!display) continue;
|
await update_folder_elements_recursively(display, current_path);
|
||||||
update_folder_elements_recursively(display, current_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_display_ids_where_file_is_missing(path: string, file: FolderElement, selected_display_ids: string[], all_files: Record<string, Record<string, FolderElement[]>>): string[][] {
|
export async function get_missing_colliding_display_ids(
|
||||||
if (!all_files.hasOwnProperty(path)) return [selected_display_ids, []];
|
file: Inode,
|
||||||
const missing: string[] = [];
|
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: string[] = [];
|
||||||
Display:
|
const colliding_files = await db.files
|
||||||
for (const selected_display_id of selected_display_ids) {
|
.where('[path+name]')
|
||||||
if (!all_files[path].hasOwnProperty(selected_display_id)) {
|
.equals([file.path, file.name])
|
||||||
missing.push(selected_display_id);
|
.filter((e) => e.size !== file.size || e.type !== file.type)
|
||||||
continue;
|
.toArray();
|
||||||
}
|
for (const colliding_file of colliding_files) {
|
||||||
for (const folder_element of all_files[path][selected_display_id]) {
|
colliding.push(
|
||||||
if (folder_element.name === file.name) {
|
...(await get_display_ids_where_file_is_missing(colliding_file, selected_display_ids))
|
||||||
if (folder_element.hash !== file.hash) {
|
);
|
||||||
colliding.push(selected_display_id);
|
|
||||||
}
|
|
||||||
continue Display;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
missing.push(selected_display_id);
|
|
||||||
}
|
|
||||||
return [missing, colliding];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_display_ids_where_path_does_not_exist(path: string, selected_display_ids: string[], all_files: Record<string, Record<string, FolderElement[]>>): string[] {
|
return { missing, colliding };
|
||||||
if (!all_files.hasOwnProperty(path)) return selected_display_ids;
|
|
||||||
const out: string[] = [];
|
|
||||||
for (const selected_display_id of selected_display_ids) {
|
|
||||||
if (!all_files[path].hasOwnProperty(selected_display_id)) {
|
|
||||||
out.push(selected_display_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function get_changed_directory_paths(display: Display, file_path: string): Promise<string[] | null> {
|
async function get_display_ids_where_file_is_missing(
|
||||||
|
file: Inode,
|
||||||
|
selected_display_ids: string[]
|
||||||
|
): Promise<string[]> {
|
||||||
|
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<Display[]> {
|
||||||
|
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('[path+name+type]')
|
||||||
|
.equals([path_without_last_part, last_path_part, '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 = selected_display_ids.filter(async (display_id) => {
|
||||||
|
const folder_exists = await db.files_on_display.get([display_id, folder_primary_key]);
|
||||||
|
if (invert) {
|
||||||
|
return !folder_exists;
|
||||||
|
} else {
|
||||||
|
return folder_exists;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (await db.displays.bulkGet(display_ids)).filter((e) => e !== undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function get_changed_directory_paths(
|
||||||
|
display: Display,
|
||||||
|
file_path: string
|
||||||
|
): Promise<string[] | null> {
|
||||||
const current_folder = await get_file_tree_data(display.ip, file_path);
|
const current_folder = await get_file_tree_data(display.ip, file_path);
|
||||||
const directory_strings = get_recursive_changed_directory_paths(display, file_path, current_folder, get(all_files));
|
const directory_strings = await get_recursive_changed_directory_paths(
|
||||||
|
display,
|
||||||
|
file_path,
|
||||||
|
current_folder
|
||||||
|
);
|
||||||
if (directory_strings.size === 0) return null;
|
if (directory_strings.size === 0) return null;
|
||||||
const directory_strings_array = [...directory_strings];
|
const directory_strings_array = [...directory_strings];
|
||||||
return directory_strings_array.filter((e) => (!directory_strings_array.some((f) => (f !== e && f.startsWith(e)))));
|
return directory_strings_array.filter(
|
||||||
|
(e) => !directory_strings_array.some((f) => f !== e && f.startsWith(e))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function get_recursive_changed_directory_paths(display: Display, current_file_path: string, current_folder_elements: TreeElement[] | null, files: Record<string, Record<string, FolderElement[]>>): Set<string> {
|
async function get_recursive_changed_directory_paths(
|
||||||
const files_folder: FolderElement[] = files[current_file_path][display.id];
|
display: Display,
|
||||||
if ((!files_folder || files_folder.length === 0) && (!current_folder_elements || current_folder_elements.length === 0)) {
|
current_file_path: string,
|
||||||
|
current_folder_elements: TreeElement[] | null
|
||||||
|
): Promise<Set<string>> {
|
||||||
|
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
|
return new Set([]); // no data -> no update needed
|
||||||
} else if (!files_folder || !current_folder_elements || current_folder_elements.length !== files_folder.length) {
|
} 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
|
return new Set([current_file_path]); // existing data does not match new data -> update
|
||||||
}
|
}
|
||||||
|
|
||||||
let has_changed: Set<string> = new Set();
|
const has_changed: Set<string> = new Set();
|
||||||
for (const tree_folder_element of current_folder_elements) {
|
for (const tree_folder_element of current_folder_elements) {
|
||||||
const folder_element = files_folder.find(e => e.name === tree_folder_element.name);
|
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)) {
|
if (
|
||||||
|
!folder_element ||
|
||||||
|
(tree_folder_element.type !== 'directory' && folder_element.size !== tree_folder_element.size)
|
||||||
|
) {
|
||||||
return new Set([current_file_path]);
|
return new Set([current_file_path]);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tree_folder_element.type === "directory" && tree_folder_element.contents) {
|
if (tree_folder_element.type === 'directory' && tree_folder_element.contents) {
|
||||||
const new_file_path = current_file_path + tree_folder_element.name + '/';
|
const new_file_path = current_file_path + tree_folder_element.name + '/';
|
||||||
for (const string of get_recursive_changed_directory_paths(display, new_file_path, tree_folder_element.contents, files)) {
|
for (const string of await get_recursive_changed_directory_paths(
|
||||||
|
display,
|
||||||
|
new_file_path,
|
||||||
|
tree_folder_element.contents
|
||||||
|
)) {
|
||||||
has_changed.add(string);
|
has_changed.add(string);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -133,116 +198,95 @@ function get_recursive_changed_directory_paths(display: Display, current_file_pa
|
|||||||
return has_changed;
|
return has_changed;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function update_folder_elements_recursively(display: Display, file_path: string = '/'): Promise<number> {
|
export async function update_folder_elements_recursively(
|
||||||
|
display: Display,
|
||||||
|
file_path: string = '/'
|
||||||
|
): Promise<void> {
|
||||||
const new_folder_elements = await get_file_data(display.ip, file_path);
|
const new_folder_elements = await get_file_data(display.ip, file_path);
|
||||||
if (new_folder_elements === null) return 0;
|
if (!new_folder_elements) return;
|
||||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
|
||||||
if (!files.hasOwnProperty(file_path)) {
|
const existing_file_keys_on_display_in_path: [string, string, number, string][] = (
|
||||||
files[file_path] = {};
|
await db.files_on_display.where('display_id').equals(display.id).toArray()
|
||||||
|
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||||
|
const existing_files_on_display_in_path: 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 diff = get_folder_elements_difference(
|
||||||
|
existing_files_on_display_in_path,
|
||||||
|
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),
|
||||||
|
is_loading: false,
|
||||||
|
percentage: 0,
|
||||||
|
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 + '/'
|
||||||
|
);
|
||||||
}
|
}
|
||||||
if (!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);
|
|
||||||
// Generate Thumbnails:
|
// Generate Thumbnails:
|
||||||
setTimeout(async () => {
|
setTimeout(async () => {
|
||||||
for (const folder_element of diff.new) {
|
for (const new_element of diff.new) {
|
||||||
await generate_thumbnail(display.ip, file_path, folder_element);
|
await generate_thumbnail(display.ip, file_path, new_element.folder_element);
|
||||||
}
|
}
|
||||||
}, 0)
|
}, 0);
|
||||||
|
|
||||||
files[file_path][display.id].push(...diff.new);
|
|
||||||
return remove_folder_elements_recursively(files, display, diff.deleted, file_path);
|
|
||||||
})
|
|
||||||
|
|
||||||
let folder_size = 0;
|
|
||||||
for (const element of new_folder_elements) {
|
|
||||||
if (element.type === 'inode/directory') {
|
|
||||||
const folder_content_size = await update_folder_elements_recursively(display, file_path + element.name + '/');
|
|
||||||
folder_size += folder_content_size;
|
|
||||||
// Update foldersize
|
|
||||||
all_files.update((files: Record<string, Record<string, FolderElement[]>>) => {
|
|
||||||
for (const current_folder_element of files[file_path][display.id]) {
|
|
||||||
if (current_folder_element.id === element.id) {
|
|
||||||
current_folder_element.size = folder_content_size;
|
|
||||||
}
|
}
|
||||||
|
if (diff.deleted.length > 0) {
|
||||||
|
// Remove old Folder-Elements
|
||||||
|
for (const old_element of diff.deleted) {
|
||||||
|
remove_file_from_display(display.id, get_file_primary_key(old_element));
|
||||||
}
|
}
|
||||||
return files;
|
await remove_all_files_without_display();
|
||||||
})
|
|
||||||
} else {
|
|
||||||
folder_size += element.size;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return folder_size;
|
|
||||||
}
|
|
||||||
|
|
||||||
function remove_folder_elements_recursively(files: Record<string, Record<string, FolderElement[]>>, display: Display, folder_elements: FolderElement[], file_path: string): Record<string, Record<string, FolderElement[]>> {
|
|
||||||
if (!files.hasOwnProperty(file_path) || !files[file_path].hasOwnProperty(display.id)) {
|
|
||||||
console.error("File remove path does not exist:", files, display, folder_elements, file_path);
|
|
||||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist: ${file_path} display_ip: ${display.ip}`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
for (const folder_element of folder_elements) {
|
|
||||||
files[file_path][display.id] = files[file_path][display.id].filter((f) => f.id !== folder_element.id);
|
|
||||||
|
|
||||||
if (folder_element.type === 'inode/directory') {
|
|
||||||
const new_file_path = file_path + folder_element.name + '/';
|
|
||||||
if (!files.hasOwnProperty(new_file_path) || !files[new_file_path].hasOwnProperty(display.id)) {
|
|
||||||
console.error("File remove path does not exist (recursion):", files, display, folder_elements, file_path, new_file_path);
|
|
||||||
notifications.push("error", "Fehler beim Aktualisieren der Dateien", `File remove path does not exist (recursion): ${new_file_path} display_ip: ${display.ip}`);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const sub_folder = files[new_file_path][display.id];
|
|
||||||
remove_folder_elements_recursively(files, display, sub_folder, new_file_path);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return files;
|
function get_folder_elements_difference(
|
||||||
}
|
old_elements: Inode[],
|
||||||
|
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)));
|
||||||
|
const new_keys = new Set(new_elements.map((e) => get_file_primary_key(e.folder_element)));
|
||||||
|
|
||||||
function get_folder_elements_difference(old_elements: FolderElement[], new_elements: FolderElement[]): { deleted: FolderElement[], new: FolderElement[] } {
|
const only_in_old = old_elements.filter((e) => !new_keys.has(get_file_primary_key(e)));
|
||||||
const old_hashes = new Set(old_elements.map(e => e.hash));
|
const only_in_new = new_elements.filter(
|
||||||
const new_hashes = new Set(new_elements.map(e => e.hash));
|
(e) => !old_keys.has(get_file_primary_key(e.folder_element))
|
||||||
|
);
|
||||||
const only_in_old = old_elements.filter(e => !new_hashes.has(e.hash));
|
|
||||||
const only_in_new = new_elements.filter(e => !old_hashes.has(e.hash));
|
|
||||||
return { deleted: only_in_old, new: only_in_new };
|
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<Inode[]> {
|
||||||
|
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);
|
||||||
export function get_current_folder_elements(all_files: Record<string, Record<string, FolderElement[]>>, current_file_path: string, selected_display_ids: string[]) {
|
|
||||||
if (!all_files.hasOwnProperty(current_file_path)) return [];
|
|
||||||
|
|
||||||
const files_on_display_array = all_files[current_file_path];
|
|
||||||
const files: FolderElement[] = [];
|
|
||||||
for (const key of Object.keys(files_on_display_array)) {
|
|
||||||
if (selected_display_ids.includes(key)) {
|
|
||||||
FileOnDisplay:
|
|
||||||
for (const file_on_display of files_on_display_array[key]) {
|
|
||||||
for (const existing_file of files) {
|
|
||||||
const both_same_folder = file_on_display.type === "inode/directory" && existing_file.type === "inode/directory" && file_on_display.name === existing_file.name;
|
|
||||||
if (both_same_folder && file_on_display.size !== existing_file.size) {
|
|
||||||
existing_file.size = -1;
|
|
||||||
}
|
|
||||||
if (file_on_display.hash === existing_file.hash) {
|
|
||||||
continue FileOnDisplay;
|
|
||||||
} else if (both_same_folder) {
|
|
||||||
existing_file.date_created = null;
|
|
||||||
continue FileOnDisplay;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
files.push({ ...file_on_display });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return sort_files(files);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function sort_files(files: FolderElement[]) {
|
function sort_files(files: Inode[]) {
|
||||||
files.sort((a, b) => {
|
files.sort((a, b) => {
|
||||||
const isDirA = a.type === 'inode/directory';
|
const isDirA = a.type === 'inode/directory';
|
||||||
const isDirB = b.type === 'inode/directory';
|
const isDirB = b.type === 'inode/directory';
|
||||||
@@ -255,63 +299,75 @@ function sort_files(files: FolderElement[]) {
|
|||||||
const nameCompare = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
const nameCompare = a.name.localeCompare(b.name, undefined, { sensitivity: 'base' });
|
||||||
if (nameCompare !== 0) return nameCompare;
|
if (nameCompare !== 0) return nameCompare;
|
||||||
|
|
||||||
// Wenn name gleich, absteigend nach date_created
|
return -1;
|
||||||
if (!b.date_created || !a.date_created) return -1;
|
|
||||||
return b.date_created.getTime() - a.date_created.getTime();
|
|
||||||
});
|
});
|
||||||
return files;
|
return files;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function get_file_by_id(file_id: string, all_files: Record<string, Record<string, FolderElement[]>>, current_file_path: string, only_from_selected_displays: boolean = false): FolderElement | null {
|
export async function get_file_by_id(
|
||||||
let current_path_elements: Record<string, FolderElement[]> | undefined = all_files[current_file_path];
|
file_primary_key: string,
|
||||||
if (!current_path_elements) return null;
|
only_from_selected_displays: boolean = false
|
||||||
if (only_from_selected_displays) {
|
): Promise<Inode | null> {
|
||||||
current_path_elements = Object.fromEntries(
|
const file = (await db.files.get(JSON.parse(file_primary_key))) ?? null;
|
||||||
Object.entries(current_path_elements).filter(([key]) => get(selected_file_ids).includes(key))
|
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;
|
||||||
}
|
}
|
||||||
const all_folder_elements = Object.values(current_path_elements).flat();
|
|
||||||
const found = all_folder_elements.find(el => el.id === file_id);
|
|
||||||
if (!found) return null;
|
|
||||||
return found
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function run_for_selected_files_on_selected_displays(action: (ip: string, file_names: string[]) => Promise<void>): Promise<void> {
|
export async function run_for_selected_files_on_selected_displays(
|
||||||
const files = get(all_files);
|
action: (ip: string, file_names: string[]) => Promise<void>
|
||||||
const file_path = get(current_file_path);
|
): Promise<void> {
|
||||||
const folder_elements: FolderElement[] = get(selected_file_ids)
|
|
||||||
.map((file_id) => get_file_by_id(file_id, files, file_path))
|
|
||||||
.filter((element) => element !== null);
|
|
||||||
const folder_element_hashs = folder_elements
|
|
||||||
.map((folder_element) => folder_element.hash)
|
|
||||||
.filter((hash) => hash !== null);
|
|
||||||
|
|
||||||
for (const display_id of get(selected_display_ids)) {
|
for (const display_id of get(selected_display_ids)) {
|
||||||
if (!files[file_path].hasOwnProperty(display_id)) continue;
|
const file_keys_on_display: [string, string, number, string][] = (
|
||||||
const files_on_display = files[file_path][display_id];
|
await db.files_on_display.where('display_id').equals(display_id).toArray()
|
||||||
|
).map((e) => JSON.parse(e.file_primary_key) as [string, string, number, string]);
|
||||||
|
const file_names_on_display: string[] = (
|
||||||
|
await db.files.where('[path+name+size+type]').anyOf(file_keys_on_display).toArray()
|
||||||
|
).map((e) => e.name);
|
||||||
|
|
||||||
const selected_file_names_on_display: string[] = files_on_display
|
const display = await get_display_by_id(display_id);
|
||||||
.filter((e) => (e.hash && folder_element_hashs.includes(e.hash)) || (e.type === 'inode/directory' && folder_elements.some(e2 => e2.name === e.name && e2.type === 'inode/directory')))
|
|
||||||
.map((folder_element) => folder_element.name);
|
|
||||||
if (selected_file_names_on_display.length === 0) continue;
|
|
||||||
|
|
||||||
const display = get_display_by_id(display_id, get(displays));
|
|
||||||
if (!display) continue;
|
if (!display) continue;
|
||||||
|
|
||||||
await action(display.ip, selected_file_names_on_display);
|
await action(display.ip, file_names_on_display);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
export async function create_folder_on_all_selected_displays(
|
||||||
|
folder_name: string,
|
||||||
|
path: string,
|
||||||
|
selected_display_ids: string[]
|
||||||
|
): Promise<void> {
|
||||||
|
const path_parts = path
|
||||||
|
.slice(1, path.length - 1)
|
||||||
|
.split('/')
|
||||||
|
.filter((e) => e.length !== 0);
|
||||||
|
let remaining_display_ids = [...selected_display_ids];
|
||||||
|
|
||||||
export function get_longest_existing_path_and_needed_parts(path: string, display_id: string, all_files: Record<string, Record<string, FolderElement[]>>): { existing: string; needed: string[] } {
|
const getDisplaysForPath = async (currentPath: string): Promise<Display[]> => {
|
||||||
const path_parts = path.slice(1, path.length - 1).split('/').filter(e => e.length !== 0); // remove front and back / and split after that, then remove empty strings
|
if (currentPath === '/') {
|
||||||
for (let i = path_parts.length; i > 0; i--) {
|
const displays = await db.displays.bulkGet(remaining_display_ids);
|
||||||
const current_path = '/' + [...path_parts].splice(0, i).join('/') + '/';;
|
return displays.filter((d): d is Display => !!d);
|
||||||
if (all_files.hasOwnProperty(current_path)) {
|
}
|
||||||
if (all_files[current_path].hasOwnProperty(display_id)) {
|
return get_displays_where_path_exists(currentPath, remaining_display_ids, false);
|
||||||
return { existing: current_path, needed: [...path_parts].splice(i) };
|
};
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
const folders_to_create = [...path_parts.slice(depth), folder_name];
|
||||||
|
for (const display of displays) {
|
||||||
|
await create_folders(display.ip, currentPath, folders_to_create);
|
||||||
|
remaining_display_ids = remaining_display_ids.filter((id) => id !== display.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return { existing: '/', needed: path_parts };
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import { get, writable } from "svelte/store";
|
import { writable } from "svelte/store";
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
id: number;
|
id: number;
|
||||||
|
|||||||
Regular → Executable
+9
-6
@@ -1,17 +1,20 @@
|
|||||||
import { writable, type Writable } from "svelte/store";
|
import { writable, type Writable } from 'svelte/store';
|
||||||
|
|
||||||
export const selected_file_ids: Writable<string[]> = writable<string[]>([]);
|
export const selected_file_ids: Writable<string[]> = writable<string[]>([]); // JSON.stringify([string, string, number, string])
|
||||||
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
|
export const selected_display_ids: Writable<string[]> = writable<string[]>([]);
|
||||||
|
|
||||||
|
export function select(
|
||||||
export function select(selected_ids: Writable<string[]>, id: string, new_value: boolean | null = null) {
|
selected_ids: Writable<string[]>,
|
||||||
|
id: string,
|
||||||
|
action: 'toggle' | 'select' | 'deselect'
|
||||||
|
) {
|
||||||
selected_ids.update((all_ids: string[]) => {
|
selected_ids.update((all_ids: string[]) => {
|
||||||
if (all_ids.includes(id)) {
|
if (all_ids.includes(id)) {
|
||||||
const index = all_ids.indexOf(id);
|
const index = all_ids.indexOf(id);
|
||||||
if (index > -1 && new_value !== true) {
|
if (index > -1 && action !== 'select') {
|
||||||
all_ids.splice(index, 1);
|
all_ids.splice(index, 1);
|
||||||
}
|
}
|
||||||
} else if (new_value !== false) {
|
} else if (action !== 'deselect') {
|
||||||
all_ids.push(id);
|
all_ids.push(id);
|
||||||
}
|
}
|
||||||
return all_ids;
|
return all_ids;
|
||||||
|
|||||||
Regular → Executable
+22
-17
@@ -1,31 +1,34 @@
|
|||||||
import { get, writable, type Writable } from "svelte/store";
|
import { get, writable, type Writable } from 'svelte/store';
|
||||||
import { get_thumbnail_blob } from "../api_handler";
|
import { get_thumbnail_blob } from '../api_handler';
|
||||||
import { type FolderElement } from "../types";
|
import { type Inode } from '../types';
|
||||||
import { db, type ThumbnailBlobDBEntry } from "../indexdb/file_thumbnails.db";
|
import { db } from '../files_display.db';
|
||||||
import { get_file_type } from "../utils";
|
import { get_file_type } from '../utils';
|
||||||
|
|
||||||
export const active_thumbnail_urls: Writable<string[]> = writable<string[]>([]);
|
export const active_thumbnail_urls: Writable<string[]> = writable<string[]>([]);
|
||||||
|
|
||||||
export async function generate_thumbnail(display_ip: string, path: string, folder_element: FolderElement): Promise<void> {
|
export async function generate_thumbnail(
|
||||||
|
display_ip: string,
|
||||||
|
path: string,
|
||||||
|
folder_element: Inode
|
||||||
|
): Promise<void> {
|
||||||
const supported_file_type = get_file_type(folder_element);
|
const supported_file_type = get_file_type(folder_element);
|
||||||
if (!supported_file_type || !folder_element.hash) return;
|
if (!supported_file_type) 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);
|
const thumbnail_blob = await get_thumbnail_blob(display_ip, path + folder_element.name);
|
||||||
if (!thumbnail_blob) return;
|
if (!thumbnail_blob) return;
|
||||||
await db.thumbnail_blobs.add({ hash: hash, blob: thumbnail_blob });
|
|
||||||
|
folder_element.thumbnail = thumbnail_blob;
|
||||||
|
|
||||||
|
await db.files.put(folder_element); // save
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function get_thumbnail_url(hash: string | null): Promise<string | null> {
|
export async function get_thumbnail_url(file: Inode): Promise<string | null> {
|
||||||
if (hash === null) return null;
|
if (!file.thumbnail) return null;
|
||||||
const thumbnail_blob = await db.thumbnail_blobs.get(hash) as ThumbnailBlobDBEntry;
|
const new_url = URL.createObjectURL(file.thumbnail);
|
||||||
if (!thumbnail_blob) return null;
|
|
||||||
const new_url = URL.createObjectURL(thumbnail_blob.blob);
|
|
||||||
active_thumbnail_urls.update((current: string[]) => {
|
active_thumbnail_urls.update((current: string[]) => {
|
||||||
current.push(new_url);
|
current.push(new_url);
|
||||||
return current;
|
return current;
|
||||||
})
|
});
|
||||||
return new_url;
|
return new_url;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -34,5 +37,7 @@ export function deactivate_old_thumbnail_urls() {
|
|||||||
for (const url of current_urls) {
|
for (const url of current_urls) {
|
||||||
URL.revokeObjectURL(url);
|
URL.revokeObjectURL(url);
|
||||||
}
|
}
|
||||||
active_thumbnail_urls.update(() => { return []; })
|
active_thumbnail_urls.update(() => {
|
||||||
|
return [];
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Regular → Executable
+44
-36
@@ -1,73 +1,81 @@
|
|||||||
import { FileBox, FileImage, FileText, FileVideoCamera, ImagePlay, type X } from "lucide-svelte";
|
import { FileBox, FileImage, FileText, FileVideoCamera, ImagePlay, type X } from 'lucide-svelte';
|
||||||
import type { Snippet } from "svelte";
|
import type { Snippet } from 'svelte';
|
||||||
|
|
||||||
export type RequestResponse = {
|
export type RequestResponse = {
|
||||||
ok: boolean,
|
ok: boolean;
|
||||||
http_code?: number
|
http_code?: number;
|
||||||
blob?: Blob,
|
blob?: Blob;
|
||||||
json?: any,
|
json?: Record<string, unknown>;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type ShellCommandResponse = {
|
export type ShellCommandResponse = {
|
||||||
stdout: string,
|
stdout: string;
|
||||||
stderr: string,
|
stderr: string;
|
||||||
exitCode: number,
|
exitCode: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type SupportedFileType = {
|
export type SupportedFileType = {
|
||||||
display_name: string;
|
display_name: string;
|
||||||
mime_type: string;
|
mime_type: string;
|
||||||
};
|
};
|
||||||
export const supported_file_type_icon: Record<string, typeof X> = {
|
export const supported_file_type_icon: Record<string, typeof X> = {
|
||||||
'MP4': FileVideoCamera,
|
MP4: FileVideoCamera,
|
||||||
'JPG': FileImage,
|
JPG: FileImage,
|
||||||
'PNG': FileImage,
|
PNG: FileImage,
|
||||||
'GIF': ImagePlay,
|
GIF: ImagePlay,
|
||||||
'PPTX': FileBox,
|
PPTX: FileBox,
|
||||||
'ODP': FileBox,
|
ODP: FileBox,
|
||||||
'PDF': FileText
|
PDF: FileText
|
||||||
}
|
};
|
||||||
|
|
||||||
export type FolderElement = {
|
export type Inode = {
|
||||||
id: string;
|
path: string;
|
||||||
hash: string;
|
|
||||||
name: string;
|
name: string;
|
||||||
type: string;
|
|
||||||
date_created: Date | null;
|
|
||||||
size: number;
|
size: number;
|
||||||
|
type: string;
|
||||||
|
date_created: Date;
|
||||||
|
thumbnail: Blob | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function get_file_primary_key(file: Inode): string {
|
||||||
|
return JSON.stringify([file.path, file.name, file.size, file.type]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export type TreeElement = {
|
export type TreeElement = {
|
||||||
contents?: TreeElement[];
|
contents?: TreeElement[];
|
||||||
type: "file" | "directory";
|
type: 'file' | 'directory';
|
||||||
name: string;
|
name: string;
|
||||||
size: number;
|
size: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
export type Display = {
|
export type Display = {
|
||||||
id: string;
|
id: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
preview_url: string | null;
|
|
||||||
preview_timeout_id: number | null;
|
|
||||||
mac: string | null;
|
mac: string | null;
|
||||||
|
position: number;
|
||||||
|
preview: PreviewObject;
|
||||||
|
group_id: string;
|
||||||
name: string;
|
name: string;
|
||||||
status: DisplayStatus;
|
status: DisplayStatus;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type DisplayGroup = {
|
export type DisplayGroup = {
|
||||||
id: string;
|
id: string;
|
||||||
data: Display[];
|
position: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type PreviewObject = {
|
||||||
|
currently_updating: boolean;
|
||||||
|
url: string | null;
|
||||||
|
};
|
||||||
|
|
||||||
export type MenuOption = {
|
export type MenuOption = {
|
||||||
icon?: typeof X;
|
icon?: typeof X;
|
||||||
name: string;
|
name: string;
|
||||||
class?: string;
|
class?: string;
|
||||||
on_select?: () => void;
|
on_select?: () => void | Promise<void>;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type PopupContent = {
|
export type PopupContent = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
@@ -78,12 +86,12 @@ export type PopupContent = {
|
|||||||
title_icon?: typeof X | null;
|
title_icon?: typeof X | null;
|
||||||
window_class?: string;
|
window_class?: string;
|
||||||
closable?: boolean;
|
closable?: boolean;
|
||||||
}
|
};
|
||||||
|
|
||||||
export type DisplayStatus = "host_offline" | "app_offline" | "app_online" | null;
|
export type DisplayStatus = 'host_offline' | 'app_offline' | 'app_online' | null;
|
||||||
|
|
||||||
export function to_display_status(value: string): DisplayStatus {
|
export function to_display_status(value: string): DisplayStatus {
|
||||||
return ["host_offline", "app_offline", "app_online"].includes(value)
|
return ['host_offline', 'app_offline', 'app_online'].includes(value)
|
||||||
? (value as DisplayStatus)
|
? (value as DisplayStatus)
|
||||||
: null;
|
: null;
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,12 @@
|
|||||||
import type { DisplayStatus, FolderElement, SupportedFileType } from "./types";
|
import type { DisplayStatus, Inode, SupportedFileType } from './types';
|
||||||
import supported_file_types_json from './../../../../shared/supported_file_types.json';
|
import supported_file_types_json from './../../../../shared/supported_file_types.json';
|
||||||
|
|
||||||
const supported_file_types: Record<string, SupportedFileType> = supported_file_types_json as Record<string, SupportedFileType>;
|
const supported_file_types: Record<string, SupportedFileType> = supported_file_types_json as Record<
|
||||||
|
string,
|
||||||
|
SupportedFileType
|
||||||
|
>;
|
||||||
|
|
||||||
export function get_file_type(file: FolderElement): SupportedFileType | null {
|
export function get_file_type(file: Inode): SupportedFileType | null {
|
||||||
for (const key of Object.keys(supported_file_types)) {
|
for (const key of Object.keys(supported_file_types)) {
|
||||||
if (file.type === supported_file_types[key].mime_type) {
|
if (file.type === supported_file_types[key].mime_type) {
|
||||||
return supported_file_types[key];
|
return supported_file_types[key];
|
||||||
@@ -24,11 +27,12 @@ export function get_uuid(): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function get_file_size_display_string(size: number, toFixed: number | null = null): string {
|
export function get_file_size_display_string(size: number, toFixed: number | null = null): string {
|
||||||
if (size < 0) return toFixed === null ? "versch." : "Verschiedene Größen auf verschiedenen Bildschirmen";
|
if (size < 0)
|
||||||
if (size === 0) return "0 B";
|
return toFixed === null ? 'versch.' : 'Verschiedene Größen auf verschiedenen Bildschirmen';
|
||||||
|
if (size === 0) return '0 B';
|
||||||
|
|
||||||
const k = 1024;
|
const k = 1024;
|
||||||
const sizes = ["B", "KB", "MB", "GB", "TB"];
|
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||||
|
|
||||||
const i = Math.floor(Math.log(size) / Math.log(k));
|
const i = Math.floor(Math.log(size) / Math.log(k));
|
||||||
const value = size / Math.pow(k, i);
|
const value = size / Math.pow(k, i);
|
||||||
@@ -38,14 +42,13 @@ export function get_file_size_display_string(size: number, toFixed: number | nul
|
|||||||
return size_string.replace('.', ',');
|
return size_string.replace('.', ',');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export async function image_content_hash(blob: Blob, size = 32): Promise<number> {
|
export async function image_content_hash(blob: Blob, size = 32): Promise<number> {
|
||||||
// Blob → ImageBitmap (GPU-dekodiert, superschnell)
|
// Blob → ImageBitmap (GPU-dekodiert, superschnell)
|
||||||
const bitmap = await createImageBitmap(blob);
|
const bitmap = await createImageBitmap(blob);
|
||||||
|
|
||||||
// OffscreenCanvas ist sehr schnell (kein Layout nötig)
|
// OffscreenCanvas ist sehr schnell (kein Layout nötig)
|
||||||
const canvas = new OffscreenCanvas(size, size);
|
const canvas = new OffscreenCanvas(size, size);
|
||||||
const ctx = canvas.getContext("2d")!;
|
const ctx = canvas.getContext('2d')!;
|
||||||
ctx.drawImage(bitmap, 0, 0, size, size);
|
ctx.drawImage(bitmap, 0, 0, size, size);
|
||||||
|
|
||||||
// Pixel-Daten holen
|
// Pixel-Daten holen
|
||||||
@@ -62,8 +65,6 @@ export async function image_content_hash(blob: Blob, size = 32): Promise<number>
|
|||||||
return hash >>> 0; // unsigned int
|
return hash >>> 0; // unsigned int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function is_valid_name(input: string): boolean {
|
export function is_valid_name(input: string): boolean {
|
||||||
return /^[\p{L}\p{N}\p{M}\-_.+,()[\]{}@!§$%&=~^ ]+$/u.test(input);
|
return /^[\p{L}\p{N}\p{M}\-_.+,()[\]{}@!§$%&=~^ ]+$/u.test(input);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user