Move recordings UI to Livewire pages with Flux components.

Replace controller-driven Blade views with full-page Livewire index/show/create flows so Flux tables, toasts, and uploads work natively.
This commit is contained in:
ben
2026-08-12 19:17:52 +02:00
parent bfcfc12f58
commit 34ccf0c32b
37 changed files with 1482 additions and 1103 deletions
+18
View File
@@ -0,0 +1,18 @@
<?php
namespace App\Livewire\Recordings;
use Illuminate\Contracts\View\View;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Component;
#[Layout('layouts.app')]
#[Title('Upload recording')]
class Create extends Component
{
public function render(): View
{
return view('livewire.recordings.create');
}
}
+90
View File
@@ -0,0 +1,90 @@
<?php
namespace App\Livewire\Recordings;
use App\Models\Recording;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Auth;
use Livewire\Attributes\Layout;
use Livewire\Attributes\Title;
use Livewire\Attributes\Url;
use Livewire\Component;
use Livewire\WithPagination;
#[Layout('layouts.app')]
#[Title('Recordings')]
class Index extends Component
{
use WithPagination;
#[Url(as: 'q', history: true)]
public string $search = '';
public function updatedSearch(): void
{
$this->resetPage();
}
public function queuePending(): void
{
$queued = 0;
Auth::user()->recordings()
->whereIn('transcription_status', ['pending', 'failed', 'cancelled'])
->orderBy('id')
->each(function (Recording $recording) use (&$queued): void {
if ($recording->hasActiveTranscriptionJob()) {
return;
}
$recording->queueLocalTranscription();
$queued++;
});
if ($queued === 0) {
Flux::toast(text: 'No recordings need transcription right now.', variant: 'danger');
return;
}
Flux::toast(
text: $queued === 1
? 'Queued 1 recording for transcription.'
: "Queued {$queued} recordings for transcription.",
variant: 'success',
);
}
public function render(): View
{
$user = Auth::user();
$user->recordings()
->whereIn('transcription_status', ['pending', 'processing'])
->orderBy('id')
->each(fn (Recording $recording) => $recording->recoverOrphanedTranscription());
$query = $user->recordings()->latest();
$search = trim($this->search);
if ($search !== '') {
$query->search($search);
}
$recordings = $query->paginate(20);
$pendingCount = $user->recordings()
->whereIn('transcription_status', ['pending', 'failed', 'cancelled'])
->get()
->filter(fn (Recording $recording) => ! $recording->hasActiveTranscriptionJob())
->count();
return view('livewire.recordings.index', [
'recordings' => $recordings,
'search' => $search,
'pendingCount' => $pendingCount,
]);
}
}
+70
View File
@@ -0,0 +1,70 @@
<?php
namespace App\Livewire\Recordings;
use App\Models\Recording;
use Flux\Flux;
use Illuminate\Contracts\View\View;
use Illuminate\Support\Facades\Gate;
use Livewire\Attributes\Layout;
use Livewire\Component;
#[Layout('layouts.app')]
class Show extends Component
{
public Recording $recording;
public function mount(Recording $recording): void
{
Gate::authorize('view', $recording);
$recording->recoverOrphanedTranscription();
$recording->refresh();
$this->recording = $recording;
}
public function startTranscription(): void
{
Gate::authorize('transcribe', $this->recording);
$this->recording->queueLocalTranscription();
$this->recording->refresh();
Flux::toast(text: 'Transcription started. Progress updates below.', variant: 'success');
}
public function cancelTranscription(): void
{
Gate::authorize('transcribe', $this->recording);
if (! $this->recording->isTranscribing()) {
Flux::toast(text: 'No transcription is currently running.', variant: 'danger');
return;
}
$this->recording->cancelTranscription();
$this->recording->refresh();
Flux::toast(text: 'Transcription stopped.', variant: 'success');
}
public function delete(): mixed
{
Gate::authorize('delete', $this->recording);
$this->recording->deleteFile();
$this->recording->delete();
session()->flash('success', 'Recording deleted.');
return $this->redirect(route('recordings.index'), navigate: true);
}
public function render(): View
{
return view('livewire.recordings.show')
->title($this->recording->title);
}
}
+92
View File
@@ -0,0 +1,92 @@
<?php
namespace App\Livewire;
use App\Actions\StoreUploadedRecordings;
use App\Http\Requests\StoreRecordingRequest;
use Illuminate\Support\Facades\Auth;
use Illuminate\Validation\Rules\File;
use Livewire\Component;
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
use Livewire\WithFileUploads;
class UploadRecordings extends Component
{
use WithFileUploads;
/**
* @var list<TemporaryUploadedFile>
*/
public array $audio = [];
public string $title = '';
public bool $saving = false;
public function updatedAudio(): void
{
if ($this->saving || $this->audio === []) {
return;
}
$this->save();
}
public function save(?StoreUploadedRecordings $store = null): mixed
{
if ($this->saving) {
return null;
}
$this->saving = true;
$store ??= app(StoreUploadedRecordings::class);
$this->validate([
'audio' => ['required', 'array', 'min:1', 'max:50'],
'audio.*' => [
'required',
File::types(StoreRecordingRequest::AUDIO_EXTENSIONS)->max('2gb'),
],
'title' => ['nullable', 'string', 'max:255'],
], [
'audio.required' => 'Please choose at least one audio file to upload.',
'audio.min' => 'Please choose at least one audio file to upload.',
'audio.max' => 'You can upload at most 50 files at once.',
'audio.*' => 'Unsupported audio type. Use MP3, WAV, OGG, FLAC, M4A, AAC, WebM, WMA, or AIFF.',
'audio.*.max' => 'Each audio file may not be larger than 2 GB.',
]);
$result = $store->handle(
Auth::user(),
$this->audio,
filled($this->title) ? $this->title : null,
);
$this->audio = [];
$this->title = '';
$this->saving = false;
if ($result['recordings'] === []) {
session()->flash('error', $result['message']);
return $this->redirect(route('recordings.create'), navigate: true);
}
session()->flash('success', $result['message']);
if (count($result['recordings']) === 1) {
return $this->redirect(
route('recordings.show', $result['recordings'][0]),
navigate: true,
);
}
return $this->redirect(route('recordings.index'), navigate: true);
}
public function render()
{
return view('livewire.upload-recordings');
}
}