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.
This commit is contained in:
@@ -27,6 +27,7 @@ class RecordingTranscriptionUpdated implements ShouldBroadcastNow
|
||||
{
|
||||
return [
|
||||
new PrivateChannel('recording.'.$this->recording->id),
|
||||
new PrivateChannel('user.'.$this->recording->user_id.'.recordings'),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Livewire\Attributes\Layout;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Title;
|
||||
use Livewire\Attributes\Url;
|
||||
use Livewire\Component;
|
||||
@@ -19,14 +20,30 @@ class Index extends Component
|
||||
{
|
||||
use WithPagination;
|
||||
|
||||
public int $userId;
|
||||
|
||||
#[Url(as: 'q', history: true)]
|
||||
public string $search = '';
|
||||
|
||||
public function mount(): void
|
||||
{
|
||||
$this->userId = (int) Auth::id();
|
||||
}
|
||||
|
||||
public function updatedSearch(): void
|
||||
{
|
||||
$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;
|
||||
@@ -69,6 +86,36 @@ class Index extends Component
|
||||
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();
|
||||
@@ -78,6 +125,8 @@ class Index extends Component
|
||||
->orderBy('id')
|
||||
->each(fn (Recording $recording) => $recording->recoverOrphanedTranscription());
|
||||
|
||||
$totalCount = $user->recordings()->count();
|
||||
|
||||
$query = $user->recordings()->latest();
|
||||
|
||||
$search = trim($this->search);
|
||||
@@ -94,10 +143,16 @@ class Index extends Component
|
||||
->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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,10 +19,10 @@ class UploadRecordings extends Component
|
||||
*/
|
||||
public array $audio = [];
|
||||
|
||||
public string $title = '';
|
||||
|
||||
public bool $saving = false;
|
||||
|
||||
public bool $showCancel = true;
|
||||
|
||||
public function updatedAudio(): void
|
||||
{
|
||||
if ($this->saving || $this->audio === []) {
|
||||
@@ -40,49 +40,46 @@ class UploadRecordings extends Component
|
||||
|
||||
$this->saving = true;
|
||||
|
||||
$store ??= app(StoreUploadedRecordings::class);
|
||||
try {
|
||||
$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.',
|
||||
]);
|
||||
$this->validate([
|
||||
'audio' => ['required', 'array', 'min:1', 'max:50'],
|
||||
'audio.*' => [
|
||||
'required',
|
||||
File::types(StoreRecordingRequest::AUDIO_EXTENSIONS)->max('2gb'),
|
||||
],
|
||||
], [
|
||||
'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,
|
||||
);
|
||||
$result = $store->handle(Auth::user(), $this->audio);
|
||||
|
||||
$this->audio = [];
|
||||
$this->title = '';
|
||||
$this->saving = false;
|
||||
$this->audio = [];
|
||||
|
||||
if ($result['recordings'] === []) {
|
||||
session()->flash('error', $result['message']);
|
||||
if ($result['recordings'] === []) {
|
||||
session()->flash('error', $result['message']);
|
||||
|
||||
return $this->redirect(route('recordings.create'), navigate: true);
|
||||
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);
|
||||
} finally {
|
||||
$this->saving = false;
|
||||
}
|
||||
|
||||
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()
|
||||
|
||||
@@ -135,7 +135,7 @@ class Recording extends Model
|
||||
'ollama_url' => null,
|
||||
'transcription_status' => 'pending',
|
||||
'transcription_progress' => 'Queued — waiting to start…',
|
||||
'transcription_percent' => 5,
|
||||
'transcription_percent' => null,
|
||||
'transcription_started_at' => now(),
|
||||
'transcription_error' => null,
|
||||
// Keep the previous transcript until a new run succeeds.
|
||||
|
||||
Reference in New Issue
Block a user