40 Commits

Author SHA1 Message Date
b966d19792 Ende des Tages 26.05. 2025-05-26 13:55:19 +02:00
def4e22226 initila check if vorgang exists 2025-05-23 11:24:57 +02:00
b44187b010 remove admin user output 2025-05-23 10:22:05 +02:00
bebd226c80 fixed fluechtigkeitsfehler 2025-05-22 14:16:44 +02:00
4adea19981 temp 22.05 2025-05-22 14:13:53 +02:00
6b7188350f temporär zum Testen 2025-05-22 10:22:45 +02:00
20c74d49d9 Merge branch 'development' into f03_user-management 2025-05-21 10:21:51 +02:00
131318da14 input field fuer Zugangscode beim Anlegen 2025-05-21 10:13:50 +02:00
8bc7fad146 fix two style element 2025-05-16 10:40:50 +02:00
titver968
255f14faae Merge branch 'development' of ssh://gitea.innovation-hub-niedersachsen.de:4422/innohub/tatort into development 2025-05-16 08:35:16 +02:00
0fc496cd5c Merge branch 'CameraSteuerung' into development 2025-05-15 12:02:53 +02:00
23123c6291 warnings korrigiert 2025-05-15 12:01:46 +02:00
f56eeeee2c update kleine Anpassungen 2025-05-15 11:58:10 +02:00
9058ea38e2 Merge branch 'CameraSteuerung' into development 2025-05-14 17:14:38 +02:00
9519b0fdd1 update des Tages CameraSteuerung 2025-05-14 17:08:30 +02:00
titver968
2278b18f02 Fehlerkorrekturen 2025-05-14 13:59:06 +02:00
titver968
18038903cb warning korrigiert 2025-05-14 13:50:31 +02:00
75f88e3e49 Merge pull request 'development' (#1) from development into main
Reviewed-on: #1
2025-05-14 13:42:23 +02:00
a7c8bf91d9 Merge branches 'f01_verify_file_upload' and 'f02_rename_file' into dev_merge 2025-05-14 11:02:51 +02:00
06427253e4 comment on renaming operation of tatort 2025-05-14 10:59:11 +02:00
9b2ebd3d31 refactor success message 2025-05-14 10:51:14 +02:00
8d71633f50 Update Buttons in model viewer 2025-05-12 16:22:27 +02:00
657fd422bc refactor event handlers 2025-05-12 13:20:53 +02:00
2809df9fd4 Add modal for renaming operation 2025-05-12 13:05:29 +02:00
33edc125c3 implement delete of Vorgaenge 2025-05-09 12:36:57 +02:00
617033fccb check is name already exists during renaming 2025-05-09 09:42:17 +02:00
9575ef84d4 fixed issues with dashes in name and id 2025-05-09 09:08:58 +02:00
f3850649c7 zwischenspeichern 2025-05-08 15:35:24 +02:00
49d2ee04f1 remove vertical align 2025-05-08 14:03:21 +02:00
aa3dcc1a75 add delete function 2025-05-08 14:02:17 +02:00
74f06d9995 first version finished 2025-05-08 08:40:52 +02:00
985d4a424f Tages-Update 2025-05-07 18:09:47 +02:00
fac5cc80a9 check for valid .GLD file 2025-04-28 10:22:33 +02:00
titver968
7cd19ceed8 Dockerfile port 3000 2025-04-17 12:05:15 +02:00
titver968
14b1e12e65 minio client beim upload definiert 2025-04-17 09:03:48 +02:00
e72ad77866 reset branch 2025-04-16 16:46:28 +02:00
012336ac56 erste Versuche 2025-04-16 16:30:36 +02:00
titver968
d11741d1ec new Dockerfile 2025-04-11 12:12:06 +02:00
titver968
38f8b7c1a9 svelte 5 mit npm 2025-04-11 11:31:10 +02:00
titver968
104e86136b delete old version 2025-04-11 11:29:00 +02:00
19 changed files with 7487 additions and 4853 deletions

View File

@@ -1,23 +0,0 @@
# Patterns to ignore when building packages.
# This supports shell glob matching, relative path matching, and
# negation (prefixed with !). Only one pattern per line.
.DS_Store
# Common VCS dirs
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
# Common backup files
*.swp
*.bak
*.tmp
*.orig
*~
# Various IDEs
.project
.idea/
*.tmproj
.vscode/

View File

@@ -3,7 +3,7 @@
"singleQuote": true, "singleQuote": true,
"trailingComma": "none", "trailingComma": "none",
"printWidth": 100, "printWidth": 100,
"plugins": ["prettier-plugin-svelte", "prettier-plugin-tailwindcss"], "plugins": ["prettier-plugin-svelte"],
"overrides": [ "overrides": [
{ {
"files": "*.svelte", "files": "*.svelte",

View File

@@ -1,26 +1,17 @@
# Verwende ein offizielles Node.js-Image als Basis-Image # --- Build stage ---
FROM node:22 FROM node:22 AS build
ENV NODE_ENV=production
ENV ORIGIN=https://tatort.innovation-hub-niedersachsen.de
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . ./
RUN npm run build
# Setze den Arbeitsverzeichnis im Container # --- Production stage ---
WORKDIR /usr/src/app FROM node:22-alpine3.20
COPY --from=build /app .
# Installiere pnpm ENV HOST=0.0.0.0
RUN npm install -g pnpm
# Kopiere package.json und pnpm-lock.yaml in das Arbeitsverzeichnis
COPY package.json pnpm-lock.yaml ./
# Installiere die Abhängigkeiten
RUN pnpm install
# Kopiere den Rest der Anwendung in das Arbeitsverzeichnis
COPY . .
# Setze die Umgebungsvariable, um pnpm zu verwenden
ENV PNPM_HOME="/usr/local/lib/node_modules/pnpm"
ENV PATH="$PNPM_HOME:$PATH"
# Exponiere den Port, auf dem die Anwendung läuft
EXPOSE 3000 EXPOSE 3000
CMD ["sh", "-c", "ORIGIN=https://tatort.innovation-hub-niedersachsen.de node build/index.js"] CMD ["sh", "-c", "ORIGIN=https://tatort.innovation-hub-niedersachsen.de node build/index.js"]

6754
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -17,45 +17,37 @@
"test:e2e": "playwright test" "test:e2e": "playwright test"
}, },
"devDependencies": { "devDependencies": {
"@eslint/compat": "^1.2.8", "@eslint/compat": "^1.2.5",
"@eslint/js": "^9.24.0", "@eslint/js": "^9.18.0",
"@google/model-viewer": "^4.0.0", "@playwright/test": "^1.49.1",
"@playwright/test": "^1.51.1", "@sveltejs/adapter-auto": "^4.0.0",
"@sveltejs/adapter-auto": "^6.0.0", "@sveltejs/kit": "^2.16.0",
"@sveltejs/adapter-node": "^5.2.12", "@sveltejs/vite-plugin-svelte": "^5.0.0",
"@sveltejs/kit": "^2.20.4",
"@sveltejs/vite-plugin-svelte": "^5.0.3",
"@tailwindcss/forms": "^0.5.10",
"@tailwindcss/vite": "^4.1.3",
"@testing-library/jest-dom": "^6.6.3", "@testing-library/jest-dom": "^6.6.3",
"@testing-library/svelte": "^5.2.7", "@testing-library/svelte": "^5.2.4",
"autoprefixer": "^10.4.21", "eslint": "^9.18.0",
"eslint": "^9.24.0", "eslint-config-prettier": "^10.0.1",
"eslint-config-prettier": "^10.1.1", "eslint-plugin-svelte": "^3.0.0",
"eslint-plugin-svelte": "^3.5.1",
"globals": "^16.0.0", "globals": "^16.0.0",
"jsdom": "^26.0.0", "jsdom": "^26.0.0",
"prettier": "^3.4.2",
"prettier-plugin-svelte": "^3.3.3",
"svelte": "^5.0.0",
"svelte-check": "^4.0.0",
"typescript": "^5.0.0",
"typescript-eslint": "^8.20.0",
"vite": "^6.2.5",
"vitest": "^3.0.0"
},
"dependencies": {
"@google/model-viewer": "^4.1.0",
"@sveltejs/adapter-node": "^5.2.12",
"@tailwindcss/forms": "^0.5.10",
"autoprefixer": "^10.4.21",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"minio": "^8.0.5", "minio": "^8.0.5",
"node": "^23.11.0",
"postcss": "^8.5.3", "postcss": "^8.5.3",
"prettier": "^3.5.3",
"prettier-plugin-svelte": "^3.3.3",
"prettier-plugin-tailwindcss": "^0.6.11",
"svelte": "^5.25.8",
"svelte-check": "^4.1.5",
"svelte-cubed": "^0.2.1", "svelte-cubed": "^0.2.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17"
"three": "~0.169.0",
"typescript": "^5.8.3",
"typescript-eslint": "^8.29.1",
"vite": "^6.2.5",
"vitest": "^3.1.1"
},
"pnpm": {
"onlyBuiltDependencies": [
"esbuild",
"node"
]
} }
} }

4737
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -40,7 +40,7 @@
</p> </p>
</div> </div>
{/if} {/if}
{#if data.user.admin} <!--{#if data.user.admin}
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4"> <div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
<div <div
class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white" class="flex h-11 w-11 items-center justify-center rounded-lg bg-gray-50 group-hover:bg-white"
@@ -66,7 +66,7 @@
</a> </a>
<p class="mt-1 text-gray-600">Stelle einen weiteren Tatort für die Anwendung bereit.</p> <p class="mt-1 text-gray-600">Stelle einen weiteren Tatort für die Anwendung bereit.</p>
</div> </div>
{/if} {/if}-->
{#if data.user.admin} {#if data.user.admin}
<div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4"> <div class="group relative rounded-lg p-6 text-sm leading-6 hover:bg-gray-50 w-1/4">
<div <div

View File

@@ -4,13 +4,14 @@
} }
</style> </style>
<script> <script lang="ts">
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { page } from '$app/stores';
/** /**
* @type any[] * @type any[]
*/ */
let list = []; let list: any[] = [];
//$: list; //$: list;
onMount(async () => { onMount(async () => {
@@ -34,6 +35,39 @@
list = list.concat(objs); list = list.concat(objs);
} }
}); });
async function delete_item(ev: Event) {
let delete_item = window.confirm("Bist du sicher?");
if (delete_item) {
const target = ev.currentTarget as HTMLElement | null;
if (!target) return;
let filename = target.id.split('del__')[1]
// delete request
// --------------
let url = `/api/list/${filename}`
console.log(`--- ${filename} + ${url}`)
try {
const response = await fetch(url,
{method: 'DELETE'}
)
if (response.status == 204) {
setTimeout(() => {window.location.reload()}, 500)
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message);
} else {
console.log(error);
}
}
}
}
</script> </script>
<div class="-z-10 bg-white"> <div class="-z-10 bg-white">
@@ -61,7 +95,17 @@
/> />
</svg> </svg>
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900">{item.name}</p> <span class="text-sm font-semibold leading-6 text-gray-900">{item.name}</span>
<!-- Delete button -->
<button
style="padding: 2px"
id="del__{item.name}"
on:click|preventDefault={delete_item}
aria-label="Vorgang {item.name} löschen"
>
<svg
height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m8 3v1 1h1v-1h4v1h1v-1-1zm-4 3v1h14v-1zm2 2v11h1 9v-1-10h-1v10h-8v-10z" fill="#373737" transform="translate(1 1)"/></svg>
</button>
</div> </div>
</div> </div>
<div class="hidden sm:flex sm:flex-col sm:items-end"> <div class="hidden sm:flex sm:flex-col sm:items-end">

View File

@@ -4,21 +4,44 @@
} }
</style> </style>
<script> <script lang="ts">
import { onMount } from 'svelte'; import { onMount, tick } from 'svelte';
import shortenFileSize from '$lib/helper/shortenFileSize'; import shortenFileSize from '$lib/helper/shortenFileSize';
import { page } from '$app/stores'; import { page } from '$app/stores';
import timeElapsed from '$lib/helper/timeElapsed'; import timeElapsed from '$lib/helper/timeElapsed';
/** @type {import('./$types').PageData} */ import Alert from '$lib/components/ui/Alert.svelte';
/** export let data; */ import Button from '$lib/components/ui/Button.svelte';
import Modal from '$lib/components/ui/Modal/Modal.svelte';
import ModalTitle from '$lib/components/ui/Modal/ModalTitle.svelte';
import ModalContent from '$lib/components/ui/Modal/ModalContent.svelte';
import ModalFooter from '$lib/components/ui/Modal/ModalFooter.svelte';
/** /** @type {import('./$types').PageData} */
* @type any[] export let data;
*/
let list = []; interface ListItem {
name: string;
size: number;
lastModified: string | number | Date;
show_button?: boolean;
// add other properties as needed
}
let list: ListItem[] = [];
$: list; $: list;
let open = false;
$: open;
let inProgress = false;
$: inProgress;
let err = false;
$: err;
let rename_input;
$: rename_input;
onMount(async () => { onMount(async () => {
const response = await fetch('/api/list/' + $page.params.vorgang); const response = await fetch('/api/list/' + $page.params.vorgang);
const stream = response.body; const stream = response.body;
@@ -38,8 +61,104 @@
console.log(objs); console.log(objs);
list = list.concat(objs); list = list.concat(objs);
list = list.map((item) => {
item.show_button = true;
return item;
})
} }
}); });
function uploadSuccessful() {
console.log('reset');
open = false;
}
function defocus_element(i: number) {
let item = list[i]
let text_field_id = `label__${item.name}`
let text_field = document.getElementById(text_field_id)
if (text_field) {
text_field.setAttribute("contenteditable", "false")
text_field.textContent = item.name;
}
// reshow button
list[i].show_button = true;
return;
}
async function handle_input(ev: KeyboardEvent, i: number) {
let item = list[i]
if (ev.key == "Escape") {
let text_field_id = `label__${item.name}`
let text_field = document.getElementById(text_field_id)
if (text_field) {
text_field.setAttribute("contenteditable", "false");
text_field.textContent = item.name;
}
// reshow button
item.show_button = true
return;
}
if (ev.key == "Enter") {
console.log('--- hitted')
let name_field = ev.currentTarget as HTMLElement | null;
let new_name = name_field ? (name_field.textContent || (name_field as any).innerText || '') : '';
if (new_name == '') {
alert('Bitte einen gültigen Namen eingeben.');
ev.preventDefault();
return;
}
// actual upload
// -------------
// to prevent from item being selected
ev.preventDefault();
// construct PUT URL
const url = $page.url
console.log(url);
let data_obj: { new_name: string; old_name: string } = { new_name: '', old_name: '' };
data_obj["new_name"] = new_name
data_obj["old_name"] = ev.currentTarget && (ev.currentTarget as HTMLElement).id ? (ev.currentTarget as HTMLElement).id.split('__')[1] : ''
open = true;
inProgress = true;
const response = await fetch(url,
{method: 'PUT', body: JSON.stringify( data_obj )
})
inProgress = false;
if (!response.ok) {
err = true;
if (response.status == 400) {
let json_res = await response.json()
// alert(json_res['msg'])
return;
}
throw new Error(`Fehlgeschlagen: ${response.status}`)
} else {
uploadSuccessful();
setTimeout(() => {window.location.reload()}, 500)
}
// --- upload finished ---
return;
}
}
</script> </script>
<div class="-z-10 bg-white"> <div class="-z-10 bg-white">
@@ -48,7 +167,7 @@
</div> </div>
<div class="mx-auto flex justify-center max-w-7xl h-full"> <div class="mx-auto flex justify-center max-w-7xl h-full">
<ul class="divide-y divide-gray-100"> <ul class="divide-y divide-gray-100">
{#each list as item} {#each list as item, i}
<li> <li>
<a <a
href="/view/{$page.params.vorgang}/{item.name}" href="/view/{$page.params.vorgang}/{item.name}"
@@ -70,7 +189,94 @@
/> />
</svg> </svg>
<div class="min-w-0 flex-auto"> <div class="min-w-0 flex-auto">
<p class="text-sm font-semibold leading-6 text-gray-900">{item.name}</p> <span
style="display: inline-block; min-width: 5px;"
id="label__{item.name}"
class="text-sm font-semibold leading-6 text-gray-900"
contenteditable={! item.show_button}
role="textbox"
tabindex="0"
aria-label="Dateiname bearbeiten"
on:focusout={() => {
defocus_element(i);
}}
on:keydown|stopPropagation={
// event needed to identify ID
// TO-DO: check if event is needed or if index is sufficient
async (ev) => {handle_input(ev, i)}
}
>{item.name}</span>
<!-- https://iconduck.com/icons/192863/edit-rename -->
{#if item.show_button}
<button
style="padding: 2px"
id="edit__{item.name}"
aria-label="Datei umbenennen"
on:click|preventDefault={(ev) => {
let text_field_id = `label__${item.name}`
let text_field = document.getElementById(text_field_id)
if (text_field) {
text_field.setAttribute("contenteditable", "true");
text_field.focus();
text_field.textContent = '';
}
// hide button
item.show_button = false
}}
>
<svg
height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m14.996094 3-11.9921878 11.992188h-.0039062v4.007812h1 2 1.0078125v-.0039l11.9921875-11.9921938-.001953-.0019531.001953-.0019531-4-4-.002.00195zm-1.998047 3.4121094 2.589844 2.5898437-7.587891 7.5878909v-1.589844h-1-1v-1-.589844zm-7.998047 7.9980466v1.589844h1 1v1 .589844l-.4101562.410156h-1.5898438l-1-1v-1.589844z" fill="#373737" transform="translate(1 1)"/></svg>
</button>
{/if}
<button
style="padding: 2px"
id="del__{item.name}"
on:click|preventDefault={async (ev) => {
let delete_item = window.confirm("Bist du sicher?");
if (delete_item) {
// bucket: tatort, name: <vorgang>/item-name
let vorgang = $page.params.vorgang
let filename = '';
if (ev && ev.currentTarget && (ev.currentTarget as HTMLElement).id) {
filename = (ev.currentTarget as HTMLElement).id.split('del__')[1];
}
// delete request
// --------------
let url = new URL($page.url);
url.pathname += `/${filename}`;
console.log(`--- ${vorgang} + ${filename} + ${url}`)
try {
const response = await fetch(url,
{method: 'DELETE'}
)
if (response.status == 204) {
setTimeout(() => {window.location.reload()}, 500)
}
} catch (error) {
if (error instanceof Error) {
console.log(error.message)
} else {
console.log(error)
}
}
}
}}
aria-label="Datei löschen"
>
<svg
height="20" width="20" xmlns="http://www.w3.org/2000/svg"><path d="m8 3v1 1h1v-1h4v1h1v-1-1zm-4 3v1h14v-1zm2 2v11h1 9v-1-10h-1v10h-8v-10z" fill="#373737" transform="translate(1 1)"/></svg>
</button>
<p class="mt-1 truncate text-xs leading-5 text-gray-500"> <p class="mt-1 truncate text-xs leading-5 text-gray-500">
{shortenFileSize(item.size)} {shortenFileSize(item.size)}
</p> </p>
@@ -89,4 +295,17 @@
{/each} {/each}
</ul> </ul>
</div> </div>
<Modal {open}
><ModalTitle>Umbenennen</ModalTitle><ModalContent>
{#if inProgress}
<p class="py-2 mb-1">Vorgang läuft...</p>
{/if}
{#if err}
<Alert class="w-full" type="error">Fehler beim Umbenennen</Alert>
{/if}
</ModalContent>
<ModalFooter><Button disabled={inProgress} on:click={uploadSuccessful}>Ok</Button></ModalFooter>
</Modal>
</div> </div>

View File

@@ -0,0 +1,36 @@
import { client } from '$lib/minio';
import { json } from '@sveltejs/kit';
// rename operation
export async function PUT({ request }) {
const data = await request.json();
console.log(`--- ${request.url.split('/').at(-1)} +++ ${JSON.stringify(data)}`);
// Vorgang
let vorgang = request.url.split('/').at(-1)
// prepare copy, incl. check if new name exists already
let old_name = data["old_name"]
let src_full_path = `/tatort/${vorgang}/${old_name}`
let new_name = `${vorgang}/${data["new_name"]}`
try {
let file_stats = await client.statObject('tatort', new_name)
return json({ msg: 'Die Datei existiert bereits.' }, { status: 400 })
} catch (error) {
// continue operation
console.log('continue operation')
}
// actual copy operation
await client.copyObject('tatort', new_name, src_full_path)
// delete
await client.removeObject('tatort', `${vorgang}/${old_name}`)
// return success or failure
return json({ success: 'success' }, { status: 200 });
};

View File

@@ -0,0 +1,13 @@
import { client } from '$lib/minio';
import { json } from '@sveltejs/kit';
export async function DELETE({ request }) {
let url_fragments = request.url.split('/')
let item = url_fragments.at(-1);
let vorgang = url_fragments.at(-2);
await client.removeObject('tatort', `${vorgang}/${item}`)
return new Response(null, { status: 204 });
};

View File

@@ -73,8 +73,24 @@
<p class="block text-sm leading-6 text-red-900 mt-2">{form.error.description}</p> <p class="block text-sm leading-6 text-red-900 mt-2">{form.error.description}</p>
{/if} {/if}
</div> </div>
<label for="code">
<span >Zugangscode (optional) </span>
</label>
<div class="mt-2">
<div
>
<input
type="text"
id="code"
/>
</div>
</div>
</div> </div>
</div> </div>
<div class="mt-6 flex items-center justify-end gap-x-6"> <div class="mt-6 flex items-center justify-end gap-x-6">
<button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button> <button type="button" class="text-sm font-semibold leading-6 text-gray-900">Cancel</button>
<Button <Button

View File

@@ -4,7 +4,7 @@ import { createReadStream } from 'fs';
/** import Minio from 'minio'; */ /** import Minio from 'minio'; */
import { Readable } from 'stream'; import { Readable } from 'stream';
/** const MINIO_ACCESS_KEY = 'tMhLrfog47lMm0HZ'; */ /** const MINIO_ACCESS_KEY = 'tMhLrfog47lMm0HZ'; */
/** import { client } from '$lib/minio'; */ import { client } from '$lib/minio';
import { fail } from '@sveltejs/kit'; import { fail } from '@sveltejs/kit';
function isRequiredFieldValid(value) { function isRequiredFieldValid(value) {
@@ -22,6 +22,7 @@ export const actions = {
const vorgang = data.get('vorgang'); const vorgang = data.get('vorgang');
const name = data.get('name'); const name = data.get('name');
const type = data.get('type'); const type = data.get('type');
const code = data.get('zugangscode');
const fileName = data.get('fileName'); const fileName = data.get('fileName');
let objectName = `${vorgang}/${name}`; let objectName = `${vorgang}/${name}`;
@@ -35,6 +36,8 @@ export const actions = {
const url = await client.presignedPutObject('tatort', objectName); const url = await client.presignedPutObject('tatort', objectName);
// enter code
return { url }; return { url };
}, },
validate: async ({ request }) => { validate: async ({ request }) => {
@@ -42,6 +45,7 @@ export const actions = {
const data = Object.fromEntries(requestData); const data = Object.fromEntries(requestData);
const vorgang = data.vorgang; const vorgang = data.vorgang;
const name = data.name; const name = data.name;
const zugangscode = data.zugangscode;
let success = true; let success = true;
let err = {}; let err = {};
@@ -57,6 +61,12 @@ export const actions = {
success = false; success = false;
} }
if (isRequiredFieldValid(zugangscode)) err.zugangscode = null;
else {
err.zugangscode = 'Das Feld Zugangscode darf nicht leer bleiben.';
success = false;
}
if (success) return { success }; if (success) return { success };
return fail(400, err); return fail(400, err);
@@ -99,5 +109,8 @@ export const actions = {
return { etag, error }; return { etag, error };
//await writeFile(filePath, Buffer.from(await data.file.arrayBuffer())); //await writeFile(filePath, Buffer.from(await data.file.arrayBuffer()));
},
get_code: async ({ request }) => {
console.log('TO-DO');
} }
}; };

View File

@@ -14,6 +14,16 @@
let open = false; let open = false;
let inProgress = false; let inProgress = false;
let vorgang = ''; let vorgang = '';
const code_len = 8;
let zugangscode = ''
let zugangscode_prev = '';
$: zugangscode = Math.random().toString(36).slice(2, 2+code_len);
let case_existing = undefined;
$: case_existing = false;
let name = ''; let name = '';
/** @type {?string}*/ /** @type {?string}*/
let etag = null; let etag = null;
@@ -29,6 +39,7 @@
let data = new FormData(); let data = new FormData();
data.append('vorgang', vorgang); data.append('vorgang', vorgang);
data.append('name', name); data.append('name', name);
data.append('zugangscode', zugangscode)
const response = await fetch('?/validate', { method: 'POST', body: data }); const response = await fetch('?/validate', { method: 'POST', body: data });
/** @type {import('@sveltejs/kit').ActionResult} */ /** @type {import('@sveltejs/kit').ActionResult} */
const result = deserialize(await response.text()); const result = deserialize(await response.text());
@@ -47,6 +58,11 @@
success = false; success = false;
} }
if (!(await check_valid_glb_file())) {
formErrors = { file: 'Keine gültige .GLD-Datei', ...formErrors };
success = false;
}
return success; return success;
} }
@@ -54,6 +70,7 @@
let data = new FormData(); let data = new FormData();
data.append('vorgang', vorgang); data.append('vorgang', vorgang);
data.append('name', name); data.append('name', name);
data.append('zugangscode', zugangscode)
if (files?.length === 1) { if (files?.length === 1) {
data.append('type', files[0].type); data.append('type', files[0].type);
data.append('fileName', files[0].name); data.append('fileName', files[0].name);
@@ -97,6 +114,74 @@
name = ''; name = '';
files = null; files = null;
} }
// `val` is hex string
function swap_endian(val) {
// from https://www.geeksforgeeks.org/bit-manipulation-swap-endianness-of-a-number/
let leftmost_byte = (val & eval(0x000000FF)) >> 0;
let left_middle_byte = (val & eval(0x0000FF00)) >> 8;
let right_middle_byte = (val & eval(0x00FF0000)) >> 16;
let rightmost_byte = (val & eval(0xFF000000)) >> 24;
leftmost_byte <<= 24;
left_middle_byte <<= 16;
right_middle_byte <<= 8;
rightmost_byte <<= 0;
let res = (leftmost_byte | left_middle_byte | right_middle_byte | rightmost_byte)
return res
}
async function check_valid_glb_file() {
// GLD Header, magic value 0x46546C67, identifies data as binary glTF, 4 bytes
// little endian!
const GLD_MAGIC = 0x46546C67;
// big endian!
let file = files[0];
let file_header = file.slice(0, 4)
let header_bytes = await file_header.bytes()
let file_header_hex = '0x' + header_bytes.toHex().toString();
if (GLD_MAGIC == swap_endian(file_header_hex)) {
return true;
} else {
return false;
}
}
// return true or false
async function case_exists(case_no) {
// ping `/(angemeldet)/view` with caseNumber in POST body
let url = '/view'
let data = new FormData();
data.append('caseNumber', case_no);
const response = await fetch(url, {
method: 'POST',
body: data,
redirect: 'error'
})
const res_json = await response.json();
const status = res_json.status;
// aktualisiere Zugangscode mit
if (status == 303) {
// TO-DO: check existing code
console.log(`+ existiert`)
case_existing = true;
return;
}
case_existing = false;
}
</script> </script>
<div class="mx-auto max-w-2xl"> <div class="mx-auto max-w-2xl">
@@ -131,12 +216,18 @@
id="vorgang" id="vorgang"
autocomplete={vorgang} autocomplete={vorgang}
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6" class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
on:input={() => case_exists(vorgang)}
/> />
</div> </div>
</div> </div>
{#if formErrors?.vorgang} {#if formErrors?.vorgang}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.vorgang}</p> <p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.vorgang}</p>
{/if} {/if}
{#if case_existing && (vorgang.length > 0) }
<span>Datei wird zum existierenden Vorgang hinzugefügt.</span>
{:else if (vorgang.length > 0) }
<span>Neuer Vorgang wird angelegt.</span>
{/if}
</div> </div>
<div> <div>
@@ -166,6 +257,32 @@
{/if} {/if}
</div> </div>
<div>
<label for="zugangscode" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex"
>{#if formErrors?.zugangscode}
<span class="inline-block mr-1"><Exclamation /></span>
{/if} Zugangscode</span
></label
>
<div class="mt-2">
<div
class="flex rounded-md shadow-sm ring-1 ring-inset ring-gray-300 focus-within:ring-2 focus-within:ring-inset focus-within:ring-indigo-600"
>
<input
bind:value={zugangscode}
type="text"
name="zugangscode"
id="zugangscode"
class="block flex-1 border-0 bg-transparent py-1.5 pl-1 text-gray-900 placeholder:text-gray-400 focus:ring-0 sm:text-sm sm:leading-6"
/>
</div>
</div>
{#if formErrors?.code}
<p class="block text-sm leading-6 text-red-900 mt-2">{formErrors.code}</p>
{/if}
</div>
<div class="col-span-full"> <div class="col-span-full">
<label for="file" class="block text-sm font-medium leading-6 text-gray-900" <label for="file" class="block text-sm font-medium leading-6 text-gray-900"
><span class="flex" ><span class="flex"

View File

@@ -22,6 +22,6 @@ export const actions = {
error: { caseNumber: 'Die Vorgangsnummer existiert in dieser Anwendung nicht.' } error: { caseNumber: 'Die Vorgangsnummer existiert in dieser Anwendung nicht.' }
}); });
} }
throw redirect(303, `/list/${caseNumber}`); redirect(303, `/list/${caseNumber}`);
} }
}; };

View File

@@ -1,14 +1,8 @@
<style>
model-viewer {
height: 100%;
width: 100%;
}
</style>
<script> <script>
import { preloadCode } from '$app/navigation';
import Panel from '$lib/components/ui/Panel.svelte'; import Panel from '$lib/components/ui/Panel.svelte';
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import Button from '$lib/components/ui/Button.svelte';
export let data; export let data;
onMount(() => { onMount(() => {
@@ -17,8 +11,33 @@
let progress = 0; let progress = 0;
let hideProgressScreen = false; let hideProgressScreen = false;
let cameraOrbit = '0deg 0deg 100%';
let cameraTarget = '0m 0m 0m';
let fieldOfView = '10deg';
let cameraAzimuth = 0;
let cameraPolar = 0;
let frontView = cameraAzimuth === 0 && cameraPolar === 0 ? true : false;
let topView = cameraAzimuth === 0 && cameraPolar === 90 ? true : false;
let cameraZoom = 100;
let xRotation = 0;
let yRotation = 0;
let zRotation = 0;
/**
* @type {any}
*/
let modelViewer;
$: style = `width: ${progress}%`; $: style = `width: ${progress}%`;
/**
* @param {any} detail
*/
function onProgress({ detail }) { function onProgress({ detail }) {
progress = Math.ceil(detail.totalProgress * 100.0); progress = Math.ceil(detail.totalProgress * 100.0);
if (progress == 100) { if (progress == 100) {
@@ -27,29 +46,176 @@
}, 250); }, 250);
} else hideProgressScreen = false; } else hideProgressScreen = false;
} }
function onResetView() {
console.log(
'show cameraOrbit:',
modelViewer.getCameraOrbit(),
modelViewer.cameraOrbit,
modelViewer.getDimensions()
);
console.log(
'Camera-orbit: ',
modelViewer.getAttribute('camera-orbit'),
'camera-target: ',
modelViewer.getAttribute('camera-target'),
'object-rotation: ',
modelViewer.getAttribute('rotation')
);
cameraAzimuth = 0;
cameraPolar = 0;
cameraZoom = 100;
modelViewer.cameraOrbit = cameraOrbit;
modelViewer.cameraTarget = cameraTarget;
modelViewer.fieldOfView = fieldOfView;
cameraAzimuth = 0;
cameraPolar = 0;
cameraZoom = 100;
fieldOfView = '10deg';
}
/**
* @param {number} azimuth
* @param {number} polar
* @param {number} zoom
*/
function updateCameraOrbit(azimuth, polar, zoom) {
cameraAzimuth = azimuth;
cameraPolar = polar;
cameraZoom = zoom;
}
</script> </script>
<div class="h-full w-full bg-neutral-100 p-4"> <div class="h-full model w-full bg-neutral-200 p-4 transition-all delay-250">
<model-viewer <model-viewer
src={data.url} src={data.url}
camera-controls bind:this={modelViewer}
field-of-view="auto" touch-action="pan-y"
field-of-view={fieldOfView}
min-field-of-view="1deg"
max-field-of-view="10deg" max-field-of-view="10deg"
min-field-of-view="0.1deg" camera-controls
orientation={`${xRotation}deg ${yRotation}deg ${zRotation}deg`}
camera-target="0m 0m 0m"
camera-orbit={`${cameraAzimuth}deg ${cameraPolar}deg ${cameraZoom}%`}
on:progress={onProgress} on:progress={onProgress}
> >
<!--Buttons zum Steuern-->
<div
class=" p-4 flex z-10 absolute bottom-0"
class:opacity-0={!hideProgressScreen}
class:hidden={!hideProgressScreen}
>
<div class="flex flex-col bg-white/50">
<!--3 Buttons-->
<div class="p-2">
<Button
on:click={() => {
updateCameraOrbit(0, 0, cameraZoom);
}}
type="button"
variant="white"
align="left"
class="relative cursor-default justify-start py-2 pl-3 pr-10 text-left "
>
Draufsicht
</Button>
<Button
on:click={() => {
onResetView();
}}
type="button"
variant="white"
align="left"
class="relative cursor-default justify-start py-2 pl-3 pr-10 text-left"
>
Werte zurücksetzen
</Button>
<Button
on:click={() => {
updateCameraOrbit(0, 90, cameraZoom);
}}
type="button"
variant="white"
align="left"
class="relative cursor-default justify-start py-2 pl-3 pr-10 text-left"
>
Frontansicht
</Button>
</div>
<!--Horizontal-->
<div class="p-2">
<label for="cazimuth">Horizontal drehen (Azimut):</label>
<input id="cazimuth" type="range" min="0" max="360" bind:value={cameraAzimuth} />
</div>
<!--Vertikal-->
<div class="p-2">
<label class=" mb-2" for="polarSlider">Vertikal drehen (Polar):</label>
<input id="polarSlider" type="range" min="0" max="90" bind:value={cameraPolar} />
</div>
<!--Zoom/Distanz-->
<div class="p-2">
<label for="cZoom">Abstand (zoom):</label>
<input id="cZoom" type="range" min="0" max="100" bind:value={cameraZoom} />
</div>
</div>
<!--Rotationen-->
<div class="flex flex-col ml-2 p-4 bg-white/50">
Modell rotieren lassen:
<br />
Modell auf dem Kopf? -> y auf 270°
<div class="p-2">
<label class="" for="polarSlider">Rotation X:</label>
<input id="polarSlider" type="range" min="0" max="360" bind:value={xRotation} />
</div>
<div class="p-2">
<label class="" for="polarSlider">Rotation Y:</label>
<input id="polarSlider" type="range" min="0" max="360" bind:value={yRotation} />
</div>
<div class="p-2">
<label class="" for="polarSlider">Rotation Z:</label>
<input id="polarSlider" type="range" min="0" max="360" bind:value={zRotation} />
</div>
</div>
</div>
<!--Loading-->
<div <div
slot="progress-bar" slot="progress-bar"
class="flex items-center justify-center h-full w-full transition-all delay-250" class="flex items-center justify-center h-5/6 w-full transition-all delay-250"
class:opacity-0={hideProgressScreen} class:opacity-0={hideProgressScreen}
class:hidden={hideProgressScreen} class:hidden={hideProgressScreen}
> >
<Panel class="w-72 bg-gray-50 flex items-center flex-col" <Panel class="w-72 bg-gray-50 flex items-center flex-col"
><p class="mb-5">Loading {progress}%</p> ><p class="mb-5">Loading {progress}%</p>
<div class="h-1 w-full bg-neutral-200 dark:bg-neutral-600"> <div class="h-1 w-full bg-neutral-200 dark:bg-neutral-600">
<div class="h-1 bg-blue-500" {style} ></div> <div class="h-1 bg-blue-500" {style}></div>
</div></Panel </div></Panel
> >
</div> </div>
</model-viewer> </model-viewer>
</div> </div>
<style>
model-viewer {
height: 100%;
width: 100%;
}
/* .vertical-slider {
writing-mode: bt-lr; /* Schreibt von unten nach oben (Vertikale Darstellung)
transform: rotate(270deg); /* Slider um 270° drehen
height: 200px;
} */
.model {
height: calc(100%-84px);
}
/* .active-border {
border: 2px blue solid;
} */
</style>

View File

@@ -33,3 +33,31 @@ export async function GET({ params }) {
} }
}); });
} }
export async function DELETE({ params }) {
const vorgang = params.vorgang
const object_list = await new Promise((resolve, reject) => {
const res = []
const items_str = client.listObjects('tatort', vorgang, true)
items_str.on('data', (obj) => {
res.push(obj.name)
})
items_str.on('error', reject)
items_str.on('end', async () => {
resolve(res)
})
console.log(`+++ ${vorgang}`)
})
await client.removeObjects('tatort', object_list)
return new Response(null, { status: 204 });
};

View File

@@ -1,12 +1,18 @@
/** import adapter from '@sveltejs/adapter-auto'; */
import adapter from '@sveltejs/adapter-node'; import adapter from '@sveltejs/adapter-node';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte'; import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
/** @type {import('@sveltejs/kit').Config} */
const config = { const config = {
// Consult https://svelte.dev/docs/kit/integrations
// for more information about preprocessors
preprocess: vitePreprocess(), preprocess: vitePreprocess(),
kit: { kit: {
adapter: adapter() // adapter-auto only supports some environments, see https://svelte.dev/docs/kit/adapter-auto for a list.
} // If your environment is not supported, or you settled on a specific environment, switch out the adapter.
// See https://svelte.dev/docs/kit/adapters for more information about adapters.
adapter: adapter()
}
}; };
export default config; export default config;

View File

@@ -1,10 +1,9 @@
import tailwindcss from '@tailwindcss/vite';
import { svelteTesting } from '@testing-library/svelte/vite'; import { svelteTesting } from '@testing-library/svelte/vite';
import { sveltekit } from '@sveltejs/kit/vite'; import { sveltekit } from '@sveltejs/kit/vite';
import { defineConfig } from 'vite'; import { defineConfig } from 'vite';
export default defineConfig({ export default defineConfig({
plugins: [tailwindcss(), sveltekit()], plugins: [sveltekit()],
test: { test: {
workspace: [ workspace: [
{ {