zwischenstand

This commit is contained in:
2026-03-18 15:05:45 +01:00
parent 7c7f19a4b7
commit ff63dda086
29 changed files with 626 additions and 269 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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();
}}
>

View File

@@ -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}

View File

@@ -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);

View File

@@ -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);
}