Files
AndyTranscribe/resources/views/livewire/recordings/index.blade.php
T
ben c17f8fb506 Improve live recordings UI, upload flow, and default Flux theme.
Refresh list status over Reverb with polling fallback, clarify queued vs processing, streamline upload empty states, and seed an admin login.
2026-08-12 20:14:35 +02:00

216 lines
10 KiB
PHP

<div
@if ($hasActiveTranscriptions)
wire:poll.2s.visible
@endif
x-data="recordingsIndex()"
x-init="
start();
return () => destroy();
"
>
<div class="mb-8 flex flex-col gap-4 sm:flex-row sm:items-end sm:justify-between">
<div>
<flux:heading size="xl">Recordings</flux:heading>
<flux:text class="mt-1">Manage pocket-recorder audio and transcripts.</flux:text>
</div>
<div class="flex flex-col gap-2 sm:items-end">
<div class="flex items-end gap-2">
<flux:input
type="search"
wire:model.live.debounce.400ms="search"
placeholder="Search title, artist, transcript…"
class="min-w-[16rem]"
/>
</div>
<div class="flex flex-wrap items-center justify-end gap-2">
@if ($pendingCount > 0)
<flux:button
type="button"
variant="ghost"
size="sm"
wire:click="queuePending"
>
Queue {{ $pendingCount }} pending
{{ $pendingCount === 1 ? 'transcription' : 'transcriptions' }}
</flux:button>
@endif
@if ($totalCount > 0)
<flux:modal.trigger name="delete-all-recordings">
<flux:button type="button" variant="danger" size="sm">
Delete all
</flux:button>
</flux:modal.trigger>
@endif
</div>
</div>
</div>
@if ($totalCount > 0)
<flux:modal name="delete-all-recordings" class="max-w-md">
<div class="space-y-6">
<div>
<flux:heading size="lg">Delete all recordings?</flux:heading>
<flux:text class="mt-2">
This permanently removes
{{ $totalCount === 1 ? 'your 1 recording' : "all {$totalCount} recordings" }}
and their audio files. This cannot be undone.
</flux:text>
</div>
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="ghost">Cancel</flux:button>
</flux:modal.close>
<flux:button type="button" variant="danger" wire:click="deleteAll">
Delete all
</flux:button>
</div>
</div>
</flux:modal>
@endif
@if ($recordings->isEmpty())
@if (filled($search))
<flux:card class="border-dashed py-16 text-center">
<flux:text>No recordings match {{ $search }}.</flux:text>
<div class="mt-4">
<flux:button variant="ghost" wire:click="$set('search', '')">Clear search</flux:button>
</div>
</flux:card>
@else
<livewire:upload-recordings :show-cancel="false" />
@endif
@else
<audio
x-ref="player"
class="hidden"
preload="none"
@play="syncPlayer()"
@pause="syncPlayer()"
@ended="playingId = null; syncPlayer()"
></audio>
<flux:table :paginate="$recordings">
<flux:table.columns>
<flux:table.column class="w-12"></flux:table.column>
<flux:table.column>Title</flux:table.column>
<flux:table.column>Duration</flux:table.column>
<flux:table.column>Words</flux:table.column>
<flux:table.column>Status</flux:table.column>
<flux:table.column>Uploaded</flux:table.column>
<flux:table.column class="w-24"></flux:table.column>
</flux:table.columns>
<flux:table.rows>
@foreach ($recordings as $recording)
<flux:table.row wire:key="recording-{{ $recording->id }}-{{ $recording->transcription_status }}-{{ $recording->transcription_percent }}">
<flux:table.cell>
<flux:button
type="button"
variant="ghost"
size="sm"
square
data-audio-url="{{ route('recordings.audio', $recording) }}"
x-bind:aria-label="isPlayingRow({{ $recording->id }}) ? 'Pause' : 'Play'"
x-on:click="togglePlay({{ $recording->id }}, $el.dataset.audioUrl)"
>
<flux:icon.play
variant="micro"
x-show="! isPlayingRow({{ $recording->id }})"
/>
<flux:icon.pause
variant="micro"
x-show="isPlayingRow({{ $recording->id }})"
x-cloak
/>
</flux:button>
</flux:table.cell>
<flux:table.cell class="whitespace-normal">
<flux:link href="{{ route('recordings.show', $recording) }}" wire:navigate class="font-medium">
{{ $recording->title }}
</flux:link>
@if ($recording->artist)
<flux:text class="mt-0.5 text-xs">{{ $recording->artist }}</flux:text>
@endif
@if ($snippet = $recording->transcriptSnippet($search ?: null))
<flux:text class="mt-1 max-w-xl text-xs leading-relaxed">
{{ $snippet }}
</flux:text>
@endif
</flux:table.cell>
<flux:table.cell>{{ $recording->duration_formatted }}</flux:table.cell>
<flux:table.cell>
<span class="tabular-nums">
{{ $recording->word_count > 0 ? number_format($recording->word_count) : '—' }}
</span>
</flux:table.cell>
<flux:table.cell class="whitespace-normal py-2">
<x-transcription-status-badge
:status="$recording->transcription_status"
:label="$recording->transcriptionStatusLabel()"
/>
@if ($recording->transcription_status === 'pending' && filled($recording->transcription_progress))
<div
class="mt-1 max-w-[14rem] truncate text-xs text-zinc-500 dark:text-zinc-400"
title="{{ $recording->transcription_progress }}"
>
{{ $recording->transcription_progress }}
</div>
@elseif ($recording->transcription_status === 'processing' && filled($recording->transcription_progress))
<div
class="mt-1 max-w-[14rem] truncate text-xs text-amber-700 dark:text-amber-300"
title="{{ $recording->transcription_progress }}"
>
@if ($recording->transcription_percent)
{{ $recording->transcription_percent }}% ·
@endif
{{ $recording->transcription_progress }}
</div>
@endif
</flux:table.cell>
<flux:table.cell>
{{ $recording->created_at?->format('Y-m-d H:i') }}
</flux:table.cell>
<flux:table.cell>
<flux:modal.trigger name="delete-recording-{{ $recording->id }}">
<flux:button
type="button"
variant="danger"
size="sm"
square
aria-label="Delete {{ $recording->title }}"
>
<flux:icon.trash variant="micro" />
</flux:button>
</flux:modal.trigger>
<flux:modal name="delete-recording-{{ $recording->id }}" class="max-w-md">
<div class="space-y-6">
<div>
<flux:heading size="lg">Delete recording?</flux:heading>
<flux:text class="mt-2">
This permanently removes {{ $recording->title }} and its audio file.
</flux:text>
</div>
<div class="flex justify-end gap-2">
<flux:modal.close>
<flux:button variant="ghost">Cancel</flux:button>
</flux:modal.close>
<flux:button
type="button"
variant="danger"
wire:click="delete({{ $recording->id }})"
>
Delete
</flux:button>
</div>
</div>
</flux:modal>
</flux:table.cell>
</flux:table.row>
@endforeach
</flux:table.rows>
</flux:table>
@endif
</div>