zwischenstand
This commit is contained in:
456
webpage/src/components/AudioDropzone.svelte
Normal file
456
webpage/src/components/AudioDropzone.svelte
Normal file
@@ -0,0 +1,456 @@
|
||||
<script lang="ts">
|
||||
import { GearIcon, CloudArrowUpIcon, InfoIcon, CaretDownIcon } from "phosphor-svelte";
|
||||
import { slide, fade } from "svelte/transition";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { processAudioFile } from "../lib/audioProcessor";
|
||||
import { saveLocalFile } from "../lib/db";
|
||||
import { refreshLocal } from "../lib/sync";
|
||||
import { updateLocalAudioFormat, updateLocalAudioCrc } from "../lib/tagHandler";
|
||||
import { audioOptions } from "../lib/store";
|
||||
import { AUDIO_PRESETS, type AudioPresetName } from "../lib/settings";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { clickOutside } from "../lib/actions/clickOutside"; // Angenommen, diese Action existiert bereits (analog zu Ihren anderen Menüs).
|
||||
|
||||
let isFileDragOver = false;
|
||||
let showSettings = false;
|
||||
let showPresetDropdown = false; // Neuer Zustand für das Custom Dropdown
|
||||
|
||||
// Definitionen der Preset-Infos für Tooltips im Dropdown
|
||||
const presetInfos: Record<AudioPresetName, { name: string; desc: string }> = {
|
||||
normal: { name: "Normal", desc: "Für natürliche Sprache. Moderate Verdichtung." },
|
||||
broadcast: { name: "Broadcast", desc: "Aggressiv, sehr laut (Radio-Style). Maximale Präsenz." },
|
||||
custom: { name: "Benutzerdefiniert", desc: "Manuelle Anpassung aller Kompressor-Parameter." },
|
||||
};
|
||||
|
||||
// Reagiert auf Preset-Wechsel
|
||||
function handlePresetChange(preset: AudioPresetName) {
|
||||
$audioOptions.preset = preset;
|
||||
showPresetDropdown = false; // Dropdown nach Auswahl schließen
|
||||
|
||||
if (preset !== "custom") {
|
||||
const p = AUDIO_PRESETS[preset];
|
||||
$audioOptions = {
|
||||
...$audioOptions,
|
||||
compressorThreshold: p.compressorThreshold!,
|
||||
compressorRatio: p.compressorRatio!,
|
||||
compressorKnee: p.compressorKnee!,
|
||||
compressorAttack: p.compressorAttack!,
|
||||
compressorRelease: p.compressorRelease!,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Setzt das Preset automatisch auf Custom, sobald ein Slider bewegt wird
|
||||
function setCustomPreset() {
|
||||
$audioOptions.preset = "custom";
|
||||
}
|
||||
|
||||
async function handleDrop(event: DragEvent) {
|
||||
isFileDragOver = false;
|
||||
showSettings = false;
|
||||
showPresetDropdown = false; // Dropdown sicherheitshalber schließen
|
||||
|
||||
const files = event.dataTransfer?.files;
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
const fileArray = Array.from(files);
|
||||
|
||||
const audioFiles = fileArray.filter((file) => file.type.startsWith("audio/"));
|
||||
const rawFiles = fileArray.filter((file) => file.type === ""); // Ohne MIME-Type
|
||||
|
||||
if (audioFiles.length > 0 || rawFiles.length > 0) {
|
||||
// 1. Reguläre Dateien umwandeln & taggen
|
||||
for (const file of audioFiles) {
|
||||
try {
|
||||
const { buffer, name } = await processAudioFile(file, $audioOptions);
|
||||
await saveLocalFile(name, new Blob([buffer]), buffer.byteLength);
|
||||
|
||||
await updateLocalAudioFormat(name, { codec: 0, bitDepth: 16, sampleRate: 16000 });
|
||||
await updateLocalAudioCrc(name);
|
||||
|
||||
addToast(`Gespeichert & Getaggt: ${name}`, "success");
|
||||
} catch (err) {
|
||||
addToast(`Fehler bei ${file.name}: ${err}`, "error");
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Rohdaten bypassen (direkt speichern & taggen)
|
||||
for (const file of rawFiles) {
|
||||
try {
|
||||
const name = file.name;
|
||||
await saveLocalFile(name, file, file.size);
|
||||
|
||||
// TagHandler erkennt alte Tags automatisch und fügt System-Tags sicher hinzu
|
||||
await updateLocalAudioFormat(name, { codec: 0, bitDepth: 16, sampleRate: 16000 });
|
||||
await updateLocalAudioCrc(name);
|
||||
|
||||
addToast(`Roh-Datei importiert: ${name}`, "success");
|
||||
} catch (err) {
|
||||
addToast(`Fehler bei Roh-Import ${file.name}: ${err}`, "error");
|
||||
}
|
||||
}
|
||||
refreshLocal();
|
||||
} else {
|
||||
addToast("Keine gültigen Dateien gefunden.", "error");
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
on:keydown={(e) => {
|
||||
if (e.key === "Escape") showSettings = false;
|
||||
}}
|
||||
/>
|
||||
|
||||
<section class="buzzer-card flex flex-col h-full transition-all duration-300 min-w-[320px]">
|
||||
<div class="card-header" class:blur={isFileDragOver}>
|
||||
<h3 class="card-title">Dateiverarbeitung</h3>
|
||||
<button class="btn" aria-label="Einstellungen" on:click={() => (showSettings = !showSettings)}>
|
||||
<GearIcon class="icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="card-body flex flex-col flex-1"
|
||||
role="region"
|
||||
aria-label="Audiofile dropzone"
|
||||
on:dragenter={() => (isFileDragOver = true)}
|
||||
on:dragleave={() => (isFileDragOver = false)}
|
||||
on:dragover|preventDefault
|
||||
on:drop|preventDefault={handleDrop}
|
||||
>
|
||||
{#if !showSettings}
|
||||
<div
|
||||
class="flex justify-center items-center pointer-events-none p-4 pb-0"
|
||||
in:fade={{ duration: 200 }}
|
||||
>
|
||||
<div
|
||||
class="text-center text-2xs tracking-tight font-mono font-semibold transition-all"
|
||||
class:blur={isFileDragOver}
|
||||
>
|
||||
<span class={$audioOptions.lowCut ? "text-emerald-500" : "text-text-muted"}>LOW CUT</span>
|
||||
|
|
||||
<span class={$audioOptions.compress ? "text-emerald-500" : "text-text-muted"}>
|
||||
COMPRESSOR
|
||||
</span>
|
||||
|
|
||||
<span class={$audioOptions.normalize ? "text-emerald-500" : "text-text-muted"}>
|
||||
NORMALIZER
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 p-4 pt-2">
|
||||
<div
|
||||
in:fade={{ duration: 200 }}
|
||||
class="flex h-full items-center justify-center border-2 border-dashed rounded-lg pointer-events-none transition-all duration-300 min-h-[8rem]"
|
||||
class:border-slate-300={!isFileDragOver}
|
||||
class:border-indigo-500={isFileDragOver}
|
||||
class:bg-indigo-50={isFileDragOver}
|
||||
>
|
||||
<div class="relative flex size-24 transition-transform" class:scale-110={isFileDragOver}>
|
||||
<span
|
||||
class="absolute inline-flex h-full w-full opacity-50"
|
||||
class:animate-ping={isFileDragOver}
|
||||
class:text-indigo-500={isFileDragOver}
|
||||
class:text-transparent={!isFileDragOver}
|
||||
>
|
||||
<CloudArrowUpIcon class="w-full h-full" />
|
||||
</span>
|
||||
<span
|
||||
class="relative inline-flex size-24 transition-colors"
|
||||
class:text-slate-500={!isFileDragOver}
|
||||
class:text-indigo-600={isFileDragOver}
|
||||
>
|
||||
<CloudArrowUpIcon class="w-full h-full transition-colors" />
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else}
|
||||
<div class="flex-1 flex flex-col bg-white h-full" in:slide={{ duration: 300 }}>
|
||||
<div
|
||||
class="flex items-center justify-between px-4 pt-4 pb-2 border-b border-slate-100 mb-4"
|
||||
>
|
||||
<h4 class="font-semibold text-sm text-slate-800">Audio-Optionen</h4>
|
||||
</div>
|
||||
|
||||
<div class="space-y-5 flex-1 overflow-y-auto px-4 pb-4">
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: "Entfernt tieffrequentes Rumpeln (Wind, Trittschall). Schont kleine Lautsprecher und verhindert Verzerrungen.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
<InfoIcon class="text-text-muted size-4" />
|
||||
</div>
|
||||
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$audioOptions.lowCut}
|
||||
class="accent-indigo-600"
|
||||
/>
|
||||
<span>Low-Cut Filter</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if $audioOptions.lowCut}
|
||||
<div class="pl-6 flex items-center gap-3">
|
||||
<span class="text-xs text-text-muted w-10 text-right">
|
||||
{$audioOptions.lowCutFreq} Hz
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="50"
|
||||
max="300"
|
||||
step="10"
|
||||
bind:value={$audioOptions.lowCutFreq}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: "Gleicht Lautstärkeunterschiede an. Macht leise Passagen lauter und fängt laute Spitzen ab.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
<InfoIcon class="text-text-muted size-4" />
|
||||
</div>
|
||||
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$audioOptions.compress}
|
||||
class="accent-indigo-600"
|
||||
/>
|
||||
<span>Dynamik-Kompressor</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
{#if $audioOptions.compress}
|
||||
<div class="pl-6 flex flex-col gap-3">
|
||||
<div class="relative" use:clickOutside={() => (showPresetDropdown = false)}>
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center justify-between w-full text-sm border border-slate-300 rounded p-1.5 bg-slate-50 hover:bg-slate-100 transition"
|
||||
on:click={() => (showPresetDropdown = !showPresetDropdown)}
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
use:tooltip={{ text: presetInfos[$audioOptions.preset].desc, pos: "right" }}
|
||||
>
|
||||
<InfoIcon class="text-text-muted size-4" />
|
||||
</div>
|
||||
<span>Preset: {presetInfos[$audioOptions.preset].name}</span>
|
||||
</div>
|
||||
<span class:rotate-180={showPresetDropdown}>
|
||||
<CaretDownIcon class="size-4 text-slate-500 transition-transform" />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{#if showPresetDropdown}
|
||||
<div
|
||||
class="absolute top-full left-0 right-0 mt-1 bg-white border border-slate-200 rounded-md shadow-lg z-20 overflow-hidden"
|
||||
transition:fade={{ duration: 150 }}
|
||||
>
|
||||
{#each Object.entries(presetInfos) as [key, info]}
|
||||
<button
|
||||
type="button"
|
||||
class="flex items-center gap-2 w-full text-left text-sm px-3 py-2 hover:bg-indigo-50 transition"
|
||||
class:bg-indigo-100={$audioOptions.preset === key}
|
||||
class:font-medium={$audioOptions.preset === key}
|
||||
on:click={() => handlePresetChange(key as AudioPresetName)}
|
||||
>
|
||||
<div use:tooltip={{ text: info.desc, pos: "right" }}>
|
||||
<InfoIcon class="text-text-muted size-4" />
|
||||
</div>
|
||||
<span>{info.name}</span>
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-1 gap-2 text-xs">
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
class:opacity-50={$audioOptions.preset !== "custom"}
|
||||
>
|
||||
<span
|
||||
class="w-20 cursor-help"
|
||||
use:tooltip={{
|
||||
text: "Pegel, ab dem der Kompressor zu arbeiten beginnt.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
Threshold
|
||||
</span>
|
||||
<span class="text-text-muted w-16 text-right">
|
||||
{$audioOptions.compressorThreshold} dB
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="-60"
|
||||
max="0"
|
||||
step="1"
|
||||
bind:value={$audioOptions.compressorThreshold}
|
||||
disabled={$audioOptions.preset !== "custom"}
|
||||
on:input={setCustomPreset}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
class:opacity-50={$audioOptions.preset !== "custom"}
|
||||
>
|
||||
<span
|
||||
class="w-20 cursor-help"
|
||||
use:tooltip={{
|
||||
text: "Stärke der Pegelreduzierung oberhalb des Thresholds.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
Ratio
|
||||
</span>
|
||||
<span class="text-text-muted w-16 text-right">
|
||||
{$audioOptions.compressorRatio}:1
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="1"
|
||||
max="20"
|
||||
step="1"
|
||||
bind:value={$audioOptions.compressorRatio}
|
||||
disabled={$audioOptions.preset !== "custom"}
|
||||
on:input={setCustomPreset}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
class:opacity-50={$audioOptions.preset !== "custom"}
|
||||
>
|
||||
<span
|
||||
class="w-20 cursor-help"
|
||||
use:tooltip={{
|
||||
text: "Reaktionszeit des Kompressors auf Pegelüberschreitungen.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
Attack
|
||||
</span>
|
||||
<span class="text-text-muted w-16 text-right">
|
||||
{($audioOptions.compressorAttack * 1000).toFixed(0)} ms
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0.001"
|
||||
max="0.1"
|
||||
step="0.001"
|
||||
bind:value={$audioOptions.compressorAttack}
|
||||
disabled={$audioOptions.preset !== "custom"}
|
||||
on:input={setCustomPreset}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="flex items-center gap-2"
|
||||
class:opacity-50={$audioOptions.preset !== "custom"}
|
||||
>
|
||||
<span
|
||||
class="w-20 cursor-help"
|
||||
use:tooltip={{
|
||||
text: "Zeit, bis der Kompressor nach Unterschreitung des Thresholds aufhört zu arbeiten.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
Release
|
||||
</span>
|
||||
<span class="text-text-muted w-16 text-right">
|
||||
{($audioOptions.compressorRelease * 1000).toFixed(0)} ms
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="0.01"
|
||||
max="1"
|
||||
step="0.01"
|
||||
bind:value={$audioOptions.compressorRelease}
|
||||
disabled={$audioOptions.preset !== "custom"}
|
||||
on:input={setCustomPreset}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="flex flex-col gap-2">
|
||||
<div class="flex items-center gap-2">
|
||||
<div
|
||||
use:tooltip={{
|
||||
text: "Hebt das verarbeitete Signal linear an, bis der lauteste Peak exakt den Zielwert erreicht.",
|
||||
pos: "right",
|
||||
}}
|
||||
>
|
||||
<InfoIcon class="text-text-muted size-4" />
|
||||
</div>
|
||||
<label class="flex items-center space-x-2 text-sm font-medium cursor-pointer flex-1">
|
||||
<input
|
||||
type="checkbox"
|
||||
bind:checked={$audioOptions.normalize}
|
||||
class="accent-indigo-600"
|
||||
/>
|
||||
<span>Lautstärke normalisieren</span>
|
||||
</label>
|
||||
</div>
|
||||
{#if $audioOptions.normalize}
|
||||
<div class="pl-6 flex items-center gap-3">
|
||||
<span class="text-xs text-text-muted w-10 text-right">
|
||||
{$audioOptions.normalizeTargetDb} dB
|
||||
</span>
|
||||
<input
|
||||
type="range"
|
||||
min="-10"
|
||||
max="0"
|
||||
step="0.1"
|
||||
bind:value={$audioOptions.normalizeTargetDb}
|
||||
class="flex-1 accent-indigo-500 h-1"
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
<div class="text-xs text-text-muted bg-slate-100 p-3 rounded mt-4">
|
||||
Hinweis: Die Konvertierung zu 16kHz Mono PCM erfolgt immer automatisch beim Speichern.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<style>
|
||||
@reference "../styles/app.css";
|
||||
|
||||
.btn {
|
||||
@apply rounded-full transition-colors border-0 p-2;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-slate-200 hover:shadow-sm;
|
||||
}
|
||||
}
|
||||
|
||||
/* Styling für die Range-Slider */
|
||||
input[type="range"] {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
input[type="range"]:disabled {
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
</style>
|
||||
@@ -1,54 +1,145 @@
|
||||
<script lang="ts">
|
||||
import FlashUsage from "./FlashUsage.svelte"
|
||||
import { BatteryEmptyIcon, BatteryLowIcon, BatteryMediumIcon, BatteryHighIcon, BatteryFullIcon, BatteryChargingIcon } from "phosphor-svelte"
|
||||
|
||||
import FlashUsage from "./FlashUsage.svelte";
|
||||
import { deviceInfo, fwInfo } from "../lib/store";
|
||||
import { FW_STATUS } from "../lib/protocol/constants";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import {
|
||||
CheckCircleIcon,
|
||||
WarningIcon,
|
||||
BatteryEmptyIcon,
|
||||
BatteryLowIcon,
|
||||
BatteryMediumIcon,
|
||||
BatteryHighIcon,
|
||||
BatteryFullIcon,
|
||||
BatteryChargingIcon,
|
||||
} from "phosphor-svelte";
|
||||
</script>
|
||||
|
||||
<div class="text-sm">
|
||||
<div class="text-sm">
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="key">
|
||||
Modell
|
||||
</td>
|
||||
<td class="value">
|
||||
nrf52840dk-prototyp
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">
|
||||
Version
|
||||
</td>
|
||||
<td class="value">
|
||||
2.3.22-debug
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">
|
||||
HW-ID
|
||||
</td>
|
||||
<td class="value">
|
||||
<span class="font-mono">DEAD-BEAF-0102-3456</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">
|
||||
Batterie
|
||||
</td>
|
||||
<td class="value flex items-center gap-2">
|
||||
85% <BatteryChargingIcon weight="bold" class="w-5 h-5"/> 1200mAh
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">
|
||||
Speicher
|
||||
</td>
|
||||
<td class="value">
|
||||
<div class="py-1">
|
||||
<FlashUsage/>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<table>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="key">Modell</td>
|
||||
<td class="value">
|
||||
{#if $deviceInfo}
|
||||
{$deviceInfo.boardName}
|
||||
{#if $deviceInfo.boardRevision}
|
||||
<span class="text-muted">>Rev. {$deviceInfo.boardRevision}</span>
|
||||
{/if}
|
||||
{:else}
|
||||
unbekannt
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">Version</td>
|
||||
<td class="value flex items-center gap-1">
|
||||
{#if $fwInfo}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: `Die Firmware-Slotgrösse beträgt <span class="font-medium">${$fwInfo.slot1Size / 1024} kB</span>. Wieso musst Du das wissen? Edi sorgt schon dafür, dass die Updates nicht zu gross sind.<br/> <div class="text-3xl align-center text-center pb-2">😈</div>`,
|
||||
pos: "bottom",
|
||||
}}
|
||||
>
|
||||
{$fwInfo.fwVersion}
|
||||
</span>
|
||||
{#if $fwInfo.fwStatus === FW_STATUS.CONFIRMED}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: "Firmware ist bestätigt und damit dauerhaft nutzbar",
|
||||
pos: "bottom",
|
||||
}}
|
||||
class="relative flex size-5"
|
||||
>
|
||||
<CheckCircleIcon
|
||||
weight="fill"
|
||||
class="text-emerald-500 relative inline-flex h-full w-full"
|
||||
/>
|
||||
</span>
|
||||
{:else if $fwInfo.fwStatus === FW_STATUS.PENDING}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: "Firmware ist nur hochgeladen und noch nicht bestätigt. Du kannst sie nach einem <b>reboot</b> testen. Wenn sie funktioniert, bestätige sie, damit sie dauerhaft nutzbar ist.<br/><b><i>Achtung:</i></b> der Reboot dauert einen Moment, da die Firmware erst umkopiert werden muss.",
|
||||
pos: "bottom",
|
||||
}}
|
||||
class="relative flex size-5"
|
||||
>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-amber-500 absolute inline-flex h-full w-full animate-ping"
|
||||
/>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-amber-500 relative inline-flex h-full w-full"
|
||||
/>
|
||||
</span>
|
||||
{:else if $fwInfo.fwStatus === FW_STATUS.TESTING}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: "Die Firmware ist im Testmodus. Wenn sie gut funktioniert, dann <b>bestätige</b> sie, damit sie dauerhaft nutzbar ist. Wenn sie nicht richtig funktioniert, dann kannst Du den Buzzer <b>rebooten</b>, er kehrt dann zur vorhergehenden Version zurück.<br/><b><i>Achtung:</i></b> der Reboot dauert einen Moment, da die Firmware erst umkopiert werden muss.",
|
||||
pos: "bottom",
|
||||
}}
|
||||
class="relative flex size-5"
|
||||
>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-amber-600 absolute inline-flex h-full w-full animate-ping"
|
||||
/>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-amber-600 relative inline-flex h-full w-full"
|
||||
/>
|
||||
</span>
|
||||
{:else if $fwInfo.fwStatus === FW_STATUS.UNKNOWN}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: `Die Firmware hat den unbekannten Status <span class="bold font-mono">0x${$fwInfo.fwStatus.toString(16).padStart(2, "0").toUpperCase()}.</span> Weiss der Teufel, was da wieder passiert ist... Vielleicht hilft ein Reboot? Oder Firmware neu flashen? Oder... hast Du ne Idee???`,
|
||||
pos: "bottom",
|
||||
}}
|
||||
class="relative flex size-5"
|
||||
>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-red-500 absolute inline-flex h-full w-full animate-ping"
|
||||
/>
|
||||
<WarningIcon
|
||||
weight="fill"
|
||||
class="text-red-500 relative inline-flex h-full w-full"
|
||||
/>
|
||||
</span>
|
||||
{/if}
|
||||
<span class="text-text-muted ml-1">(Zephyr {$fwInfo.kernelVersion})</span>
|
||||
{:else}
|
||||
unbekannt
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">HW-ID</td>
|
||||
<td class="value">
|
||||
{#if $deviceInfo}
|
||||
<span class="font-mono">{$deviceInfo.deviceId}</span>
|
||||
{:else}
|
||||
unbekannt
|
||||
{/if}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">Batterie</td>
|
||||
<td class="value flex items-center gap-2">
|
||||
85% <BatteryChargingIcon weight="bold" class="w-5 h-5" /> 1200mAh
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="key">Speicher</td>
|
||||
<td class="value">
|
||||
<div class="py-1">
|
||||
<FlashUsage />
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
import { tagEditorState } from "../lib/store";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { deleteRemoteFile } from "../lib/transport";
|
||||
import { deleteLocalFile } from "../lib/db";
|
||||
import { deleteLocalFile, playLocalFile } from "../lib/db";
|
||||
import { refreshRemote, refreshLocal } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
|
||||
@@ -56,6 +56,7 @@
|
||||
case SyncState.UNKNOWN:
|
||||
return {
|
||||
icon: QuestionIcon,
|
||||
weight: "fill",
|
||||
color: "text-amber-500",
|
||||
variant: "warning",
|
||||
text: "Prüfsumme fehlt. Bitte Metadaten aktualisieren.",
|
||||
@@ -63,20 +64,24 @@
|
||||
case SyncState.SINGLE_SIDED:
|
||||
return {
|
||||
icon: CircleIcon,
|
||||
color: "text-green-600",
|
||||
weight: "bold",
|
||||
color: "text-emerald-600",
|
||||
variant: "info",
|
||||
text: `Datei existiert nur ${type === "buzzer" ? "auf dem Buzzer" : "lokal"}.`,
|
||||
};
|
||||
case SyncState.SYNCED:
|
||||
return {
|
||||
icon: CheckCircleIcon,
|
||||
color: "text-green-600",
|
||||
weight: "fill",
|
||||
color: "text-emerald-500",
|
||||
variant: "info",
|
||||
text: "Datei ist synchronisiert.",
|
||||
};
|
||||
case SyncState.CONFLICT:
|
||||
return {
|
||||
icon: WarningIcon,
|
||||
ping: true,
|
||||
weight: "fill",
|
||||
color: "text-amber-600",
|
||||
variant: "warning",
|
||||
text: `Konflikt: Name/Tags weichen ab. Vergleiche mit <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles[0]}</span> ${type === "buzzer" ? "lokal" : "auf dem Buzzer"}`,
|
||||
@@ -88,6 +93,8 @@
|
||||
: `Mehrfach vorhanden: Gleicher Inhalt auch in <span class="text-medium text-on-surface bg-white rounded px-1">${syncStatus.linkedFiles.join("</span> <span class='text-medium text-on-surface bg-white rounded px-1'>")}</span>`;
|
||||
return {
|
||||
icon: WarningCircleIcon,
|
||||
ping: true,
|
||||
weight: "fill",
|
||||
color: "text-red-600",
|
||||
variant: "danger",
|
||||
text: duplicateText,
|
||||
@@ -158,6 +165,7 @@
|
||||
></div>
|
||||
{/if}
|
||||
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<button
|
||||
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'}
|
||||
@@ -169,11 +177,15 @@
|
||||
{$isTransferingRemote ? 'cursor-default' : ''}
|
||||
{state === 'pending' ? 'grayscale opacity-80' : ''}"
|
||||
on:click={toggleSelection}
|
||||
on:mouseenter|stopPropagation={() => (menuOpen = true)}
|
||||
on:mouseleave|stopPropagation={() => (menuOpen = false)}
|
||||
on:blur|stopPropagation={() => (menuOpen = false)}
|
||||
on:focus|stopPropagation={() => (menuOpen = true)}
|
||||
disabled={$isTransferingRemote}
|
||||
>
|
||||
<MusicNotesIcon weight="fill" class="mr-3 w-5 h-5 shrink-0" />
|
||||
<MusicNotesIcon weight="fill" class="mr-2 w-5 h-5 shrink-0" />
|
||||
|
||||
<div class="flex flex-col flex-1 min-w-0 overflow-hidden">
|
||||
<div class="flex flex-col flex-1 min-w-0 overflow-hidden pl-1">
|
||||
<div class="flex items-center min-w-0">
|
||||
<span class="font-light truncate min-w-0 text-sm">
|
||||
{file.name || "Unbekannte Datei"}
|
||||
@@ -193,11 +205,26 @@
|
||||
variant: statusConfig.variant,
|
||||
}}
|
||||
>
|
||||
<svelte:component
|
||||
this={statusConfig.icon}
|
||||
weight="fill"
|
||||
class="mr-1 shrink-0 w-3.5 h-3.5 {statusConfig.color}"
|
||||
/>
|
||||
{#if statusConfig.ping}
|
||||
<span class="mr-1 relative inline-flex size-3.5">
|
||||
<svelte:component
|
||||
this={statusConfig.icon}
|
||||
weight={statusConfig.weight ? statusConfig.weight : "fill"}
|
||||
class="opacity-57 text-amber-600 absolute inline-flex h-full w-full animate-ping"
|
||||
/>
|
||||
<svelte:component
|
||||
this={statusConfig.icon}
|
||||
weight={statusConfig.weight ? statusConfig.weight : "fill"}
|
||||
class="shrink-0 w-3.5 h-3.5 {statusConfig.color}"
|
||||
/>
|
||||
</span>
|
||||
{:else}
|
||||
<svelte:component
|
||||
this={statusConfig.icon}
|
||||
weight={statusConfig.weight ? statusConfig.weight : "fill"}
|
||||
class="mr-1 shrink-0 w-3.5 h-3.5 {statusConfig.color}"
|
||||
/>
|
||||
{/if}
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
@@ -222,26 +249,32 @@
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<div class="menu-btn-grp group/menu" class:is-open={menuOpen}>
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<!-- svelte-ignore a11y_mouse_events_have_key_events -->
|
||||
<div
|
||||
class="menu-btn-grp group/menu"
|
||||
class:is-open={menuOpen}
|
||||
on:mouseout|stopPropagation={() => (menuOpen = false)}
|
||||
>
|
||||
<div
|
||||
class="flex items-center overflow-hidden transition-all duration-300 ease-in-out
|
||||
{menuOpen
|
||||
? 'max-w-[120px] opacity-100'
|
||||
: 'max-w-0 opacity-0 group-hover/menu:max-w-[120px] group-hover/menu:opacity-100'}"
|
||||
>
|
||||
<button
|
||||
class="menu-btn danger"
|
||||
title="Löschen"
|
||||
on:click|stopPropagation={handleDeleteClick}
|
||||
>
|
||||
<button class="menu-btn danger" title="Löschen" on:click|stopPropagation={handleDeleteClick}>
|
||||
<TrashIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="menu-btn"
|
||||
title="Abspielen"
|
||||
on:click|stopPropagation={() => {
|
||||
console.log("Play", file.name);
|
||||
menuOpen = false;
|
||||
if (type === "buzzer") {
|
||||
addToast;
|
||||
} else {
|
||||
playLocalFile(file.name);
|
||||
menuOpen = false;
|
||||
}
|
||||
}}
|
||||
>
|
||||
<PlayIcon class="list-menu-icon" />
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
||||
import FileTagEditor from "./FileTagEditor.svelte";
|
||||
import FileListMenu from "./FileListMenu.svelte";
|
||||
import AudioDropzone from "./AudioDropzone.svelte";
|
||||
|
||||
let showOverlay = false;
|
||||
let isTransferFinished = false;
|
||||
@@ -18,6 +19,7 @@
|
||||
let showBuzzerMenu = false;
|
||||
let editModeType: "local" | "buzzer" | null = null;
|
||||
let fileToEdit: string | null = null;
|
||||
let isFileDragOver = false;
|
||||
|
||||
$: currentDevice = $pairedDevices.find((d) => d.id === $activeDeviceId);
|
||||
|
||||
@@ -57,26 +59,7 @@
|
||||
</script>
|
||||
|
||||
<div class="main-layout mt-16 lg:mt-20 pb-12">
|
||||
<section class="buzzer-card flex flex-col">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Dateiverarbeitung</h3>
|
||||
<button class="btn" aria-label="Einstellungen">
|
||||
<GearIcon class="icon" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="card-body p-4">
|
||||
<div class="flex justify-center items-center">
|
||||
<div class="text-center text-2xs tracking-tight font-mono font-semibold">
|
||||
16kHz 16bit MONO | <span class="text-emerald-700">NORMALIZER ON</span>
|
||||
|
|
||||
<span class="text-red-700">COMPRESSOR OFF</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-center">
|
||||
<CloudArrowUpIcon class="w-24 h-24 text-slate-500" />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<AudioDropzone />
|
||||
|
||||
<section class="buzzer-card flex flex-col">
|
||||
<div class="card-header">
|
||||
|
||||
Reference in New Issue
Block a user