diff --git a/webpage/src/components/FileList.svelte b/webpage/src/components/FileList.svelte index a055969..eb1d5f1 100644 --- a/webpage/src/components/FileList.svelte +++ b/webpage/src/components/FileList.svelte @@ -1,13 +1,21 @@ -
- {#each $buzzerAudioFiles as file, index(index)} - - {/each} +
+ {#each $activeStore as file (file.name)} + + {/each} + + {#if $activeStore.length === 0} +
+ Keine Dateien vorhanden. +
+ {/if}
\ No newline at end of file diff --git a/webpage/src/components/FileListItem.svelte b/webpage/src/components/FileListItem.svelte index 615e985..b4554ca 100644 --- a/webpage/src/components/FileListItem.svelte +++ b/webpage/src/components/FileListItem.svelte @@ -1,16 +1,53 @@ -
+ +
+ + {#if state === 'active'} +
+ {/if} +
\ No newline at end of file diff --git a/webpage/src/components/MainGrid.svelte b/webpage/src/components/MainGrid.svelte index 067eb6d..d067f9a 100644 --- a/webpage/src/components/MainGrid.svelte +++ b/webpage/src/components/MainGrid.svelte @@ -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 @@
- - {#if showOverlay} -
- {#if isTransferFinished} - - {/if} - -
-
-
-
-
- {$transferDetails.filePercent}% -
-
- {$transferDetails.filePercent}% -
-
-
- - {$transferStats.currentFileName || "Lade..."} - - {formatTime($transferDetails.fileEta)} -
-
- -
-
-
-
- {$transferDetails.totalPercent}% -
-
- {$transferDetails.totalPercent}% -
-
-
- {$transferDetails.speedKbs} kB/s - {formatTime($transferDetails.totalEta)} -
-
- -
- -
-
-
- {/if} +
@@ -208,9 +113,7 @@
-
- Bibliothek leer -
+
diff --git a/webpage/src/components/TransferProgress.svelte b/webpage/src/components/TransferProgress.svelte index c838d2e..6b3993a 100644 --- a/webpage/src/components/TransferProgress.svelte +++ b/webpage/src/components/TransferProgress.svelte @@ -1,61 +1,106 @@ -
-{#if $transferStats.bytesTotal > 0} -
- -
-
- Datei: {$transferStats.currentFileName || 'Übertragung...'} - {formatTime($transferDetails.fileEta)} -
-
-
-
-
+{#if showOverlay} +
+ {#if isTransferFinished} + + {/if} -
-
- Gesamtfortschritt - {formatTime($transferDetails.totalEta)} +
+ +
+
+
+
+ {$transferDetails.filePercent}% +
+
+ {$transferDetails.filePercent}% +
+
+
+ {$transferStats.currentFileName || "Lade..."} + {formatTime($transferDetails.fileEta)} +
-
-
-
-
-
- Rate: {$transferDetails.speedKbs.toFixed(0)}kBps - {$transferDetails.totalPercent}% abgeschlossen +
+
+
+
+ {$transferDetails.totalPercent}% +
+
+ {$transferDetails.totalPercent}% +
+
+
+ {$transferDetails.speedKbs} kB/s + {formatTime($transferDetails.totalEta)} +
+
+ +
+ +
+
-{:else} -
- Kein Transfer aktiv -
-{/if} -
\ No newline at end of file +{/if} \ No newline at end of file diff --git a/webpage/src/lib/db.ts b/webpage/src/lib/db.ts new file mode 100644 index 0000000..c01577f --- /dev/null +++ b/webpage/src/lib/db.ts @@ -0,0 +1,47 @@ +// src/lib/db.ts + +const DB_NAME = 'BuzzerDB'; +const STORE_NAME = 'localAudio'; + +function initDB(): Promise { + 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 { + 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 { + 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); + }); +} \ No newline at end of file diff --git a/webpage/src/lib/init.ts b/webpage/src/lib/init.ts index ca3e0c5..400d44f 100644 --- a/webpage/src/lib/init.ts +++ b/webpage/src/lib/init.ts @@ -1,5 +1,6 @@ // src/lib/init.ts import { isBluetoothSupported, isSerialSupported, isInitializing } from './store'; +import { refreshLocal } from './sync'; export function getBrowserName(): string { const ua = navigator.userAgent; @@ -20,5 +21,8 @@ export function performHardwareCheck() { isBluetoothSupported.set(hasBT); isSerialSupported.set(hasSerial); - isInitializing.set(false); + + refreshLocal().then(() => { + isInitializing.set(false); + }); } \ No newline at end of file diff --git a/webpage/src/lib/protocol/parser.ts b/webpage/src/lib/protocol/parser.ts index 75fa6d5..bb34243 100644 --- a/webpage/src/lib/protocol/parser.ts +++ b/webpage/src/lib/protocol/parser.ts @@ -3,6 +3,9 @@ import { protocolInfo, fsInfo, transferStats, resetTransferStats, transferDetail import { addToast } from '../toast'; import { SETTINGS } from '../settings'; import { crc32 } from './crc32'; +import { get } from 'svelte/store'; +import { saveLocalFile } from '../db'; +import { refreshLocal } from '../sync'; let lastUiUpdate = 0; let currentFileCrc32 = 0; @@ -10,6 +13,7 @@ let currentFileCrc32 = 0; export type FrameSender = (buffer: ArrayBuffer) => Promise; let lsBuffer: any[] = []; +let fileChunks: Uint8Array[] = []; let lsTimeout: ReturnType | null = null; let lsResolve: ((data: any[]) => void) | null = null; let lsReject: ((error: Error) => void) | null = null; @@ -98,6 +102,7 @@ case FRAME.FILE_START: currentFileCrc32 = 0; const totalBytes = view.getUint32(3, true); const nowStart = performance.now(); + fileChunks = []; transferStats.update(s => ({ ...s, @@ -155,6 +160,7 @@ case FRAME.FILE_START: const chunkData = new Uint8Array(view.buffer, 3, payloadLength); currentFileCrc32 = crc32(chunkData, currentFileCrc32); + fileChunks.push(new Uint8Array(chunkData)); const previousReceived = fileTransfer.receivedBytes; fileTransfer.receivedBytes += payloadLength; @@ -202,6 +208,17 @@ case FRAME.FILE_START: if (currentFileCrc32 === buzzerCrc32) { 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 { console.error("[CRC] Mismatch! Datei beschädigt."); addToast("CRC Fehler: Datei wurde fehlerhaft übertragen.", "error"); diff --git a/webpage/src/lib/settings.ts b/webpage/src/lib/settings.ts index 16f95b0..d0541f8 100644 --- a/webpage/src/lib/settings.ts +++ b/webpage/src/lib/settings.ts @@ -7,8 +7,8 @@ export const SETTINGS = { }, ui: { toastDurationMs: 5000, - transferUpdateIntervalMs: 100, - kbpsCalculationWindowMs: 10000, + transferUpdateIntervalMs: 300, + kbpsCalculationWindowMs: 5500, transferOverlayPersistMs: 4000, } }; \ No newline at end of file diff --git a/webpage/src/lib/sync.ts b/webpage/src/lib/sync.ts index 5600835..38c74c9 100644 --- a/webpage/src/lib/sync.ts +++ b/webpage/src/lib/sync.ts @@ -1,9 +1,10 @@ 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 type { BuzzerFile } from './types'; import { getFile } from './transport'; import { addToast } from './toast'; +import { getLocalFiles } from './db'; function mapToBuzzerFile(rawFile: any): BuzzerFile { return { @@ -51,16 +52,26 @@ export async function refreshRemote() { export async function refreshLocal() { isFetchingLocal.set(true); try { - // TODO: Implementierung lokaler Dateisystem-Zugriff (z.B. File System Access API) - // const files = await readLocalDirectory(); - // localAudioFiles.set(files.map(mapToBuzzerFile)); + const dbFiles = await getLocalFiles(); + + // 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) { - console.error("Fehler beim Aktualisieren der lokalen Daten:", error); + console.error("Fehler beim Laden der lokalen Datenbank:", error); } finally { isFetchingLocal.set(false); } } - export async function downloadSelectedFiles() { const files = get(buzzerAudioFiles).filter(f => f.selected); const pathPrefix = get(fsInfo)?.audioPath || "/lfs/a";