Files
landing-api/app/Filament/Resources/Media/MediaResource.php
Josep Piguillem 3897113313 Initial commit: Pireon Admin — headless admin panel and public API
Laravel 13 + Filament 5 admin panel and REST API for a non-profit developer
collective, backed by the Pireon OAuth identity server. The public site is a
separate, server-rendered application consuming /api/public/v1.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-19 17:55:27 +02:00

204 lines
7.2 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace App\Filament\Resources\Media;
use App\Filament\Resources\Media\Pages\ListMedia;
use App\Jobs\ProcessMedia;
use App\Models\Media;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Notifications\Notification;
use Filament\Resources\Resource;
use Filament\Support\Icons\Heroicon;
use Filament\Tables\Columns\ImageColumn;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Filters\Filter;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Storage;
use UnitEnum;
/**
* Everything that has been uploaded, and what is known about it.
*
* Nothing here is authored: the rows are derived from the files, so this is a
* place to look rather than to write. What it answers is the set of questions
* nowhere else could — what is taking up the disk, which images never got
* processed, and which files nothing points at any more.
*/
class MediaResource extends Resource
{
protected static ?string $model = Media::class;
protected static string|BackedEnum|null $navigationIcon = Heroicon::OutlinedPhoto;
protected static ?int $navigationSort = 86;
public static function getModelLabel(): string
{
return __('panel.media.label');
}
public static function getPluralModelLabel(): string
{
return __('panel.media.plural');
}
public static function getNavigationGroup(): string|UnitEnum|null
{
return __('panel.groups.administration');
}
public static function getRecordTitle(?Model $record): ?string
{
return $record?->path;
}
public static function canCreate(): bool
{
// Files arrive by being uploaded on a record, not from here.
return false;
}
public static function table(Table $table): Table
{
return $table
->defaultSort('created_at', 'desc')
->columns([
ImageColumn::make('path')
->label('')
->disk(fn (Media $record) => $record->disk)
->height(48),
TextColumn::make('path')
->label(__('panel.media.fields.file'))
->fontFamily('mono')
->size('xs')
->wrap()
->searchable()
->description(fn (Media $record) => $record->mime),
TextColumn::make('width')
->label(__('panel.media.fields.size'))
->state(fn (Media $record) => $record->width
? "{$record->width}×{$record->height}"
: __('panel.media.pending'))
->badge()
->color(fn (Media $record) => $record->width ? 'gray' : 'warning'),
TextColumn::make('variants')
->label(__('panel.media.fields.versions'))
->state(fn (Media $record) => count($record->variants ?? []))
->badge()
->color(fn (int $state) => $state > 0 ? 'success' : 'gray'),
// Original plus every derived version: the number that
// explains where the disk went.
TextColumn::make('size')
->label(__('panel.media.fields.weight'))
->state(fn (Media $record) => round($record->totalSize() / 1024).' KB')
->sortable(),
TextColumn::make('used')
->label(__('panel.media.fields.used'))
->state(fn (Media $record) => $record->isReferenced()
? __('panel.media.in_use')
: __('panel.media.orphan'))
->badge()
->color(fn (Media $record) => $record->isReferenced() ? 'success' : 'warning'),
TextColumn::make('error')
->label(__('panel.media.fields.error'))
->wrap()
->color('danger')
->placeholder('—')
->toggleable(isToggledHiddenByDefault: true),
])
->filters([
Filter::make('pending')
->label(__('panel.media.filters.pending'))
->query(fn (Builder $query) => $query->pending()),
Filter::make('failed')
->label(__('panel.media.filters.failed'))
->query(fn (Builder $query) => $query->whereNotNull('error')),
// Everything the prune command would take away.
Filter::make('orphans')
->label(__('panel.media.filters.orphans'))
->query(fn (Builder $query) => $query
->whereNotIn('path', Media::referencedPaths())),
Filter::make('missing')
->label(__('panel.media.filters.missing'))
->query(fn (Builder $query) => $query
->whereIn('id', Media::query()->get()
->reject(fn (Media $m) => $m->exists())
->pluck('id'))),
])
->recordActions([
Action::make('open')
->label(__('panel.media.actions.open'))
->icon('heroicon-o-arrow-top-right-on-square')
->url(fn (Media $record) => $record->url())
->openUrlInNewTab(),
Action::make('reprocess')
->label(__('panel.media.actions.reprocess'))
->icon('heroicon-o-arrow-path')
->color('gray')
->action(function (Media $record): void {
ProcessMedia::dispatch($record);
Notification::make()
->title(__('panel.media.actions.reprocess_queued'))
->success()
->send();
}),
// Only offered for files nothing points at: deleting one in
// use would put a broken image on the public site.
DeleteAction::make()
->visible(fn (Media $record) => ! $record->isReferenced())
->before(function (Media $record): void {
$record->deleteDerived();
Storage::disk($record->disk)->delete($record->path);
}),
])
->emptyStateHeading(__('panel.media.empty.heading'))
->emptyStateDescription(__('panel.media.empty.description'))
->emptyStateIcon('heroicon-o-photo');
}
/**
* @return array<int, string>
*/
public static function getGloballySearchableAttributes(): array
{
return ['path'];
}
public static function getPages(): array
{
return [
'index' => ListMedia::route('/'),
];
}
public static function getNavigationBadge(): ?string
{
$pending = static::getModel()::query()->pending()->count();
return $pending > 0 ? (string) $pending : null;
}
public static function getNavigationBadgeColor(): ?string
{
return 'warning';
}
}