Lokale Dateien implementiert
This commit is contained in:
@@ -1,13 +1,21 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import FileListItem from "./FileListItem.svelte";
|
import FileListItem from "./FileListItem.svelte";
|
||||||
import { buzzerAudioFiles } from "../lib/store";
|
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
||||||
import type { BuzzerFile } from "../lib/types";
|
|
||||||
|
|
||||||
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>
|
</script>
|
||||||
|
|
||||||
<div>
|
<div class="flex flex-col w-full">
|
||||||
{#each $buzzerAudioFiles as file, index(index)}
|
{#each $activeStore as file (file.name)}
|
||||||
<FileListItem bind:file={file}/>
|
<FileListItem {file} {type} />
|
||||||
{/each}
|
{/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>
|
</div>
|
||||||
@@ -1,16 +1,53 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { BuzzerFile } from "../lib/types";
|
import type { BuzzerFile } from "../lib/types";
|
||||||
import { FileAudioIcon } from "phosphor-svelte";
|
import { FileAudioIcon } from "phosphor-svelte";
|
||||||
export let file: BuzzerFile;
|
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>
|
</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
|
<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
|
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 bg-blue-50 hover:bg-blue-100' : 'border-l-transparent hover:bg-slate-100 hover:border-l-blue-200'} "
|
{file.selected ? 'border-l-blue-600' : 'border-l-transparent'}
|
||||||
on:click={() => {file.selected = !file.selected;}}
|
{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" />
|
<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">
|
<span class="font-light">
|
||||||
{file.name || "Unbekannte Datei"}
|
{file.name || "Unbekannte Datei"}
|
||||||
{#if file.metaTags?.t}
|
{#if file.metaTags?.t}
|
||||||
@@ -23,7 +60,7 @@
|
|||||||
{parseFloat((file.size/1024).toFixed(1))} kB
|
{parseFloat((file.size/1024).toFixed(1))} kB
|
||||||
</span>
|
</span>
|
||||||
<span>
|
<span>
|
||||||
{#if file.metaTags.a}<span class="text-text-muted"> | Author:</span> {file.metaTags.a}{/if}
|
{#if file.metaTags?.a}<span class="text-text-muted"> | Author:</span> {file.metaTags.a}{/if}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -31,5 +68,4 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
@@ -25,7 +25,7 @@
|
|||||||
transferDetails,
|
transferDetails,
|
||||||
} from "../lib/store";
|
} from "../lib/store";
|
||||||
import { SETTINGS } from "../lib/settings";
|
import { SETTINGS } from "../lib/settings";
|
||||||
import { fade } from "svelte/transition";
|
import TransferProgress from "./TransferProgress.svelte";
|
||||||
|
|
||||||
let showOverlay = false;
|
let showOverlay = false;
|
||||||
let isTransferFinished = false;
|
let isTransferFinished = false;
|
||||||
@@ -101,102 +101,7 @@
|
|||||||
<div class="card-body transition-all duration-500 h-full" class:disconnected={!$isConnected}>
|
<div class="card-body transition-all duration-500 h-full" class:disconnected={!$isConnected}>
|
||||||
<DeviceInfo />
|
<DeviceInfo />
|
||||||
</div>
|
</div>
|
||||||
|
<TransferProgress />
|
||||||
{#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}
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
@@ -208,9 +113,7 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="py-10 text-center">
|
<FileList type="local" />
|
||||||
<span class="text-slate-300 uppercase italic tracking-widest">Bibliothek leer</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
|||||||
@@ -1,61 +1,106 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { transferDetails, transferStats } from "../lib/store";
|
import { fade } from 'svelte/transition';
|
||||||
import { fade, slide } 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
|
let showOverlay = false;
|
||||||
$: formatTime = (seconds: number): string => {
|
let isTransferFinished = false;
|
||||||
if (seconds === Infinity || seconds > 3600) return '∞';
|
let overlayTimeout: ReturnType<typeof setTimeout>;
|
||||||
|
|
||||||
const narrowNbsp = '\u202F';
|
|
||||||
|
|
||||||
if (seconds >= 60) {
|
$: if ($isFetchingRemote && $transferStats.overallTotal > 0) {
|
||||||
const m = Math.floor(seconds / 60);
|
showOverlay = true;
|
||||||
const s = Math.floor(seconds % 60);
|
isTransferFinished = false;
|
||||||
return `${m}:${s.toString().padStart(2, '0')}${narrowNbsp}m`;
|
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>
|
</script>
|
||||||
|
|
||||||
<div class="mt-4 min-h-[100px]">
|
{#if showOverlay}
|
||||||
{#if $transferStats.bytesTotal > 0}
|
<div
|
||||||
<div class="flex flex-col gap-3 mt-4 w-full" transition:slide={{ duration: 400 }}>
|
class="absolute inset-0 z-10 bg-white/95 backdrop-blur-[2px] p-4 flex flex-col justify-end"
|
||||||
|
transition:fade={{ duration: 300 }}
|
||||||
<div class="flex flex-col gap-1">
|
>
|
||||||
<div class="flex justify-between text-[10px] tracking-tighter font-bold text-slate-500">
|
{#if isTransferFinished}
|
||||||
<span class="truncate">Datei: {$transferStats.currentFileName || 'Übertragung...'}</span>
|
<button
|
||||||
<span>{formatTime($transferDetails.fileEta)}</span>
|
class="absolute top-2 right-2 p-1 text-slate-400 hover:text-slate-700 transition-colors"
|
||||||
</div>
|
on:click={closeOverlay}
|
||||||
<div class="w-full h-2 bg-slate-100 rounded-full overflow-hidden shadow-inner">
|
aria-label="Overlay schließen"
|
||||||
<div
|
>
|
||||||
class="h-full bg-blue-500 transition-all duration-500 ease-out"
|
<XIcon class="w-5 h-5" />
|
||||||
style="width: {$transferDetails.filePercent}%"
|
</button>
|
||||||
></div>
|
{/if}
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="flex flex-col gap-1">
|
<div class="w-full flex flex-col gap-1">
|
||||||
<div class="flex justify-between text-[10px] tracking-tighter font-bold text-slate-500">
|
|
||||||
<span>Gesamtfortschritt</span>
|
<div class="flex flex-col gap-1">
|
||||||
<span>{formatTime($transferDetails.totalEta)}</span>
|
<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>
|
||||||
<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">
|
<div class="flex flex-col gap-1">
|
||||||
<span>Rate: {$transferDetails.speedKbs.toFixed(0)}kBps</span>
|
<div class="relative w-full h-3 bg-slate-100 rounded-sm overflow-hidden shadow-inner shadow-sm">
|
||||||
<span>{$transferDetails.totalPercent}% abgeschlossen</span>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
{:else}
|
{/if}
|
||||||
<div class="mt-4 text-xs text-slate-400 italic text-center tracking-widest" transition:slide={{ duration: 400 }}>
|
|
||||||
Kein Transfer aktiv
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
</div>
|
|
||||||
47
webpage/src/lib/db.ts
Normal file
47
webpage/src/lib/db.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// src/lib/db.ts
|
||||||
|
|
||||||
|
const DB_NAME = 'BuzzerDB';
|
||||||
|
const STORE_NAME = 'localAudio';
|
||||||
|
|
||||||
|
function initDB(): Promise<IDBDatabase> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request = indexedDB.open(DB_NAME, 1);
|
||||||
|
|
||||||
|
request.onupgradeneeded = () => {
|
||||||
|
const db = request.result;
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
// Der Dateiname fungiert als eindeutiger Schlüssel
|
||||||
|
db.createObjectStore(STORE_NAME, { keyPath: 'name' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveLocalFile(name: string, blob: Blob, size: number): Promise<void> {
|
||||||
|
const db = await initDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE_NAME, 'readwrite');
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
|
||||||
|
// Speichert das Blob zusammen mit Metadaten
|
||||||
|
store.put({ name, blob, size, timestamp: Date.now() });
|
||||||
|
|
||||||
|
tx.oncomplete = () => resolve();
|
||||||
|
tx.onerror = () => reject(tx.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getLocalFiles(): Promise<any[]> {
|
||||||
|
const db = await initDB();
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const tx = db.transaction(STORE_NAME, 'readonly');
|
||||||
|
const store = tx.objectStore(STORE_NAME);
|
||||||
|
const request = store.getAll();
|
||||||
|
|
||||||
|
request.onsuccess = () => resolve(request.result);
|
||||||
|
request.onerror = () => reject(request.error);
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
// src/lib/init.ts
|
// src/lib/init.ts
|
||||||
import { isBluetoothSupported, isSerialSupported, isInitializing } from './store';
|
import { isBluetoothSupported, isSerialSupported, isInitializing } from './store';
|
||||||
|
import { refreshLocal } from './sync';
|
||||||
|
|
||||||
export function getBrowserName(): string {
|
export function getBrowserName(): string {
|
||||||
const ua = navigator.userAgent;
|
const ua = navigator.userAgent;
|
||||||
@@ -20,5 +21,8 @@ export function performHardwareCheck() {
|
|||||||
|
|
||||||
isBluetoothSupported.set(hasBT);
|
isBluetoothSupported.set(hasBT);
|
||||||
isSerialSupported.set(hasSerial);
|
isSerialSupported.set(hasSerial);
|
||||||
isInitializing.set(false);
|
|
||||||
|
refreshLocal().then(() => {
|
||||||
|
isInitializing.set(false);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,9 @@ import { protocolInfo, fsInfo, transferStats, resetTransferStats, transferDetail
|
|||||||
import { addToast } from '../toast';
|
import { addToast } from '../toast';
|
||||||
import { SETTINGS } from '../settings';
|
import { SETTINGS } from '../settings';
|
||||||
import { crc32 } from './crc32';
|
import { crc32 } from './crc32';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import { saveLocalFile } from '../db';
|
||||||
|
import { refreshLocal } from '../sync';
|
||||||
|
|
||||||
let lastUiUpdate = 0;
|
let lastUiUpdate = 0;
|
||||||
let currentFileCrc32 = 0;
|
let currentFileCrc32 = 0;
|
||||||
@@ -10,6 +13,7 @@ let currentFileCrc32 = 0;
|
|||||||
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
export type FrameSender = (buffer: ArrayBuffer) => Promise<void>;
|
||||||
|
|
||||||
let lsBuffer: any[] = [];
|
let lsBuffer: any[] = [];
|
||||||
|
let fileChunks: Uint8Array[] = [];
|
||||||
let lsTimeout: ReturnType<typeof setTimeout> | null = null;
|
let lsTimeout: ReturnType<typeof setTimeout> | null = null;
|
||||||
let lsResolve: ((data: any[]) => void) | null = null;
|
let lsResolve: ((data: any[]) => void) | null = null;
|
||||||
let lsReject: ((error: Error) => void) | null = null;
|
let lsReject: ((error: Error) => void) | null = null;
|
||||||
@@ -98,6 +102,7 @@ case FRAME.FILE_START:
|
|||||||
currentFileCrc32 = 0;
|
currentFileCrc32 = 0;
|
||||||
const totalBytes = view.getUint32(3, true);
|
const totalBytes = view.getUint32(3, true);
|
||||||
const nowStart = performance.now();
|
const nowStart = performance.now();
|
||||||
|
fileChunks = [];
|
||||||
|
|
||||||
transferStats.update(s => ({
|
transferStats.update(s => ({
|
||||||
...s,
|
...s,
|
||||||
@@ -155,6 +160,7 @@ case FRAME.FILE_START:
|
|||||||
|
|
||||||
const chunkData = new Uint8Array(view.buffer, 3, payloadLength);
|
const chunkData = new Uint8Array(view.buffer, 3, payloadLength);
|
||||||
currentFileCrc32 = crc32(chunkData, currentFileCrc32);
|
currentFileCrc32 = crc32(chunkData, currentFileCrc32);
|
||||||
|
fileChunks.push(new Uint8Array(chunkData));
|
||||||
|
|
||||||
const previousReceived = fileTransfer.receivedBytes;
|
const previousReceived = fileTransfer.receivedBytes;
|
||||||
fileTransfer.receivedBytes += payloadLength;
|
fileTransfer.receivedBytes += payloadLength;
|
||||||
@@ -202,6 +208,17 @@ case FRAME.FILE_START:
|
|||||||
|
|
||||||
if (currentFileCrc32 === buzzerCrc32) {
|
if (currentFileCrc32 === buzzerCrc32) {
|
||||||
console.log("%c[CRC] Match! Datei ist integer.", "color: green; font-weight: bold;");
|
console.log("%c[CRC] Match! Datei ist integer.", "color: green; font-weight: bold;");
|
||||||
|
const fileBlob = new Blob(fileChunks, { type: 'audio/wav' });
|
||||||
|
const fileName = get(transferStats).currentFileName;
|
||||||
|
saveLocalFile(fileName, fileBlob, fileTransfer.totalBytes)
|
||||||
|
.then(() => {
|
||||||
|
console.log(`Datei ${fileName} erfolgreich lokal gespeichert.`);
|
||||||
|
refreshLocal();
|
||||||
|
})
|
||||||
|
.catch(err => {
|
||||||
|
console.error("Datenbankfehler:", err);
|
||||||
|
addToast(`Speichern von ${fileName} fehlgeschlagen.`, 'error');
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
console.error("[CRC] Mismatch! Datei beschädigt.");
|
console.error("[CRC] Mismatch! Datei beschädigt.");
|
||||||
addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error");
|
addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error");
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ export const SETTINGS = {
|
|||||||
},
|
},
|
||||||
ui: {
|
ui: {
|
||||||
toastDurationMs: 5000,
|
toastDurationMs: 5000,
|
||||||
transferUpdateIntervalMs: 100,
|
transferUpdateIntervalMs: 300,
|
||||||
kbpsCalculationWindowMs: 10000,
|
kbpsCalculationWindowMs: 5500,
|
||||||
transferOverlayPersistMs: 4000,
|
transferOverlayPersistMs: 4000,
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isFetchingRemote, isFetchingLocal, storageUsage, transferDetails, transferStats } from './store';
|
import { isConnected, fsInfo, buzzerAudioFiles, buzzerSysFiles, isFetchingRemote, isFetchingLocal, storageUsage, localAudioFiles, transferStats } from './store';
|
||||||
import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport';
|
import { requestProtocolInfo, requestFSInfo, fetchDirectory } from './transport';
|
||||||
import type { BuzzerFile } from './types';
|
import type { BuzzerFile } from './types';
|
||||||
import { getFile } from './transport';
|
import { getFile } from './transport';
|
||||||
import { addToast } from './toast';
|
import { addToast } from './toast';
|
||||||
|
import { getLocalFiles } from './db';
|
||||||
|
|
||||||
function mapToBuzzerFile(rawFile: any): BuzzerFile {
|
function mapToBuzzerFile(rawFile: any): BuzzerFile {
|
||||||
return {
|
return {
|
||||||
@@ -51,16 +52,26 @@ export async function refreshRemote() {
|
|||||||
export async function refreshLocal() {
|
export async function refreshLocal() {
|
||||||
isFetchingLocal.set(true);
|
isFetchingLocal.set(true);
|
||||||
try {
|
try {
|
||||||
// TODO: Implementierung lokaler Dateisystem-Zugriff (z.B. File System Access API)
|
const dbFiles = await getLocalFiles();
|
||||||
// const files = await readLocalDirectory();
|
|
||||||
// localAudioFiles.set(files.map(mapToBuzzerFile));
|
// Mappen auf die BuzzerFile-Struktur
|
||||||
|
const files: BuzzerFile[] = dbFiles.map(record => ({
|
||||||
|
name: record.name,
|
||||||
|
size: record.size,
|
||||||
|
type: 0, // 0 = File
|
||||||
|
tagsLoaded: false,
|
||||||
|
sysTags: { format: null, crc32: null },
|
||||||
|
metaTags: {},
|
||||||
|
selected: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
localAudioFiles.set(files);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Fehler beim Aktualisieren der lokalen Daten:", error);
|
console.error("Fehler beim Laden der lokalen Datenbank:", error);
|
||||||
} finally {
|
} finally {
|
||||||
isFetchingLocal.set(false);
|
isFetchingLocal.set(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function downloadSelectedFiles() {
|
export async function downloadSelectedFiles() {
|
||||||
const files = get(buzzerAudioFiles).filter(f => f.selected);
|
const files = get(buzzerAudioFiles).filter(f => f.selected);
|
||||||
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";
|
||||||
|
|||||||
Reference in New Issue
Block a user