Refresh list status over Reverb with polling fallback, clarify queued vs processing, streamline upload empty states, and seed an admin login.
216 lines
10 KiB
PHP
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>
|