zwischenstand
@@ -1,12 +1,15 @@
|
||||
// @ts-check
|
||||
import { defineConfig } from 'astro/config';
|
||||
|
||||
import svelte from '@astrojs/svelte';
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite';
|
||||
|
||||
const isProd = process.env.NODE_ENV === 'production';
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
base: isProd ? '/buzzer/' : '/',
|
||||
site: 'https://home.iten.pro',
|
||||
|
||||
integrations: [svelte()],
|
||||
|
||||
vite: {
|
||||
|
||||
BIN
webpage/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
webpage/public/favicon-96x96.png
Normal file
|
After Width: | Height: | Size: 2.2 KiB |
|
Before Width: | Height: | Size: 655 B After Width: | Height: | Size: 15 KiB |
@@ -1,9 +1,17 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 128 128">
|
||||
<path d="M50.4 78.5a75.1 75.1 0 0 0-28.5 6.9l24.2-65.7c.7-2 1.9-3.2 3.4-3.2h29c1.5 0 2.7 1.2 3.4 3.2l24.2 65.7s-11.6-7-28.5-7L67 45.5c-.4-1.7-1.6-2.8-2.9-2.8-1.3 0-2.5 1.1-2.9 2.7L50.4 78.5Zm-1.1 28.2Zm-4.2-20.2c-2 6.6-.6 15.8 4.2 20.2a17.5 17.5 0 0 1 .2-.7 5.5 5.5 0 0 1 5.7-4.5c2.8.1 4.3 1.5 4.7 4.7.2 1.1.2 2.3.2 3.5v.4c0 2.7.7 5.2 2.2 7.4a13 13 0 0 0 5.7 4.9v-.3l-.2-.3c-1.8-5.6-.5-9.5 4.4-12.8l1.5-1a73 73 0 0 0 3.2-2.2 16 16 0 0 0 6.8-11.4c.3-2 .1-4-.6-6l-.8.6-1.6 1a37 37 0 0 1-22.4 2.7c-5-.7-9.7-2-13.2-6.2Z" />
|
||||
<style>
|
||||
path { fill: #000; }
|
||||
@media (prefers-color-scheme: dark) {
|
||||
path { fill: #FFF; }
|
||||
}
|
||||
</style>
|
||||
</svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><style>
|
||||
#light-icon {
|
||||
display: inline;
|
||||
}
|
||||
#dark-icon {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
#light-icon {
|
||||
display: none;
|
||||
}
|
||||
#dark-icon {
|
||||
display: inline;
|
||||
}
|
||||
}
|
||||
</style><g id="light-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(31.25,0,0,31.25,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32" fill="#000000" viewBox="0 0 256 256" id="svg1" sodipodi:docname="siren.svg" inkscape:version="1.4 (86a8ad7, 2024-10-11)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs1"></defs><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="36.46875" inkscape:cx="16" inkscape:cy="16" inkscape:window-width="3440" inkscape:window-height="1369" inkscape:window-x="1672" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg1"></sodipodi:namedview><path d="m 232,176 v 24 a 16,16 0 0 1 -16,16 H 40 A 16,16 0 0 1 24,200 V 176 A 16,16 0 0 1 40,160 V 128 A 88,88 0 0 1 128.67,40 C 176.82,40.36 216,80.29 216,129 v 31 a 16,16 0 0 1 16,16 z" id="path5"></path><path d="M 216,200 V 176 H 40 v 24 z" id="path7" style="fill:#b3b3b3"></path><path d="M 56,160 H 200 V 129 C 200,89 167.95,56.29 128.55,56 H 128 a 72,72 0 0 0 -72,72 z" id="path6" style="fill:#ff0000"></path><path d="M 137.34,72.11 A 8,8 0 1 0 134.7,87.89 C 153.67,91.08 168,108.32 168,128 a 8,8 0 0 0 16,0 c 0,-27.4 -20.07,-51.43 -46.68,-55.89 z" id="path4"></path><path d="M 50.34,45.66 A 8.0044488,8.0044488 0 0 0 61.66,34.34 l -8,-8 A 8.0044488,8.0044488 0 0 0 42.34,37.66 Z" id="path3"></path><path d="m 200,48 a 8,8 0 0 0 5.66,-2.34 l 8,-8 A 8.0044488,8.0044488 0 0 0 202.34,26.34 l -8,8 A 8,8 0 0 0 200,48 Z" id="path2"></path><path d="M 120,16 V 8 a 8,8 0 0 1 16,0 v 8 a 8,8 0 0 1 -16,0 z" id="path1"></path></svg></g></g></svg></g><g id="dark-icon"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" xmlns:xlink="http://www.w3.org/1999/xlink" width="1000" height="1000"><g><g transform="matrix(31.25,0,0,31.25,0,0)"><svg xmlns="http://www.w3.org/2000/svg" version="1.1" width="32" height="32" fill="#000000" viewBox="0 0 256 256" id="svg1" sodipodi:docname="siren_dark.svg" inkscape:version="1.4 (86a8ad7, 2024-10-11)" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" xmlns:svg="http://www.w3.org/2000/svg"><defs id="defs1"></defs><sodipodi:namedview id="namedview1" pagecolor="#ffffff" bordercolor="#000000" borderopacity="0.25" inkscape:showpageshadow="2" inkscape:pageopacity="0.0" inkscape:pagecheckerboard="0" inkscape:deskcolor="#d1d1d1" inkscape:zoom="36.46875" inkscape:cx="16" inkscape:cy="16" inkscape:window-width="3440" inkscape:window-height="1369" inkscape:window-x="1672" inkscape:window-y="-8" inkscape:window-maximized="1" inkscape:current-layer="svg1"></sodipodi:namedview><path d="m 232,176 v 24 a 16,16 0 0 1 -16,16 H 40 A 16,16 0 0 1 24,200 V 176 A 16,16 0 0 1 40,160 V 128 A 88,88 0 0 1 128.67,40 C 176.82,40.36 216,80.29 216,129 v 31 a 16,16 0 0 1 16,16 z" id="path5" style="fill:#ffffff"></path><path d="M 216,200 V 176 H 40 v 24 z" id="path7" style="fill:#b3b3b3"></path><path d="M 56,160 H 200 V 129 C 200,89 167.95,56.29 128.55,56 H 128 a 72,72 0 0 0 -72,72 z" id="path6" style="fill:#ff0000"></path><path d="M 137.34,72.11 A 8,8 0 1 0 134.7,87.89 C 153.67,91.08 168,108.32 168,128 a 8,8 0 0 0 16,0 c 0,-27.4 -20.07,-51.43 -46.68,-55.89 z" id="path4" style="fill:#ffffff"></path><path d="M 50.34,45.66 A 8.0044488,8.0044488 0 0 0 61.66,34.34 l -8,-8 A 8.0044488,8.0044488 0 0 0 42.34,37.66 Z" id="path3" style="fill:#ffffff"></path><path d="m 200,48 a 8,8 0 0 0 5.66,-2.34 l 8,-8 A 8.0044488,8.0044488 0 0 0 202.34,26.34 l -8,8 A 8,8 0 0 0 200,48 Z" id="path2" style="fill:#ffffff"></path><path d="M 120,16 V 8 a 8,8 0 0 1 16,0 v 8 a 8,8 0 0 1 -16,0 z" id="path1" style="fill:#ffffff"></path></svg></g></g></svg></g></svg>
|
||||
|
Before Width: | Height: | Size: 749 B After Width: | Height: | Size: 4.4 KiB |
21
webpage/public/site.webmanifest
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "Edis Buzzer",
|
||||
"short_name": "Buzzer",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/web-app-manifest-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
},
|
||||
{
|
||||
"src": "/web-app-manifest-512x512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "maskable"
|
||||
}
|
||||
],
|
||||
"theme_color": "#ffffff",
|
||||
"background_color": "#ffffff",
|
||||
"display": "standalone"
|
||||
}
|
||||
BIN
webpage/public/web-app-manifest-192x192.png
Normal file
|
After Width: | Height: | Size: 4.7 KiB |
BIN
webpage/public/web-app-manifest-512x512.png
Normal file
|
After Width: | Height: | Size: 16 KiB |
@@ -6,7 +6,7 @@
|
||||
|
||||
onMount(async () => {
|
||||
performHardwareCheck();
|
||||
|
||||
|
||||
if ($isBluetoothSupported) {
|
||||
const { restoreSession } = await import("../lib/bluetooth");
|
||||
await restoreSession();
|
||||
@@ -22,9 +22,11 @@
|
||||
</div>
|
||||
{:else if !$isBluetoothSupported}
|
||||
<div
|
||||
class="fixed lg:h-screen inset-0 flex flex-col items-center justify-center p-0 lg:p-4 z-[100] bg-white lg:bg-transparent" style="hyphens:auto;">
|
||||
class="fixed lg:h-screen inset-0 flex flex-col items-center justify-center p-0 lg:p-4 z-[100] bg-white lg:bg-transparent"
|
||||
style="hyphens:auto;"
|
||||
>
|
||||
<div
|
||||
class="w-full h-full lg:h-auto lg:max-w-md bg-red-50 lg:border border-red-600 lg:shadow-xl lg:rounded-lg p-6 lg:p-8 text-red-600 flex flex-col justify-center"
|
||||
class="w-full h-full lg:h-auto lg:max-w-md bg-red-50 shadow-red-500/30 lg:border border-red-600 lg:shadow-xl lg:rounded-lg p-6 lg:p-8 text-red-600 flex flex-col justify-center"
|
||||
>
|
||||
<h1 class="text-2xl font-bold mb-2 text-center">Dein Browser ist... suboptimal</h1>
|
||||
<div class="text-center text-7xl md:text-9xl font-bold mb-4">🥺</div>
|
||||
@@ -34,10 +36,12 @@
|
||||
Leider unterstützt dein Browser die benötigten Bluetooth-Funktionen nicht. Bitte versuche
|
||||
es mit einem aktuellen <span class="font-semibold">Chrome</span>
|
||||
oder einem andern Chromium-basierten Browser.
|
||||
<span class="font-semibold">Winzigweich Kante</span> soll gerüchteweise auch Chromium-basiert sein...
|
||||
<span class="font-semibold">Winzigweich Kante</span>
|
||||
soll gerüchteweise auch Chromium-basiert sein...
|
||||
</p>
|
||||
<p>
|
||||
Rundreise auf iOS unterstützt Bluetooth leider nicht, aber du kannst es mit einem vernünftigen Gerät oder Browser versuchen.
|
||||
Rundreise auf iOS unterstützt Bluetooth leider nicht, aber du kannst es mit einem
|
||||
vernünftigen Gerät oder Browser versuchen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -15,17 +15,22 @@
|
||||
WarningCircleIcon,
|
||||
} from "phosphor-svelte";
|
||||
import {
|
||||
isFetchingRemote,
|
||||
isTransferingRemote,
|
||||
transferStats,
|
||||
transferDetails,
|
||||
buzzerAudioFiles,
|
||||
localAudioFiles,
|
||||
syncStateMap,
|
||||
fsInfo,
|
||||
} from "../lib/store";
|
||||
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
import { tagEditorState } from "../lib/store";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { deleteRemoteFile } from "../lib/transport";
|
||||
import { deleteLocalFile } from "../lib/db";
|
||||
import { refreshRemote, refreshLocal } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
|
||||
export let file: BuzzerFile;
|
||||
export let type: "local" | "buzzer" = "buzzer";
|
||||
@@ -37,7 +42,7 @@
|
||||
$: myIndex = selectedFiles.findIndex((f) => f.name === file.name);
|
||||
|
||||
$: state = (() => {
|
||||
if (!file.selected || !$isFetchingRemote) return "default";
|
||||
if (!file.selected || !$isTransferingRemote) return "default";
|
||||
if (file.name === $transferStats.currentFileName) return "active";
|
||||
if (myIndex < currentIndex) return "done";
|
||||
if (myIndex > currentIndex) return "pending";
|
||||
@@ -93,7 +98,7 @@
|
||||
})();
|
||||
|
||||
function toggleSelection() {
|
||||
if ($isFetchingRemote) return;
|
||||
if ($isTransferingRemote) return;
|
||||
|
||||
if (type === "buzzer") {
|
||||
buzzerAudioFiles.update((files) =>
|
||||
@@ -110,6 +115,35 @@
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function handleDeleteClick() {
|
||||
if (!confirm(`Möchten Sie die Datei "${file.name}" wirklich löschen?`)) {
|
||||
menuOpen = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === "buzzer") {
|
||||
try {
|
||||
const basePath = $fsInfo?.audioPath || "/lfs/a";
|
||||
const fullPath = `${basePath}/${file.name}`;
|
||||
await deleteRemoteFile(fullPath);
|
||||
addToast(`Datei ${file.name} erfolgreich vom Buzzer gelöscht.`, "success");
|
||||
await refreshRemote();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen:", error);
|
||||
addToast("Fehler beim Löschen der Datei auf dem Buzzer.", "error");
|
||||
}
|
||||
} else {
|
||||
try {
|
||||
await deleteLocalFile(file.name);
|
||||
addToast(`Lokale Datei ${file.name} gelöscht.`, "success");
|
||||
await refreshLocal();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen:", error);
|
||||
}
|
||||
}
|
||||
menuOpen = false;
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window on:click={() => (menuOpen = false)} />
|
||||
@@ -128,14 +162,14 @@
|
||||
class="relative z-10 w-full text-left flex-1 px-3 py-1 pr-16 flex items-center border-l-4 transition-colors border-b border-b-border-card
|
||||
{file.selected ? 'border-l-blue-600' : 'border-l-transparent'}
|
||||
{file.selected && state !== 'active' ? 'bg-blue-50' : ''}
|
||||
{!$isFetchingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
||||
{!$isFetchingRemote && !file.selected
|
||||
{!$isTransferingRemote && file.selected ? 'hover:bg-blue-100 cursor-pointer' : ''}
|
||||
{!$isTransferingRemote && !file.selected
|
||||
? 'hover:bg-slate-100 hover:border-l-blue-200 cursor-pointer'
|
||||
: ''}
|
||||
{$isFetchingRemote ? 'cursor-default' : ''}
|
||||
{$isTransferingRemote ? 'cursor-default' : ''}
|
||||
{state === 'pending' ? 'grayscale opacity-80' : ''}"
|
||||
on:click={toggleSelection}
|
||||
disabled={$isFetchingRemote}
|
||||
disabled={$isTransferingRemote}
|
||||
>
|
||||
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" />
|
||||
|
||||
@@ -198,10 +232,7 @@
|
||||
<button
|
||||
class="menu-btn danger"
|
||||
title="Löschen"
|
||||
on:click|stopPropagation={() => {
|
||||
console.log("Delete", file.name);
|
||||
menuOpen = false;
|
||||
}}
|
||||
on:click|stopPropagation={handleDeleteClick}
|
||||
>
|
||||
<TrashIcon class="list-menu-icon" />
|
||||
</button>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
||||
import { deleteSelectedLocalFiles } from "../lib/sync";
|
||||
import { deleteSelectedLocalFiles, deleteSelectedRemoteFiles } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { updateLocalAudioCrc } from "../lib/tagHandler";
|
||||
@@ -143,12 +143,11 @@
|
||||
class="menu-btn danger"
|
||||
disabled={selectedFileCount === 0}
|
||||
on:click={() => {
|
||||
if (type === "buzzer")
|
||||
addToast(
|
||||
"Löschen von Dateien auf dem Buzzer wird derzeit nicht unterstützt.",
|
||||
"error",
|
||||
);
|
||||
else deleteSelectedLocalFiles();
|
||||
if (type === "buzzer") {
|
||||
deleteSelectedRemoteFiles();
|
||||
} else {
|
||||
deleteSelectedLocalFiles();
|
||||
}
|
||||
closeMenu();
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -2,8 +2,13 @@
|
||||
import { updateFile } from "../lib/tagHandler";
|
||||
import { refreshLocal, refreshRemote } from "../lib/sync";
|
||||
import { fade, slide } from "svelte/transition";
|
||||
import { localAudioFiles, buzzerAudioFiles, fsInfo } from "../lib/store";
|
||||
import type { MetadataTags } from "../lib/types";
|
||||
import {
|
||||
localAudioFiles,
|
||||
buzzerAudioFiles,
|
||||
fsInfo,
|
||||
syncStateMap,
|
||||
} from "../lib/store";
|
||||
import { type MetadataTags, SyncState } from "../lib/types";
|
||||
import {
|
||||
XIcon,
|
||||
CaretLeftIcon,
|
||||
@@ -15,6 +20,7 @@
|
||||
PencilIcon,
|
||||
} from "phosphor-svelte";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
|
||||
export let show = false;
|
||||
export let type: "local" | "buzzer" = "buzzer";
|
||||
@@ -29,7 +35,7 @@
|
||||
|
||||
$: autoApplyIcon = applyToBoth ? CheckSquareIcon : SquareIcon;
|
||||
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
|
||||
$: fileList = $activeStore;
|
||||
$: fileList = $activeStore || [];
|
||||
|
||||
$: if (show && initialFileName !== lastOpenedName) {
|
||||
if (initialFileName) {
|
||||
@@ -44,7 +50,7 @@
|
||||
lastOpenedName = null;
|
||||
}
|
||||
|
||||
$: currentIndex = fileList.findIndex((f) => f.name === currentFileName);
|
||||
$: currentIndex = (fileList || []).findIndex((f) => f.name === currentFileName);
|
||||
$: currentFile = fileList[currentIndex];
|
||||
$: hasDraft = currentFile ? drafts[currentFile.name] !== undefined : false;
|
||||
$: hasAnyDrafts = Object.keys(drafts).length > 0;
|
||||
@@ -53,6 +59,13 @@
|
||||
$: activeTags = activeDraft ? activeDraft.tags : currentFile?.metaTags || {};
|
||||
$: activeName = activeDraft ? activeDraft.newName : currentFile?.name || "";
|
||||
|
||||
$: syncStatus = (currentFileName && $syncStateMap[type]?.[currentFileName]) || { state: SyncState.UNKNOWN, linkedFiles: [] };
|
||||
$: isDuplicate = syncStatus.state === SyncState.DUPLICATE;
|
||||
|
||||
$: if (isDuplicate) {
|
||||
applyToBoth = false;
|
||||
}
|
||||
|
||||
$: maxFilenameLength = $fsInfo ? $fsInfo.maxPathLength - $fsInfo.audioPath.length - 2 : 30;
|
||||
|
||||
function closeEditor() {
|
||||
@@ -120,13 +133,21 @@
|
||||
const newName = draft.newName;
|
||||
|
||||
try {
|
||||
await updateFile(oldName, newName, currentFile.sysTags, draft.tags, type);
|
||||
await updateFile(oldName, newName, currentFile.sysTags, draft.tags, type, applyToBoth);
|
||||
addToast(`Datei ${newName} gespeichert.`, "success");
|
||||
delete drafts[oldName];
|
||||
drafts = drafts;
|
||||
if (oldName !== newName) currentFileName = newName;
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
|
||||
if (applyToBoth) {
|
||||
// Wenn auf beide angewendet, beide Seiten neu laden
|
||||
await refreshLocal();
|
||||
await refreshRemote();
|
||||
} else {
|
||||
// Ansonsten nur die aktive Seite
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast("Fehler beim Speichern.", "error");
|
||||
}
|
||||
@@ -138,14 +159,20 @@
|
||||
for (const [oldName, draft] of Object.entries(drafts)) {
|
||||
const file = fileList.find((f) => f.name === oldName);
|
||||
if (file) {
|
||||
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type);
|
||||
await updateFile(oldName, draft.newName, file.sysTags, draft.tags, type, applyToBoth);
|
||||
savedCount++;
|
||||
}
|
||||
}
|
||||
drafts = {};
|
||||
addToast(`${savedCount} Dateien gespeichert.`, "success");
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
|
||||
if (applyToBoth) {
|
||||
await refreshLocal();
|
||||
await refreshRemote();
|
||||
} else {
|
||||
if (type === "local") await refreshLocal();
|
||||
if (type === "buzzer") await refreshRemote();
|
||||
}
|
||||
} catch (error) {
|
||||
addToast("Fehler beim Speichern.", "error");
|
||||
}
|
||||
@@ -398,15 +425,27 @@
|
||||
<FloppyDiskIcon class="btn-icon" /> Speichern
|
||||
</button>
|
||||
</div>
|
||||
<button
|
||||
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700"
|
||||
on:click={() => (applyToBoth = !applyToBoth)}
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: "Diese Option ist deaktiviert, da ein Duplikat-Konflikt vorliegt. Lösen Sie den Konflikt, um Änderungen auf beiden Seiten anwenden zu können.",
|
||||
pos: "top",
|
||||
variant: "danger",
|
||||
disabled: !isDuplicate,
|
||||
}}
|
||||
class:grayscale={isDuplicate}
|
||||
class:cursor-not-allowed={isDuplicate}
|
||||
>
|
||||
<svelte:component
|
||||
this={autoApplyIcon}
|
||||
class="btn-icon {applyToBoth ? 'text-blue-600' : 'text-slate-400'}"
|
||||
/> Auch {type === "buzzer" ? "lokal" : "auf dem Buzzer"} anwenden
|
||||
</button>
|
||||
<button
|
||||
class="menu-btn bg-slate-50 hover:bg-slate-100 !justify-start text-slate-700 w-full"
|
||||
on:click={() => (applyToBoth = !applyToBoth)}
|
||||
disabled={isDuplicate}
|
||||
>
|
||||
<svelte:component
|
||||
this={autoApplyIcon}
|
||||
class="btn-icon {applyToBoth ? 'text-blue-600' : 'text-slate-400'}"
|
||||
/> Auch {type === "buzzer" ? "lokal" : "auf dem Buzzer"} anwenden
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import FileList from "./FileList.svelte";
|
||||
import DeviceInfo from "./DeviceInfo.svelte";
|
||||
import { refreshRemote } from "../lib/sync";
|
||||
import { transferStats, isFetchingRemote, pairedDevices, activeDeviceId } from "../lib/store";
|
||||
import { transferStats, isTransferingRemote, pairedDevices, activeDeviceId } from "../lib/store";
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
import TransferProgress from "./TransferProgress.svelte";
|
||||
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
||||
@@ -25,12 +25,12 @@
|
||||
refreshRemote();
|
||||
}
|
||||
|
||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
||||
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||
// Transfer startet oder läuft
|
||||
showOverlay = true;
|
||||
isTransferFinished = false;
|
||||
clearTimeout(overlayTimeout);
|
||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
||||
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||
// Transfer wurde soeben abgeschlossen
|
||||
isTransferFinished = true;
|
||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
<script lang="ts">
|
||||
import { fade } from 'svelte/transition';
|
||||
import { XIcon } from "phosphor-svelte";
|
||||
import { isFetchingRemote, transferStats, transferDetails } from '../lib/store';
|
||||
import { isTransferingRemote, transferStats, transferDetails } from '../lib/store';
|
||||
import { SETTINGS } from '../lib/settings';
|
||||
|
||||
let showOverlay = false;
|
||||
let isTransferFinished = false;
|
||||
let overlayTimeout: ReturnType<typeof setTimeout>;
|
||||
|
||||
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
||||
$: if ($isTransferingRemote && $transferStats.overallTotal > 0) {
|
||||
showOverlay = true;
|
||||
isTransferFinished = false;
|
||||
clearTimeout(overlayTimeout);
|
||||
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
|
||||
} else if (showOverlay && !$isTransferingRemote && $transferStats.overallDone > 0) {
|
||||
isTransferFinished = true;
|
||||
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
|
||||
}
|
||||
|
||||
@@ -1,15 +1,30 @@
|
||||
<!-- MainLayout.astro -->
|
||||
---
|
||||
import "../styles/app.css";
|
||||
---
|
||||
|
||||
<!-- MainLayout.astro -->
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link
|
||||
rel="icon"
|
||||
type="image/png"
|
||||
href={`${import.meta.env.BASE_URL}favicon-96x96.png`}
|
||||
sizes="96x96"
|
||||
/>
|
||||
<link rel="icon" type="image/svg+xml" href={`${import.meta.env.BASE_URL}favicon.svg`} />
|
||||
<link rel="shortcut icon" href={`${import.meta.env.BASE_URL}favicon.ico`} />
|
||||
<link
|
||||
rel="apple-touch-icon"
|
||||
sizes="180x180"
|
||||
href={`${import.meta.env.BASE_URL}apple-touch-icon.png`}
|
||||
/>
|
||||
<meta name="apple-mobile-web-app-title" content="Edis Buzzer" />
|
||||
<link rel="manifest" href={`${import.meta.env.BASE_URL}site.webmanifest`} />
|
||||
<title>Edis Buzzer</title>
|
||||
</head>
|
||||
<body class="bg-surface text-on-surface subpixel-antialiased transition-colors duration-300">
|
||||
<body class="bg-surface text-on-surface antialiased transition-colors duration-300">
|
||||
<slot />
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -3,12 +3,13 @@ export const SETTINGS = {
|
||||
connectionKey: 'buzzer_connection_state'
|
||||
},
|
||||
bluetooth: {
|
||||
connectionTimeoutMs: 10000 // Timeout für den Verbindungsaufbau
|
||||
connectionTimeoutMs: 3000, // Timeout für den Verbindungsaufbau
|
||||
appleMaxInflight: 15, // iOS erlaubt nur wenige unbestätigte Nachrichten, daher begrenzen wir die Anzahl der gleichzeitig gesendeten Frames
|
||||
},
|
||||
ui: {
|
||||
toastDurationMs: 5000,
|
||||
transferUpdateIntervalMs: 300,
|
||||
kbpsCalculationWindowMs: 5500,
|
||||
transferUpdateIntervalMs: 1000,
|
||||
kbpsCalculationWindowMs: 1000,
|
||||
transferOverlayPersistMs: 4000,
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -75,7 +75,7 @@ export const buzzerSysFiles = writable<BuzzerFile[]>([]);
|
||||
export const localAudioFiles = writable<BuzzerFile[]>([]);
|
||||
|
||||
// Ladezustände getrennt nach Quelle
|
||||
export const isFetchingRemote = writable<boolean>(false);
|
||||
export const isTransferingRemote = writable<boolean>(false);
|
||||
export const isFetchingLocal = writable<boolean>(false);
|
||||
|
||||
// Persistenz des letzten Verbindungsziels (nur im Browser nutzbar)
|
||||
@@ -189,7 +189,7 @@ export function resetRemote(): void {
|
||||
activeDeviceId.set(null);
|
||||
buzzerAudioFiles.set([]);
|
||||
buzzerSysFiles.set([]);
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
resetTransferStats();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { get } from 'svelte/store';
|
||||
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isFetchingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
|
||||
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isTransferingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
|
||||
import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport';
|
||||
import type { BuzzerFile } from './types';
|
||||
import { getFile, putFile } from './transport';
|
||||
import { getFile, putFile, deleteRemoteFile } from './transport';
|
||||
import { addToast } from './toast';
|
||||
import { getLocalFiles, deleteLocalFile, getLocalFile } from './db';
|
||||
import { parseAudioFileTags } from './tagHandler';
|
||||
@@ -24,7 +24,7 @@ function mapToBuzzerFile(rawFile: any): BuzzerFile {
|
||||
export async function refreshRemote() {
|
||||
if (!get(isConnected)) return;
|
||||
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
try {
|
||||
await requestProtocolInfo();
|
||||
await requestFSInfo();
|
||||
@@ -64,7 +64,7 @@ export async function refreshRemote() {
|
||||
console.error("Fehler beim Aktualisieren der Buzzer-Daten:", error);
|
||||
addToast("Fehler beim Laden der Daten vom Buzzer: " + (error instanceof Error ? error.message : "Unbekannter Fehler"), "error");
|
||||
} finally {
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,7 @@ export async function downloadSelectedFiles() {
|
||||
bulkStartTime: bulkStart
|
||||
}));
|
||||
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
@@ -142,7 +142,7 @@ export async function downloadSelectedFiles() {
|
||||
...s,
|
||||
overallDone: s.overallTotal,
|
||||
}));
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,10 @@ export async function deleteSelectedLocalFiles() {
|
||||
|
||||
if (selectedFiles.length === 0) return;
|
||||
|
||||
if (!confirm(`Möchten Sie wirklich ${selectedFiles.length} lokale Datei(en) löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
for (const file of selectedFiles) {
|
||||
await deleteLocalFile(file.name);
|
||||
@@ -163,6 +167,33 @@ export async function deleteSelectedLocalFiles() {
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteSelectedRemoteFiles() {
|
||||
const files = get(buzzerAudioFiles).filter(f => f.selected);
|
||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
||||
|
||||
if (files.length === 0) return;
|
||||
|
||||
if (!confirm(`Möchten Sie wirklich ${files.length} Datei(en) auf dem Buzzer löschen?`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
isTransferingRemote.set(true);
|
||||
try {
|
||||
for (const file of files) {
|
||||
const fullPath = `${pathPrefix}/${file.name}`;
|
||||
console.debug(`Lösche Datei auf dem Buzzer: ${fullPath}`);
|
||||
await deleteRemoteFile(fullPath);
|
||||
}
|
||||
addToast(`${files.length} Datei(en) auf dem Buzzer gelöscht.`, "success");
|
||||
await refreshRemote();
|
||||
} catch (error) {
|
||||
console.error("Fehler beim Löschen auf dem Buzzer:", error);
|
||||
addToast("Fehler beim Löschen: " + (error instanceof Error ? error.message : "Unbekannt"), "error");
|
||||
} finally {
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
|
||||
export async function uploadSelectedFiles() {
|
||||
const files = get(localAudioFiles).filter(f => f.selected);
|
||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
||||
@@ -183,7 +214,7 @@ export async function uploadSelectedFiles() {
|
||||
}));
|
||||
|
||||
// Wir nutzen isFetchingRemote als generischen "Transfer aktiv"-Trigger für das UI TODO: Namensänderung in isTransferring?
|
||||
isFetchingRemote.set(true);
|
||||
isTransferingRemote.set(true);
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
@@ -216,6 +247,6 @@ export async function uploadSelectedFiles() {
|
||||
...s,
|
||||
overallDone: s.overallTotal, // Schließt den Ladebalken visuell sauber ab
|
||||
}));
|
||||
isFetchingRemote.set(false);
|
||||
isTransferingRemote.set(false);
|
||||
}
|
||||
}
|
||||
@@ -2,9 +2,9 @@ import { getLocalFiles, saveLocalFile, deleteLocalFile } from './db';
|
||||
import type { SystemTags, MetadataTags } from './types';
|
||||
import { addToast } from './toast';
|
||||
import { crc32 } from './protocol/crc32';
|
||||
import { getTags, putTags } from './transport';
|
||||
import { getTags, putTags, renameRemoteFile } from './transport';
|
||||
import { get } from 'svelte/store';
|
||||
import { fsInfo } from './store';
|
||||
import { fsInfo, syncStateMap } from './store';
|
||||
|
||||
function getEmptyTags() {
|
||||
return {
|
||||
@@ -206,25 +206,24 @@ export async function updateRemoteFile(
|
||||
sysTags: SystemTags,
|
||||
newMetaTags: MetadataTags
|
||||
): Promise<void> {
|
||||
// Da das Binärprotokoll noch kein echtes "Rename"-Kommando hat,
|
||||
// blockieren wir Dateinamen-Änderungen für den Buzzer vorerst.
|
||||
const currentFsInfo = get(fsInfo);
|
||||
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
||||
const oldFullPath = `${basePath}/${oldName}`;
|
||||
const newFullPath = `${basePath}/${newName}`;
|
||||
|
||||
if (oldName !== newName) {
|
||||
throw new Error("Das Umbenennen von Dateien direkt auf dem Buzzer wird noch nicht unterstützt. Bitte speichere nur die Tags.");
|
||||
console.log(`TagHandler: Benenne Datei auf dem Buzzer um (${oldFullPath} -> ${newFullPath})`);
|
||||
await renameRemoteFile(oldFullPath, newFullPath);
|
||||
}
|
||||
|
||||
// 1. Den binären TLV-Block inkl. Footer bauen
|
||||
const newTagsBuffer = buildTagBlock(sysTags, newMetaTags);
|
||||
const tagsBlob = new Blob([newTagsBuffer]);
|
||||
|
||||
// 2. Zielpfad ermitteln
|
||||
const currentFsInfo = get(fsInfo);
|
||||
const basePath = currentFsInfo?.audioPath || "/lfs/a";
|
||||
const fullPath = `${basePath}/${oldName}`;
|
||||
|
||||
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${fullPath}...`);
|
||||
console.log(`Sende modifizierte Tags (${tagsBlob.size} Bytes) an ${newFullPath}...`);
|
||||
|
||||
// 3. Über das Protokoll an den Buzzer senden
|
||||
await putTags(tagsBlob, fullPath);
|
||||
await putTags(tagsBlob, newFullPath);
|
||||
}
|
||||
|
||||
export async function updateFile(
|
||||
@@ -232,12 +231,43 @@ export async function updateFile(
|
||||
newName: string,
|
||||
sysTags: SystemTags,
|
||||
newMetaTags: MetadataTags,
|
||||
type: "local" | "buzzer"
|
||||
type: "local" | "buzzer",
|
||||
applyToBoth: boolean = false
|
||||
): Promise<void> {
|
||||
if (type === "local") {
|
||||
await updateLocalFile(oldName, newName, sysTags, newMetaTags);
|
||||
if (applyToBoth) {
|
||||
try {
|
||||
const sMap = get(syncStateMap);
|
||||
const syncStatus = sMap.local[oldName];
|
||||
if (syncStatus && syncStatus.linkedFiles.length > 0) {
|
||||
const remoteOldName = syncStatus.linkedFiles[0];
|
||||
await updateRemoteFile(remoteOldName, newName, sysTags, newMetaTags);
|
||||
} else {
|
||||
console.warn(`Keine verknüpfte Remote-Datei für '${oldName}' gefunden. Überspringe Remote-Update.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim synchronen Aktualisieren der Buzzer-Datei:", e);
|
||||
addToast("Fehler beim Update der Buzzer-Datei: " + (e instanceof Error ? e.message : "Unbekannt"), "error");
|
||||
}
|
||||
}
|
||||
} else {
|
||||
await updateRemoteFile(oldName, newName, sysTags, newMetaTags);
|
||||
if (applyToBoth) {
|
||||
try {
|
||||
const sMap = get(syncStateMap);
|
||||
const syncStatus = sMap.buzzer[oldName];
|
||||
if (syncStatus && syncStatus.linkedFiles.length > 0) {
|
||||
const localOldName = syncStatus.linkedFiles[0];
|
||||
await updateLocalFile(localOldName, newName, sysTags, newMetaTags);
|
||||
} else {
|
||||
throw new Error(`Keine verknüpfte lokale Datei für '${oldName}' gefunden.`);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Fehler beim synchronen Aktualisieren der lokalen Datei:", e);
|
||||
addToast("Fehler beim Update der lokalen Datei: " + (e instanceof Error ? e.message : "Unbekannt"), "error");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,15 @@
|
||||
import { buildLSRequest, buildProtocolInfoRequest, buildFSInfoRequest, setLsResolver, buildFileGetRequest, setFileGetResolver, buildTagsGetRequest, uploadState } from './protocol/parser';
|
||||
import { crc32 } from './protocol/crc32';
|
||||
import { get } from 'svelte/store';
|
||||
import { protocolInfo, transferStats } from './store';
|
||||
import { protocolInfo, transferStats, } from './store';
|
||||
import { DATA, FRAME } from './protocol/constants';
|
||||
import { isConnected, resetRemote } from './store';
|
||||
import { SETTINGS } from './settings';
|
||||
|
||||
const isMac = navigator.userAgent.includes('Macintosh') || navigator.userAgent.includes('Mac OS X');
|
||||
const MAX_INFLIGHT = isMac ? SETTINGS.bluetooth.appleMaxInflight : Infinity; // iOS erlaubt nur wenige unbestätigte Nachrichten
|
||||
|
||||
console.log("Transport: Max Inflight Frames =", MAX_INFLIGHT);
|
||||
|
||||
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
||||
let currentSender: FrameSender | null = null;
|
||||
@@ -183,6 +189,10 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
||||
});
|
||||
}
|
||||
|
||||
if (uploadState.credits > MAX_INFLIGHT) {
|
||||
uploadState.credits = MAX_INFLIGHT;
|
||||
}
|
||||
|
||||
const chunkLen = Math.min(maxChunkSize, fileData.length - offset);
|
||||
const chunkData = fileData.subarray(offset, offset + chunkLen);
|
||||
|
||||
@@ -252,6 +262,51 @@ export async function putFile(fileBlob: Blob, remotePath: string, fileNameForUI:
|
||||
}
|
||||
}
|
||||
|
||||
export async function deleteRemoteFile(fullPath: string): Promise<void> {
|
||||
const pathBytes = new TextEncoder().encode(fullPath);
|
||||
const payloadLength = 1 + 1 + pathBytes.length; // data_type(1) + path_length(1) + path
|
||||
|
||||
const buffer = new ArrayBuffer(3 + payloadLength);
|
||||
const view = new DataView(buffer);
|
||||
const uint8View = new Uint8Array(buffer);
|
||||
|
||||
view.setUint8(0, FRAME.REQUEST);
|
||||
view.setUint16(1, payloadLength, true);
|
||||
|
||||
view.setUint8(3, 0x24); // BUZZ_DATA_RM_FILE
|
||||
view.setUint8(4, pathBytes.length);
|
||||
uint8View.set(pathBytes, 5);
|
||||
|
||||
await sendFrame(buffer);
|
||||
// Kurze Wartezeit, bis der Parser SUCCESS verarbeitet und der Flash fertig ist
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
export async function renameRemoteFile(oldFullPath: string, newFullPath: string): Promise<void> {
|
||||
const oldBytes = new TextEncoder().encode(oldFullPath);
|
||||
const newBytes = new TextEncoder().encode(newFullPath);
|
||||
|
||||
const payloadLength = 1 + 1 + 1 + oldBytes.length + newBytes.length; // data_type + 2x len + 2x string
|
||||
|
||||
const buffer = new ArrayBuffer(3 + payloadLength);
|
||||
const view = new DataView(buffer);
|
||||
const uint8View = new Uint8Array(buffer);
|
||||
|
||||
view.setUint8(0, FRAME.REQUEST);
|
||||
view.setUint16(1, payloadLength, true);
|
||||
|
||||
view.setUint8(3, 0x25); // BUZZ_DATA_RENAME_FILE
|
||||
view.setUint8(4, oldBytes.length);
|
||||
view.setUint8(5, newBytes.length);
|
||||
|
||||
uint8View.set(oldBytes, 6);
|
||||
uint8View.set(newBytes, 6 + oldBytes.length);
|
||||
|
||||
await sendFrame(buffer);
|
||||
// Kurze Wartezeit, bis der Parser SUCCESS verarbeitet und der Flash fertig ist
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
}
|
||||
|
||||
export async function putTags(tagsBlob: Blob, remotePath: string): Promise<void> {
|
||||
if (isFileTransferring) {
|
||||
throw new Error("Ein Dateitransfer läuft bereits.");
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<!-- index.astro -->
|
||||
---
|
||||
import AppGuard from "../components/AppGuard.svelte";
|
||||
import MainLayout from "../layouts/MainLayout.astro";
|
||||
import Header from "../components/Header.svelte";
|
||||
import MainGrid from "../components/MainGrid.svelte";
|
||||
---
|
||||
|
||||
<!-- index.astro -->
|
||||
<MainLayout>
|
||||
<AppGuard client:load>
|
||||
<Header client:load/>
|
||||
<MainGrid client:load/>
|
||||
<AppGuard client:only="svelte">
|
||||
<Header client:only="svelte" />
|
||||
<MainGrid client:only="svelte" />
|
||||
</AppGuard>
|
||||
</MainLayout>
|
||||
</MainLayout>
|
||||
|
||||
@@ -52,7 +52,7 @@
|
||||
}
|
||||
|
||||
.card-title {
|
||||
@apply font-light font-features-['smcp'] tracking-tight;
|
||||
@apply font-light font-features-['smcp'] tracking-tighter;
|
||||
}
|
||||
|
||||
/* .card-body {
|
||||
|
||||