File upload. Yeah
This commit is contained in:
@@ -1,16 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { BuzzerFile } from "../lib/types";
|
||||
import { type BuzzerFile, SyncState } from "../lib/types";
|
||||
import {
|
||||
MusicNotesIcon,
|
||||
DotsThreeVerticalIcon,
|
||||
PlayIcon,
|
||||
TrashIcon,
|
||||
InfoIcon,
|
||||
PencilIcon,
|
||||
CircleIcon,
|
||||
QuestionIcon,
|
||||
UserCircleCheckIcon,
|
||||
WarningCircleIcon,
|
||||
PersonIcon,
|
||||
TagIcon,
|
||||
UserIcon,
|
||||
} from "phosphor-svelte";
|
||||
@@ -20,6 +17,7 @@
|
||||
transferDetails,
|
||||
buzzerAudioFiles,
|
||||
localAudioFiles,
|
||||
syncStateMap,
|
||||
} from "../lib/store";
|
||||
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
@@ -43,6 +41,8 @@
|
||||
return "default";
|
||||
})();
|
||||
|
||||
$: syncStatus = $syncStateMap[type][file.name] || { state: SyncState.UNKNOWN, linkedFiles: [] };
|
||||
|
||||
function toggleSelection() {
|
||||
if ($isFetchingRemote) return;
|
||||
|
||||
@@ -102,39 +102,34 @@
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-0 text-xs text-text-muted mt-0.5 min-w-0">
|
||||
{#if file.sysTags?.crc32}
|
||||
{#if syncStatus.state === SyncState.UNKNOWN}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text:
|
||||
"crc32: <span class='font-mono'>0x" +
|
||||
file.sysTags.crc32.toString(16).toUpperCase() +
|
||||
"</span>",
|
||||
pos: "right",
|
||||
"Dieser Datei fehlt der CRC32-Tag. Bitte aktualisiere über das Menu die Metadaten.",
|
||||
pos: "right",
|
||||
variant: "warning",
|
||||
}}
|
||||
>
|
||||
<CircleIcon weight="fill" class="mr-1 shrink-0 text-emerald-600 w-3.5 h-3.5" />
|
||||
</span>
|
||||
{:else}
|
||||
<span
|
||||
use:tooltip={{
|
||||
text: "Keine Prüfsumme in den Tags verfügbar. Bitte aktualisiere über das Menü die CRC32-Tags.",
|
||||
pos: "right",
|
||||
variant: "warning",
|
||||
}}
|
||||
>
|
||||
<QuestionIcon weight="fill" class="mr-1 shrink-0 text-amber-500 w-3.5 h-3.5" />
|
||||
</span>
|
||||
{/if}
|
||||
|
||||
<span class="font-light shrink-0">
|
||||
{parseFloat((file.size / 1024).toFixed(1))} kB
|
||||
</span>
|
||||
{#if file.metaTags?.a}
|
||||
<UserIcon weight="fill" class="ml-1 pl-1 mr-0.5 shrink-0 text-slate-500 w-4.5 h-3.5 border-l border-l-text-muted" />
|
||||
<UserIcon
|
||||
weight="fill"
|
||||
class="ml-1 pl-1 mr-0.5 shrink-0 text-slate-500 w-4.5 h-3.5 border-l border-l-text-muted"
|
||||
/>
|
||||
<span class="truncate min-w-0">{file.metaTags.a}</span>
|
||||
{/if}
|
||||
{#if file.metaTags?.c}
|
||||
<TagIcon weight="fill" class="ml-1 pl-1 mr-0.5 shrink-0 text-slate-500 w-4.5 h-3.5 border-l border-l-text-muted" />
|
||||
<TagIcon
|
||||
weight="fill"
|
||||
class="ml-1 pl-1 mr-0.5 shrink-0 text-slate-500 w-4.5 h-3.5 border-l border-l-text-muted"
|
||||
/>
|
||||
<span class="truncate min-w-0">{file.metaTags.c}</span>
|
||||
{/if}
|
||||
</div>
|
||||
@@ -156,7 +151,7 @@
|
||||
menuOpen = false;
|
||||
}}
|
||||
>
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
<TrashIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="menu-btn"
|
||||
@@ -166,18 +161,17 @@
|
||||
menuOpen = false;
|
||||
}}
|
||||
>
|
||||
<PlayIcon class="w-5 h-5" />
|
||||
<PlayIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="menu-btn"
|
||||
title="Datei-Info"
|
||||
on:click|stopPropagation={() => {
|
||||
console.log("Info", file.name);
|
||||
tagEditorState.set({ show: true, type, fileName: file.name });
|
||||
menuOpen = false;
|
||||
}}
|
||||
>
|
||||
<InfoIcon class="w-5 h-5" />
|
||||
<PencilIcon class="list-menu-icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -185,7 +179,7 @@
|
||||
class="menu-btn !border-r-transparent"
|
||||
on:click|stopPropagation={() => (menuOpen = !menuOpen)}
|
||||
>
|
||||
<DotsThreeVerticalIcon class="w-5 h-5" />
|
||||
<DotsThreeVerticalIcon class="list-menu-icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -196,17 +190,17 @@
|
||||
.menu-btn-grp {
|
||||
@apply absolute right-2 top-1/2 -translate-y-1/2 z-20 overflow-hidden
|
||||
p-0 flex items-center backdrop-blur-sm
|
||||
border border-transparent rounded-full transition-all;
|
||||
rounded-full transition-all;
|
||||
}
|
||||
|
||||
/* Kombiniert Hover von Mausnutzern und aktiven Touch-Zustand */
|
||||
.menu-btn-grp:hover,
|
||||
.menu-btn-grp.is-open {
|
||||
@apply border-border-card bg-white shadow-sm;
|
||||
@apply bg-white shadow-sm;
|
||||
}
|
||||
|
||||
.menu-btn {
|
||||
@apply p-1.5 border-r border-r-border-card
|
||||
@apply border-r border-r-border-card
|
||||
flex items-center justify-center shrink-0 transition-colors;
|
||||
|
||||
&:not(:disabled):not(.danger) {
|
||||
|
||||
138
webpage/src/components/FileListMenu.svelte
Normal file
138
webpage/src/components/FileListMenu.svelte
Normal file
@@ -0,0 +1,138 @@
|
||||
<script lang="ts">
|
||||
import { localAudioFiles, buzzerAudioFiles } from "../lib/store";
|
||||
import { isConnected, fsInfo, tagEditorState } from "../lib/store";
|
||||
|
||||
import { deleteSelectedLocalFiles } from "../lib/sync";
|
||||
import { addToast } from "../lib/toast";
|
||||
import { tooltip } from "../lib/actions/tooltip";
|
||||
import { updateLocalAudioCrc } from "../lib/tagHandler";
|
||||
import {
|
||||
refreshLocal,
|
||||
refreshRemote,
|
||||
downloadSelectedFiles,
|
||||
uploadSelectedFiles,
|
||||
} from "../lib/sync";
|
||||
|
||||
import {
|
||||
GearIcon,
|
||||
CloudArrowUpIcon,
|
||||
ArrowClockwiseIcon,
|
||||
DotsThreeVerticalIcon,
|
||||
CheckSquareOffsetIcon,
|
||||
SquareIcon,
|
||||
DownloadIcon,
|
||||
TrashIcon,
|
||||
FingerprintIcon,
|
||||
UploadIcon,
|
||||
} from "phosphor-svelte";
|
||||
|
||||
export let type: "local" | "buzzer" = "buzzer";
|
||||
export let onToggleMenu: () => void; // Callback-Prop definieren
|
||||
|
||||
let showMenu = false;
|
||||
|
||||
$: activeStore = type === "local" ? localAudioFiles : buzzerAudioFiles;
|
||||
$: fileCount = $activeStore.length;
|
||||
$: totalSize = $activeStore.reduce((sum, f) => sum + f.size, 0);
|
||||
$: selectedFileCount = $activeStore.filter((f) => f.selected).length;
|
||||
$: selectedFileSize = $activeStore.filter((f) => f.selected).reduce((sum, f) => sum + f.size, 0);
|
||||
</script>
|
||||
|
||||
<div class="btn-connect-group {$isConnected || type === 'local' ? 'active group' : ''}">
|
||||
{#if type === "buzzer"}
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Alle ausgewählten Dateien herunterladen"
|
||||
on:click={() => {
|
||||
downloadSelectedFiles();
|
||||
}}
|
||||
disabled={!$isConnected || selectedFileCount === 0}
|
||||
title="Alle ausgewählten Dateien herunterladen"
|
||||
>
|
||||
<DownloadIcon class="list-menu-icon" />
|
||||
</button>
|
||||
{:else}
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Alle ausgewählten Dateien herunterladen"
|
||||
on:click={() => {
|
||||
uploadSelectedFiles();
|
||||
}}
|
||||
disabled={!$isConnected || selectedFileCount === 0}
|
||||
title="Alle ausgewählten Dateien hochladen"
|
||||
>
|
||||
<UploadIcon class="list-menu-icon" />
|
||||
</button>
|
||||
{/if}
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Alles Auswählen"
|
||||
on:click={() => {
|
||||
activeStore.update((files) => files.map((f) => ({ ...f, selected: true })));
|
||||
}}
|
||||
disabled={(!$isConnected && type === "buzzer") || selectedFileCount === fileCount}
|
||||
title="Alle auswählen"
|
||||
>
|
||||
<CheckSquareOffsetIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
on:click={() => {
|
||||
activeStore.update((files) => files.map((f) => ({ ...f, selected: false })));
|
||||
}}
|
||||
aria-label="Auswahl löschen"
|
||||
disabled={(!$isConnected && type === "buzzer") || selectedFileCount === 0}
|
||||
title="Auswahl aufheben"
|
||||
>
|
||||
<SquareIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
on:click={() => {
|
||||
if (type === "buzzer") {
|
||||
refreshRemote();
|
||||
} else {
|
||||
refreshLocal();
|
||||
}
|
||||
}}
|
||||
aria-label="Reload"
|
||||
disabled={!$isConnected && type === "buzzer"}
|
||||
title="Dateiliste neu laden"
|
||||
>
|
||||
<ArrowClockwiseIcon class="list-menu-icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Menu"
|
||||
disabled={!$isConnected && type === "buzzer"}
|
||||
title="Menu"
|
||||
on:click|stopPropagation={onToggleMenu}
|
||||
>
|
||||
<DotsThreeVerticalIcon class="list-menu-icon" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
@reference "../styles/app.css";
|
||||
|
||||
.btn-connect-group {
|
||||
@apply flex items-stretch rounded-full overflow-hidden transition-all;
|
||||
}
|
||||
|
||||
.btn-connect-group.active {
|
||||
@apply hover:border-border-card hover:shadow-sm hover:bg-white;
|
||||
}
|
||||
|
||||
.btn-main {
|
||||
@apply border-r-1 border-transparent last:border-r-0 focus:outline-none;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:shadow-sm hover:bg-slate-200 cursor-pointer group-hover:border-r-slate-200;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: color-mix(in srgb, currentColor 50%, transparent);
|
||||
@apply cursor-not-allowed group-hover:border-r-slate-200 border-r-transparent;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -35,7 +35,7 @@
|
||||
}
|
||||
|
||||
function formatSize(bytes: number): string {
|
||||
if (bytes < 1024) return `${bytes}\u202B`;
|
||||
if (bytes < 1024) return `${bytes}\u202FB`;
|
||||
else if (bytes < 10 * 1024) return `${(bytes / 1024).toFixed(2)}\u202F\KiB`;
|
||||
else if (bytes < 100 * 1024) return `${(bytes / 1024).toFixed(1)}\u202F\KiB`;
|
||||
else if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(0)}\u202F\KiB`;
|
||||
|
||||
@@ -311,7 +311,7 @@
|
||||
value={activeName}
|
||||
on:input={updateName}
|
||||
maxlength={maxFilenameLength}
|
||||
class="editor-input font-medium {getInputClass(
|
||||
class="editor-input {getInputClass(
|
||||
hasDraft && activeName !== currentFile.name,
|
||||
)}"
|
||||
/>
|
||||
|
||||
@@ -1,37 +1,21 @@
|
||||
<script lang="ts">
|
||||
import { isConnected, fsInfo, tagEditorState } from "../lib/store";
|
||||
import {
|
||||
GearIcon,
|
||||
CloudArrowUpIcon,
|
||||
ArrowClockwiseIcon,
|
||||
DotsThreeVerticalIcon,
|
||||
CheckSquareOffsetIcon,
|
||||
SquareIcon,
|
||||
DownloadIcon,
|
||||
} from "phosphor-svelte";
|
||||
import { GearIcon, CloudArrowUpIcon, DotsThreeVerticalIcon } from "phosphor-svelte";
|
||||
import FileList from "./FileList.svelte";
|
||||
import DeviceInfo from "./DeviceInfo.svelte";
|
||||
import { refreshRemote, downloadSelectedFiles } from "../lib/sync";
|
||||
import {
|
||||
buzzerAudioFiles,
|
||||
buzzerFilesCount,
|
||||
selectedBuzzerFilesCount,
|
||||
transferStats,
|
||||
isFetchingRemote,
|
||||
pairedDevices,
|
||||
activeDeviceId,
|
||||
transferDetails,
|
||||
} from "../lib/store";
|
||||
import { refreshRemote } from "../lib/sync";
|
||||
import { transferStats, isFetchingRemote, pairedDevices, activeDeviceId } from "../lib/store";
|
||||
import { SETTINGS } from "../lib/settings";
|
||||
import TransferProgress from "./TransferProgress.svelte";
|
||||
import FileMenuOverlay from "./FileMenuOverlay.svelte";
|
||||
import FileTagEditor from "./FileTagEditor.svelte";
|
||||
import FileListMenu from "./FileListMenu.svelte";
|
||||
|
||||
let showOverlay = false;
|
||||
let isTransferFinished = false;
|
||||
let overlayTimeout: ReturnType<typeof setTimeout>;
|
||||
let showBuzzerMenu = false;
|
||||
let showLocalMenu = false;
|
||||
let showBuzzerMenu = false;
|
||||
let editModeType: "local" | "buzzer" | null = null;
|
||||
let fileToEdit: string | null = null;
|
||||
|
||||
@@ -117,13 +101,7 @@
|
||||
<section class="buzzer-card relative self-start">
|
||||
<div class="card-header">
|
||||
<h3 class="card-title">Lokale Bibliothek</h3>
|
||||
<button
|
||||
class="btn"
|
||||
aria-label="Menu"
|
||||
on:click|stopPropagation={() => (showLocalMenu = !showLocalMenu)}
|
||||
>
|
||||
<DotsThreeVerticalIcon class="icon" />
|
||||
</button>
|
||||
<FileListMenu type="local" onToggleMenu={() => (showLocalMenu = !showLocalMenu)} />
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="col-start-1 row-start-1">
|
||||
@@ -145,75 +123,23 @@
|
||||
</div>
|
||||
|
||||
<div class="col-start-1 row-start-1 z-20" class:pointer-events-none={!showLocalMenu}>
|
||||
<FileMenuOverlay type="local" show={showLocalMenu} onClose={() => (showLocalMenu = false)} />
|
||||
<FileMenuOverlay
|
||||
type="local"
|
||||
show={showLocalMenu}
|
||||
onClose={() => (showLocalMenu = false)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="buzzer-card relative self-start">
|
||||
<div class="card-header">
|
||||
<div>
|
||||
<h3 class="card-title">
|
||||
Gerätebibliothek <span class="text-text-muted text-xs font-mono">
|
||||
{$fsInfo?.audioPath}
|
||||
</span>
|
||||
</h3>
|
||||
</div>
|
||||
<div>
|
||||
<div class="btn-connect-group group">
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Alle ausgewählten Dateien herunterladen"
|
||||
on:click={() => {
|
||||
downloadSelectedFiles();
|
||||
}}
|
||||
disabled={!$isConnected || $selectedBuzzerFilesCount === 0}
|
||||
title="Alle ausgewählten Dateien herunterladen"
|
||||
>
|
||||
<DownloadIcon class="icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Alles Auswählen"
|
||||
on:click={() => {
|
||||
buzzerAudioFiles.update((files) => files.map((f) => ({ ...f, selected: true })));
|
||||
}}
|
||||
disabled={!$isConnected || $buzzerFilesCount === $selectedBuzzerFilesCount}
|
||||
title="Alle auswählen"
|
||||
>
|
||||
<CheckSquareOffsetIcon class="icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
on:click={() => {
|
||||
buzzerAudioFiles.update((files) => files.map((f) => ({ ...f, selected: false })));
|
||||
}}
|
||||
aria-label="Auswahl löschen"
|
||||
disabled={!$isConnected || $selectedBuzzerFilesCount === 0}
|
||||
title="Auswahl aufheben"
|
||||
>
|
||||
<SquareIcon class="icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
on:click={() => refreshRemote()}
|
||||
aria-label="Reload"
|
||||
disabled={!$isConnected}
|
||||
title="Dateiliste neu laden"
|
||||
>
|
||||
<ArrowClockwiseIcon class="icon" />
|
||||
</button>
|
||||
<button
|
||||
class="btn-main"
|
||||
aria-label="Menu"
|
||||
disabled={!$isConnected}
|
||||
title="Menu"
|
||||
on:click|stopPropagation={() => (showBuzzerMenu = !showBuzzerMenu)}
|
||||
>
|
||||
<DotsThreeVerticalIcon class="icon" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<h3 class="card-title">
|
||||
Gerätebibliothek <span class="text-text-muted text-xs font-mono">
|
||||
{$fsInfo?.audioPath}
|
||||
</span>
|
||||
</h3>
|
||||
<FileListMenu type="buzzer" onToggleMenu={() => (showBuzzerMenu = !showBuzzerMenu)} />
|
||||
</div>
|
||||
<div class="grid">
|
||||
<div class="col-start-1 row-start-1">
|
||||
@@ -222,6 +148,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="col-start-1 row-start-1 z-30"
|
||||
class:hidden={!$tagEditorState.show || $tagEditorState.type !== "buzzer"}
|
||||
>
|
||||
<FileTagEditor
|
||||
type="buzzer"
|
||||
show={$tagEditorState.show && $tagEditorState.type === "buzzer"}
|
||||
initialFileName={$tagEditorState.fileName}
|
||||
onClose={() => tagEditorState.set({ show: false, type: "buzzer", fileName: "" })}
|
||||
/>
|
||||
</div>
|
||||
<div class="col-start-1 row-start-1 z-20" class:pointer-events-none={!showBuzzerMenu}>
|
||||
<FileMenuOverlay
|
||||
type="buzzer"
|
||||
@@ -247,20 +184,4 @@
|
||||
@apply cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
.btn-connect-group {
|
||||
@apply flex items-stretch rounded-full overflow-hidden border-transparent transition-all hover:border-slate-200 hover:shadow-sm;
|
||||
}
|
||||
|
||||
.btn-main {
|
||||
@apply border-r-1 border-r-transparent last:border-r-0 focus:outline-none;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:border-border-card hover:shadow-sm hover:bg-slate-200 cursor-pointer group-hover:border-r-slate-200;
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
color: color-mix(in srgb, currentColor 50%, transparent);
|
||||
@apply cursor-not-allowed group-hover:border-r-slate-200;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user