*/ private const SORTABLE = [ 'title' => 'title', 'duration' => 'duration_seconds', 'status' => 'transcription_status', 'uploaded' => 'created_at', ]; public function mount(): void { $this->userId = (int) Auth::id(); $this->normalizeSort(); } public function updatedSearch(): void { $this->resetPage(); } public function sort(string $column): void { if ($column !== 'words' && ! array_key_exists($column, self::SORTABLE)) { return; } if ($this->sortBy === $column) { $this->sortDirection = $this->sortDirection === 'asc' ? 'desc' : 'asc'; } else { $this->sortBy = $column; $this->sortDirection = $column === 'uploaded' ? 'desc' : 'asc'; } $this->resetPage(); } /** * Re-render when any of this user's recordings broadcast a status change. */ #[On('echo-private:user.{userId}.recordings,.RecordingTranscriptionUpdated')] public function onTranscriptionUpdated(): void { // } 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 delete(int $recordingId): void { $recording = Auth::user()->recordings()->findOrFail($recordingId); Gate::authorize('delete', $recording); $recording->deleteFile(); $recording->delete(); Flux::toast(text: 'Recording deleted.', variant: 'success'); } public function deleteAll(): void { $deleted = 0; Auth::user()->recordings() ->orderBy('id') ->each(function (Recording $recording) use (&$deleted): void { Gate::authorize('delete', $recording); $recording->deleteFile(); $recording->delete(); $deleted++; }); $this->resetPage(); if ($deleted === 0) { Flux::toast(text: 'No recordings to delete.', variant: 'danger'); return; } Flux::toast( text: $deleted === 1 ? 'Deleted 1 recording.' : "Deleted {$deleted} recordings.", variant: 'success', ); } public function render(): View { $user = Auth::user(); $user->recordings() ->whereIn('transcription_status', ['pending', 'processing']) ->orderBy('id') ->each(fn (Recording $recording) => $recording->recoverOrphanedTranscription()); $totalCount = $user->recordings()->count(); $this->normalizeSort(); $query = $user->recordings(); $search = trim($this->search); if ($search !== '') { $query->search($search); } $this->applySort($query); $recordings = $query->paginate(20); $pendingCount = $user->recordings() ->whereIn('transcription_status', ['pending', 'failed', 'cancelled']) ->get() ->filter(fn (Recording $recording) => ! $recording->hasActiveTranscriptionJob()) ->count(); $hasActiveTranscriptions = $user->recordings() ->whereIn('transcription_status', ['pending', 'processing']) ->exists(); return view('livewire.recordings.index', [ 'recordings' => $recordings, 'search' => $search, 'pendingCount' => $pendingCount, 'hasActiveTranscriptions' => $hasActiveTranscriptions, 'totalCount' => $totalCount, ]); } private function normalizeSort(): void { if ($this->sortBy !== 'words' && ! array_key_exists($this->sortBy, self::SORTABLE)) { $this->sortBy = 'uploaded'; } if (! in_array($this->sortDirection, ['asc', 'desc'], true)) { $this->sortDirection = 'desc'; } } /** * @param Builder|HasMany $query */ private function applySort(Builder|HasMany $query): void { $direction = $this->sortDirection === 'asc' ? 'asc' : 'desc'; if ($this->sortBy === 'words') { $query->orderByRaw( 'CASE WHEN transcript IS NULL OR TRIM(transcript) = ? THEN 0 ELSE LENGTH(TRIM(transcript)) - LENGTH(REPLACE(TRIM(transcript), ?, ?)) + 1 END '.$direction, ['', ' ', ''], ); } else { $query->orderBy(self::SORTABLE[$this->sortBy], $direction); } $query->orderByDesc('id'); } }