Lokale Dateien implementiert

This commit is contained in:
2026-03-14 20:13:55 +01:00
parent 1a4a22eafd
commit b5eb3b56c0
9 changed files with 249 additions and 178 deletions

View File

@@ -1,13 +1,21 @@
<script lang="ts">
import FileListItem from "./FileListItem.svelte";
import { buzzerAudioFiles } from "../lib/store";
import type { BuzzerFile } from "../lib/types";
import FileListItem from "./FileListItem.svelte";
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
export let type: "local" | "buzzer" = "buzzer";
export let type: "local" | "buzzer" = "buzzer";
// Reaktive Zuweisung: Wechselt automatisch, falls sich 'type' zur Laufzeit ändert
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
</script>
<div>
{#each $buzzerAudioFiles as file, index(index)}
<FileListItem bind:file={file}/>
{/each}
<div class="flex flex-col w-full">
{#each $activeStore as file (file.name)}
<FileListItem {file} {type} />
{/each}
{#if $activeStore.length === 0}
<div class="p-4 text-center text-text-muted text-sm italic tracking-widest uppercase">
Keine Dateien vorhanden.
</div>
{/if}
</div>

View File

@@ -1,16 +1,53 @@
<script lang="ts">
import type { BuzzerFile } from "../lib/types";
import { FileAudioIcon } from "phosphor-svelte";
export let file: BuzzerFile;
import type { BuzzerFile } from "../lib/types";
import { FileAudioIcon } from "phosphor-svelte";
import { isFetchingRemote, transferStats, transferDetails, buzzerAudioFiles } from "../lib/store";
import { SETTINGS } from "../lib/settings";
export let file: BuzzerFile;
// Status-Berechnung für die Queue
$: selectedFiles = $buzzerAudioFiles.filter(f => f.selected);
$: currentIndex = selectedFiles.findIndex(f => f.name === $transferStats.currentFileName);
$: myIndex = selectedFiles.findIndex(f => f.name === file.name);
$: state = (() => {
if (!file.selected || !$isFetchingRemote) return 'default';
if (file.name === $transferStats.currentFileName) return 'active';
if (myIndex < currentIndex) return 'done';
if (myIndex > currentIndex) return 'pending';
return 'default';
})();
function toggleSelection() {
if ($isFetchingRemote) return; // Blockiert Änderungen während des Transfers
file.selected = !file.selected;
buzzerAudioFiles.update(files => files); // Triggert die Reaktivität im Svelte-Store
}
</script>
<div>
<div class="relative overflow-hidden">
{#if state === 'active'}
<div
class="absolute top-0 left-0 h-full bg-indigo-100 z-0"
style="width: {$transferDetails.filePercent}%; transition: {$transferDetails.filePercent === 0 ? 'none' : `width ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
></div>
{/if}
<button
class="w-full text-left flex-1 px-3 py-1 flex items-center cursor-pointer border-l-4 transition-colors border-b border-b-border-card
{file.selected ? 'border-l-blue-600 bg-blue-50 hover:bg-blue-100' : 'border-l-transparent hover:bg-slate-100 hover:border-l-blue-200'} "
on:click={() => {file.selected = !file.selected;}}
>
class="relative z-10 w-full text-left flex-1 px-3 py-1 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 ? 'hover:bg-slate-100 hover:border-l-blue-200 cursor-pointer' : ''}
{$isFetchingRemote ? 'cursor-default' : ''}
{state === 'pending' ? 'grayscale opacity-80' : ''}"
on:click={toggleSelection}
disabled={$isFetchingRemote}
>
<FileAudioIcon class="text-blue-600 mr-3 w-5 h-5" />
<div class="flex flex-col">
<div class="flex flex-col">
<span class="font-light">
{file.name || "Unbekannte Datei"}
{#if file.metaTags?.t}
@@ -23,7 +60,7 @@
{parseFloat((file.size/1024).toFixed(1))}&thinsp;kB
</span>
<span>
{#if file.metaTags.a}<span class="text-text-muted"> | Author:</span>&thinsp;{file.metaTags.a}{/if}
{#if file.metaTags?.a}<span class="text-text-muted"> | Author:</span>&thinsp;{file.metaTags.a}{/if}
</span>
</div>
</div>
@@ -31,5 +68,4 @@
</div>
<style>
</style>

View File

@@ -25,7 +25,7 @@
transferDetails,
} from "../lib/store";
import { SETTINGS } from "../lib/settings";
import { fade } from "svelte/transition";
import TransferProgress from "./TransferProgress.svelte";
let showOverlay = false;
let isTransferFinished = false;
@@ -101,102 +101,7 @@
<div class="card-body transition-all duration-500 h-full" class:disconnected={!$isConnected}>
<DeviceInfo />
</div>
{#if showOverlay}
<div
class="absolute inset-0 z-10 bg-white/95 backdrop-blur-[2px] p-4 flex flex-col justify-end"
transition:fade={{ duration: 300 }}
>
{#if isTransferFinished}
<button
class="absolute top-2 right-2 p-1 text-slate-400 hover:text-slate-700 transition-colors"
on:click={closeOverlay}
aria-label="Overlay schließen"
>
<XIcon class="w-5 h-5" />
</button>
{/if}
<div class="w-full flex flex-col gap-1">
<div class="flex flex-col gap-1">
<div
class="relative w-full h-3 bg-slate-100 rounded-sm overflow-hidden shadow-inner shadow-sm"
>
<div
class="absolute top-0 left-0 h-full bg-gradient-to-b from-indigo-300 to-indigo-500"
style="width: {$transferDetails.filePercent}%; transition: {$transferDetails.filePercent ===
0
? 'none'
: `width ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
></div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500"
>
{$transferDetails.filePercent}%
</div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-white"
style="clip-path: inset(0 {100 -
$transferDetails.filePercent}% 0 0); transition: {$transferDetails.filePercent ===
0
? 'none'
: `clip-path ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
>
{$transferDetails.filePercent}%
</div>
</div>
<div class="flex justify-between text-[10px] px-1">
<span class="truncate max-w-[60%]">
{$transferStats.currentFileName || "Lade..."}
</span>
<span>{formatTime($transferDetails.fileEta)}</span>
</div>
</div>
<div class="flex flex-col gap-1">
<div
class="relative w-full h-3 bg-slate-100 rounded-sm overflow-hidden shadow-inner shadow-sm"
>
<div
class="absolute top-0 left-0 h-full bg-gradient-to-b from-indigo-300 to-indigo-500"
style="width: {$transferDetails.totalPercent}%; transition: {$transferDetails.totalPercent ===
0
? 'none'
: `width ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
></div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500"
>
{$transferDetails.totalPercent}%
</div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-white"
style="clip-path: inset(0 {100 -
$transferDetails.totalPercent}% 0 0); transition: {$transferDetails.totalPercent ===
0
? 'none'
: `clip-path ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
>
{$transferDetails.totalPercent}%
</div>
</div>
<div class="flex justify-between text-[10px] px-1">
<span>{$transferDetails.speedKbs} kB/s</span>
<span>{formatTime($transferDetails.totalEta)}</span>
</div>
</div>
<div class="flex justify-center mt-1">
<button
class="px-4 py-1.5 text-xs font-semibold text-red-600 bg-red-50 hover:bg-red-100 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={true}
>
Transfer abbrechen
</button>
</div>
</div>
</div>
{/if}
<TransferProgress />
</div>
</section>
@@ -208,9 +113,7 @@
</button>
</div>
<div class="card-body">
<div class="py-10 text-center">
<span class="text-slate-300 uppercase italic tracking-widest">Bibliothek leer</span>
</div>
<FileList type="local" />
</div>
</section>

View File

@@ -1,61 +1,106 @@
<script lang="ts">
import { transferDetails, transferStats } from "../lib/store";
import { fade, slide } from 'svelte/transition';
import { fade } from 'svelte/transition';
import { XIcon } from "phosphor-svelte";
import { isFetchingRemote, transferStats, transferDetails } from '../lib/store';
import { SETTINGS } from '../lib/settings';
// Formatiert Sekunden zu "M:SS m" oder "S s" mit schmalem Leerzeichen
$: formatTime = (seconds: number): string => {
if (seconds === Infinity || seconds > 3600) return '∞';
const narrowNbsp = '\u202F';
let showOverlay = false;
let isTransferFinished = false;
let overlayTimeout: ReturnType<typeof setTimeout>;
if (seconds >= 60) {
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m}:${s.toString().padStart(2, '0')}${narrowNbsp}m`;
}
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
showOverlay = true;
isTransferFinished = false;
clearTimeout(overlayTimeout);
} else if (showOverlay && !$isFetchingRemote && $transferStats.overallDone > 0) {
isTransferFinished = true;
overlayTimeout = setTimeout(closeOverlay, SETTINGS.ui.transferOverlayPersistMs);
}
return `${Math.floor(seconds)}${narrowNbsp}s`;
};
function closeOverlay() {
clearTimeout(overlayTimeout);
showOverlay = false;
isTransferFinished = false;
}
function formatTime(seconds: number): string {
if (!isFinite(seconds) || seconds < 0) return "Berechne...";
const m = Math.floor(seconds / 60);
const s = Math.floor(seconds % 60);
return `${m.toString().padStart(2, '0')}:${s.toString().padStart(2, '0')} Min.`;
}
</script>
<div class="mt-4 min-h-[100px]">
{#if $transferStats.bytesTotal > 0}
<div class="flex flex-col gap-3 mt-4 w-full" transition:slide={{ duration: 400 }}>
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[10px] tracking-tighter font-bold text-slate-500">
<span class="truncate">Datei: {$transferStats.currentFileName || 'Übertragung...'}</span>
<span>{formatTime($transferDetails.fileEta)}</span>
</div>
<div class="w-full h-2 bg-slate-100 rounded-full overflow-hidden shadow-inner">
<div
class="h-full bg-blue-500 transition-all duration-500 ease-out"
style="width: {$transferDetails.filePercent}%"
></div>
</div>
</div>
{#if showOverlay}
<div
class="absolute inset-0 z-10 bg-white/95 backdrop-blur-[2px] p-4 flex flex-col justify-end"
transition:fade={{ duration: 300 }}
>
{#if isTransferFinished}
<button
class="absolute top-2 right-2 p-1 text-slate-400 hover:text-slate-700 transition-colors"
on:click={closeOverlay}
aria-label="Overlay schließen"
>
<XIcon class="w-5 h-5" />
</button>
{/if}
<div class="flex flex-col gap-1">
<div class="flex justify-between text-[10px] tracking-tighter font-bold text-slate-500">
<span>Gesamtfortschritt</span>
<span>{formatTime($transferDetails.totalEta)}</span>
<div class="w-full flex flex-col gap-1">
<div class="flex flex-col gap-1">
<div class="relative w-full h-3 bg-slate-100 rounded-sm overflow-hidden shadow-inner shadow-sm">
<div
class="absolute top-0 left-0 h-full bg-gradient-to-b from-indigo-300 to-indigo-500"
style="width: {$transferDetails.filePercent}%; transition: {$transferDetails.filePercent === 0 ? 'none' : `width ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
></div>
<div class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500">
{$transferDetails.filePercent}%
</div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-white"
style="clip-path: inset(0 {100 - $transferDetails.filePercent}% 0 0); transition: {$transferDetails.filePercent === 0 ? 'none' : `clip-path ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
>
{$transferDetails.filePercent}%
</div>
</div>
<div class="flex justify-between text-[10px] px-1">
<span class="truncate max-w-[60%]">{$transferStats.currentFileName || "Lade..."}</span>
<span>{formatTime($transferDetails.fileEta)}</span>
</div>
</div>
<div class="w-full h-3 bg-slate-100 rounded-full overflow-hidden shadow-inner">
<div
class="h-full bg-slate-400 transition-all duration-500 ease-out"
style="width: {$transferDetails.totalPercent}%"
></div>
</div>
</div>
<div class="flex justify-between text-[10px] tracking-tighter font-medium text-slate-400">
<span>Rate: {$transferDetails.speedKbs.toFixed(0)}kBps</span>
<span>{$transferDetails.totalPercent}% abgeschlossen</span>
<div class="flex flex-col gap-1">
<div class="relative w-full h-3 bg-slate-100 rounded-sm overflow-hidden shadow-inner shadow-sm">
<div
class="absolute top-0 left-0 h-full bg-gradient-to-b from-indigo-300 to-indigo-500"
style="width: {$transferDetails.totalPercent}%; transition: {$transferDetails.totalPercent === 0 ? 'none' : `width ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
></div>
<div class="absolute inset-0 flex items-center justify-center text-[10px] text-slate-500">
{$transferDetails.totalPercent}%
</div>
<div
class="absolute inset-0 flex items-center justify-center text-[10px] text-white"
style="clip-path: inset(0 {100 - $transferDetails.totalPercent}% 0 0); transition: {$transferDetails.totalPercent === 0 ? 'none' : `clip-path ${SETTINGS.ui.transferUpdateIntervalMs}ms linear`};"
>
{$transferDetails.totalPercent}%
</div>
</div>
<div class="flex justify-between text-[10px] px-1">
<span>{$transferDetails.speedKbs} kB/s</span>
<span>{formatTime($transferDetails.totalEta)}</span>
</div>
</div>
<div class="flex justify-center mt-1">
<button
class="px-4 py-1.5 text-xs font-semibold text-red-600 bg-red-50 hover:bg-red-100 rounded-md transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
disabled={true}
>
Transfer abbrechen
</button>
</div>
</div>
</div>
{:else}
<div class="mt-4 text-xs text-slate-400 italic text-center tracking-widest" transition:slide={{ duration: 400 }}>
Kein Transfer aktiv
</div>
{/if}
</div>
{/if}