diff --git a/.gitignore b/.gitignore index c7cf1fa..3a324ad 100644 --- a/.gitignore +++ b/.gitignore @@ -21,3 +21,4 @@ yarn-error.log /.nova /.vscode /.zed +.vite diff --git a/app/Console/Commands/MoveMediaToPublic.php b/app/Console/Commands/MoveMediaToPublic.php new file mode 100644 index 0000000..bce2727 --- /dev/null +++ b/app/Console/Commands/MoveMediaToPublic.php @@ -0,0 +1,112 @@ +option('dry-run'); + + if ($dryRun) { + $this->warn('DRY RUN MODE - No files will actually be moved'); + } + + // Get all media records using local disk + $mediaRecords = Media::where('disk', 'local')->get(); + + if ($mediaRecords->isEmpty()) { + $this->info('No media records found using local disk.'); + return self::SUCCESS; + } + + $this->info("Found {$mediaRecords->count()} media records to migrate."); + + $progressBar = $this->output->createProgressBar($mediaRecords->count()); + $progressBar->start(); + + $moved = 0; + $errors = 0; + + foreach ($mediaRecords as $media) { + // Use relative path: {id}/{filename} + $relativePath = $media->id . '/' . $media->file_name; + + // Check if source file exists + if (!Storage::disk('local')->exists($relativePath)) { + $this->newLine(); + $this->error("Source file not found: {$relativePath}"); + $errors++; + $progressBar->advance(); + continue; + } + + try { + if (!$dryRun) { + // Copy file from local to public disk + $fileContent = Storage::disk('local')->get($relativePath); + Storage::disk('public')->put($relativePath, $fileContent); + + // Verify the file was copied successfully + if (Storage::disk('public')->exists($relativePath)) { + // Update the database record + $media->update([ + 'disk' => 'public', + 'conversions_disk' => 'public', + ]); + + // Delete the old file from local disk + Storage::disk('local')->delete($relativePath); + + $moved++; + } else { + throw new \Exception("Failed to copy file to public disk"); + } + } else { + $this->newLine(); + $this->line("Would move: local:{$relativePath} -> public:{$relativePath}"); + $moved++; + } + } catch (\Exception $e) { + $this->newLine(); + $this->error("Error moving {$relativePath}: {$e->getMessage()}"); + $errors++; + } + + $progressBar->advance(); + } + + $progressBar->finish(); + $this->newLine(2); + + if ($dryRun) { + $this->info("DRY RUN: Would move {$moved} files, {$errors} errors encountered."); + $this->info("Run without --dry-run to actually perform the migration."); + } else { + $this->info("Successfully moved {$moved} files, {$errors} errors encountered."); + } + + return self::SUCCESS; + } +} diff --git a/app/Filament/Resources/Entries/Schemas/EntryForm.php b/app/Filament/Resources/Entries/Schemas/EntryForm.php index 82e163d..6c029f6 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryForm.php +++ b/app/Filament/Resources/Entries/Schemas/EntryForm.php @@ -2,13 +2,18 @@ namespace App\Filament\Resources\Entries\Schemas; +use Filament\Actions\Action; use Filament\Forms\Components\DatePicker; use Filament\Forms\Components\RichEditor; -use Filament\Forms\Components\TextInput; +use Filament\Forms\Components\Select; +use Filament\Forms\Components\SpatieMediaLibraryFileUpload; use Filament\Forms\Components\Textarea; +use Filament\Forms\Components\TextInput; use Filament\Forms\Components\Toggle; use Filament\Schemas\Schema; +use Illuminate\Support\Facades\Storage; use Illuminate\Support\Str; +use Spatie\MediaLibrary\MediaCollections\Models\Media; class EntryForm { @@ -18,22 +23,183 @@ class EntryForm ->components([ TextInput::make('title') ->required() - ->reactive() + ->live(onBlur: true) ->afterStateUpdated(function ($state, $set): void { $set('slug', Str::slug((string) $state)); }), TextInput::make('slug') ->required() - ->disabled(), + ->dehydrated() + ->readOnly(), Textarea::make('description') ->columnSpanFull(), + SpatieMediaLibraryFileUpload::make('featured_image') + ->collection('featured-image') + ->image() + ->imageEditor() + ->disk('public') + ->visibility('public') + ->columnSpanFull() + ->dehydrated(false) + ->hintAction( + Action::make('featured_picker') + ->label('Pick from Gallery') + ->icon('heroicon-m-photo') + ->schema([ + Select::make('image_id') + ->label('Select an existing image') + ->allowHtml() + ->options(function () { + return Media::where('model_type', 'temp') + ->where('model_id', 0) + ->where('disk', 'public') + ->latest() + ->limit(30) + ->get(['id', 'file_name', 'name', 'disk']) + ->mapWithKeys(function (Media $item) { + try { + $url = $item->getUrl(); + $fileName = e($item->file_name); + $name = e($item->name ?? ''); + + $html = "
" . + "{$fileName}" . + "
" . + "{$name}" . + "{$fileName}" . + "
"; + + return [$item->id => $html]; + } catch (\Exception $e) { + return []; + } + })->toArray(); + }) + ->searchable() + ->preload() + ->required(), + ]) + ->action(function (array $data, SpatieMediaLibraryFileUpload $component): void { + $record = $component->getRecord(); + + if (!$record) { + \Filament\Notifications\Notification::make() + ->warning() + ->title('Save the entry first') + ->send(); + return; + } + + if (!$data['image_id']) { + return; + } + + $sourceMedia = Media::find($data['image_id']); + if (!$sourceMedia || !file_exists($sourceMedia->getPath())) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Image file not found') + ->send(); + return; + } + + $sourceFile = $sourceMedia->getPath(); + $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; + copy($sourceFile, $tempCopy); + + try { + // Verify record has ID + if (!$record->id) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Entry must be saved first') + ->send(); + return; + } + + // Add the copy to the entry's featured-image collection + $newMedia = $record->addMedia($tempCopy) + ->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME)) + ->usingFileName($sourceMedia->file_name) + ->toMediaCollection('featured-image', 'public'); + + // Dispatch event for app.js to handle + $component->getLivewire()->dispatch('featured-image-added', ['mediaId' => $newMedia->id]); + + \Filament\Notifications\Notification::make() + ->success() + ->title('Image added to featured image') + ->send(); + } catch (\Exception $e) { + \Filament\Notifications\Notification::make() + ->danger() + ->title('Error: ' . $e->getMessage()) + ->send(); + } finally { + if (file_exists($tempCopy)) { + unlink($tempCopy); + } + } + }) + ), Toggle::make('is_published') ->required(), Toggle::make('is_featured') ->required(), DatePicker::make('published_at'), RichEditor::make('content') - ->columnSpanFull(), + ->columnSpanFull() + ->hintAction( + Action::make('picker') + ->label('Gallery Picker') + ->icon('heroicon-m-photo') + ->schema([ + Select::make('image_url') + ->label('Select an existing image') + ->allowHtml() + ->options(function () { + return Media::latest() + ->limit(30) // Limit to 30 most recent items for performance + ->get(['id', 'file_name', 'name', 'uuid', 'collection_name', 'model_type', 'model_id', 'disk']) + ->filter(function (Media $item) { + // Only include media items that have a valid disk + return $item->disk !== null; + }) + ->mapWithKeys(function (Media $item) { + try { + $url = $item->getUrl(); + } catch (\Exception $e) { + // Skip items that can't generate URLs + return []; + } + + $fileName = e($item->file_name); + $name = e($item->name ?? ''); + + // Smaller image preview for better performance + $html = "
" . + "{$fileName}" . + "
" . + "{$name}" . + "{$fileName}" . + "
"; + + return [$url => $html]; + })->toArray(); + }) + ->searchable() + ->preload() + ->required(), + ]) + ->action(function (array $data, RichEditor $component) { + // We dispatch the URL to the browser to be inserted into TipTap + $component->getLivewire()->dispatch('insert-editor-content', [ + 'statePath' => $component->getStatePath(), + 'html' => "", + ]); + }) + ), + ]); } } diff --git a/app/Filament/Resources/Entries/Schemas/EntryInfolist.php b/app/Filament/Resources/Entries/Schemas/EntryInfolist.php index 6a5d87d..be741bc 100644 --- a/app/Filament/Resources/Entries/Schemas/EntryInfolist.php +++ b/app/Filament/Resources/Entries/Schemas/EntryInfolist.php @@ -3,6 +3,7 @@ namespace App\Filament\Resources\Entries\Schemas; use Filament\Infolists\Components\IconEntry; +use Filament\Infolists\Components\SpatieMediaLibraryImageEntry; use Filament\Infolists\Components\TextEntry; use Filament\Schemas\Schema; @@ -17,6 +18,9 @@ class EntryInfolist TextEntry::make('description') ->placeholder('-') ->columnSpanFull(), + SpatieMediaLibraryImageEntry::make('featured_image') + ->collection('featured-image') + ->columnSpanFull(), IconEntry::make('is_published') ->boolean(), IconEntry::make('is_featured') diff --git a/app/Filament/Resources/Entries/Tables/EntriesTable.php b/app/Filament/Resources/Entries/Tables/EntriesTable.php index 57035c0..eb89fd4 100644 --- a/app/Filament/Resources/Entries/Tables/EntriesTable.php +++ b/app/Filament/Resources/Entries/Tables/EntriesTable.php @@ -7,6 +7,7 @@ use Filament\Actions\DeleteBulkAction; use Filament\Actions\EditAction; use Filament\Actions\ViewAction; use Filament\Tables\Columns\IconColumn; +use Filament\Tables\Columns\SpatieMediaLibraryImageColumn; use Filament\Tables\Columns\TextColumn; use Filament\Tables\Table; @@ -16,6 +17,11 @@ class EntriesTable { return $table ->columns([ + SpatieMediaLibraryImageColumn::make('featured_image') + ->collection('featured-image') + ->circular() + ->stacked() + ->limit(3), TextColumn::make('title') ->searchable(), TextColumn::make('slug') diff --git a/app/Filament/Resources/Media/MediaResource.php b/app/Filament/Resources/Media/MediaResource.php new file mode 100644 index 0000000..2ac19fe --- /dev/null +++ b/app/Filament/Resources/Media/MediaResource.php @@ -0,0 +1,58 @@ + ListMedia::route('/'), + 'create' => CreateMedia::route('/create'), + 'view' => ViewMedia::route('/{record}'), + 'edit' => EditMedia::route('/{record}/edit'), + ]; + } +} diff --git a/app/Filament/Resources/Media/Pages/CreateMedia.php b/app/Filament/Resources/Media/Pages/CreateMedia.php new file mode 100644 index 0000000..c652a96 --- /dev/null +++ b/app/Filament/Resources/Media/Pages/CreateMedia.php @@ -0,0 +1,61 @@ +uploadedFile = $file; + + // Set required fields for Media model + $data['model_type'] = $data['model_type'] ?? 'temp'; + $data['model_id'] = $data['model_id'] ?? 0; + $data['collection_name'] = $data['collection_name'] ?? 'default'; + $data['disk'] = $data['disk'] ?? 'public'; + $data['file_name'] = $file ? basename($file) : ''; + $data['mime_type'] = $file && Storage::disk('public')->exists($file) + ? Storage::disk('public')->mimeType($file) + : 'application/octet-stream'; + $data['size'] = $file && Storage::disk('public')->exists($file) + ? Storage::disk('public')->size($file) + : 0; + $data['manipulations'] = []; + $data['custom_properties'] = []; + $data['generated_conversions'] = []; + $data['responsive_images'] = []; + + return $data; + } + + protected function afterCreate(): void + { + if ($this->uploadedFile && $this->record) { + $disk = Storage::disk('public'); + + // Create the directory for this media ID (Spatie structure: {id}/{filename}) + $mediaDirectory = (string) $this->record->id; + $disk->makeDirectory($mediaDirectory); + + // Move file from temporary upload location to Spatie's expected location + if ($disk->exists($this->uploadedFile)) { + $newPath = $mediaDirectory.'/'.$this->record->file_name; + $disk->move($this->uploadedFile, $newPath); + } + } + } +} diff --git a/app/Filament/Resources/Media/Pages/EditMedia.php b/app/Filament/Resources/Media/Pages/EditMedia.php new file mode 100644 index 0000000..b6f4cdb --- /dev/null +++ b/app/Filament/Resources/Media/Pages/EditMedia.php @@ -0,0 +1,72 @@ +record->getPathRelativeToRoot()) { + $this->uploadedFile = $file; + + // Keep the original file_name to prevent breaking existing references + // $data['file_name'] is not updated - we preserve the original filename + $data['mime_type'] = Storage::disk('public')->exists($file) + ? Storage::disk('public')->mimeType($file) + : 'application/octet-stream'; + $data['size'] = Storage::disk('public')->exists($file) + ? Storage::disk('public')->size($file) + : 0; + } + + return $data; + } + + protected function afterSave(): void + { + if ($this->uploadedFile && $this->record) { + $disk = Storage::disk('public'); + $mediaDirectory = (string) $this->record->id; + + // Delete old file if it exists + $oldPath = $mediaDirectory.'/'.$this->record->getOriginal('file_name'); + if ($disk->exists($oldPath)) { + $disk->delete($oldPath); + } + + // Move new file to Spatie's expected location using the original filename + if ($disk->exists($this->uploadedFile)) { + $disk->makeDirectory($mediaDirectory); + // Use the original file_name to preserve existing references + $newPath = $mediaDirectory.'/'.$this->record->file_name; + $disk->move($this->uploadedFile, $newPath); + } + + // Redirect to the same page to refresh the form state + $this->redirect(static::getUrl(['record' => $this->record]), navigate: true); + } + } +} diff --git a/app/Filament/Resources/Media/Pages/ListMedia.php b/app/Filament/Resources/Media/Pages/ListMedia.php new file mode 100644 index 0000000..2e6b63d --- /dev/null +++ b/app/Filament/Resources/Media/Pages/ListMedia.php @@ -0,0 +1,19 @@ +components([ + TextInput::make('name') + ->required() + ->maxLength(255), + TextInput::make('collection_name') + ->default('default') + ->required() + ->maxLength(255), + Hidden::make('disk') + ->default('public'), + FileUpload::make('file') + ->label('File') + ->imageEditor() + ->imageEditorAspectRatios([ + '16:9', + '4:3', + '1:1', + ]) + ->columnSpanFull() + ->disk('public') + ->directory('media') + ->visibility('public') + ->acceptedFileTypes(['image/*', 'application/pdf']) + ->maxSize(10240) + ->required(fn ($context) => $context === 'create') + ->afterStateHydrated(function (FileUpload $component, $state, $record): void { + if (! $record) { + return; + } + + $media = $record; + + if (! $media instanceof SpatieMedia) { + return; + } + + // Construct the correct path: {media_id}/{filename} + $path = $media->id.'/'.$media->file_name; + + $component->state($path); + }), + ]); + } +} diff --git a/app/Filament/Resources/Media/Schemas/MediaInfolist.php b/app/Filament/Resources/Media/Schemas/MediaInfolist.php new file mode 100644 index 0000000..dfe4801 --- /dev/null +++ b/app/Filament/Resources/Media/Schemas/MediaInfolist.php @@ -0,0 +1,36 @@ +components([ + ImageEntry::make('file_name') + ->label('Preview') + ->getStateUsing(fn ($record) => $record->getUrl()) + ->visible(fn ($record) => $record->mime_type && str_starts_with($record->mime_type, 'image/')), + TextEntry::make('name'), + TextEntry::make('file_name'), + TextEntry::make('mime_type'), + TextEntry::make('collection_name'), + TextEntry::make('size') + ->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'), + TextEntry::make('model_type') + ->label('Attached to Model'), + TextEntry::make('model_id'), + TextEntry::make('custom_properties') + ->formatStateUsing(fn ($state) => json_encode($state, JSON_PRETTY_PRINT)), + TextEntry::make('created_at') + ->dateTime(), + TextEntry::make('updated_at') + ->dateTime(), + ]); + } +} diff --git a/app/Filament/Resources/Media/Tables/MediaTable.php b/app/Filament/Resources/Media/Tables/MediaTable.php new file mode 100644 index 0000000..dba76b0 --- /dev/null +++ b/app/Filament/Resources/Media/Tables/MediaTable.php @@ -0,0 +1,86 @@ +modifyQueryUsing(fn ($query) => $query->where('collection_name', '!=', 'avatars')) + ->columns([ + ImageColumn::make('url') + ->label('Preview') + ->getStateUsing(fn ($record) => + // Prefer the stored path produced by Filament's FileUpload (saved in custom_properties), + // fall back to Spatie's getUrl() when no stored_path exists. + ($record->getCustomProperty('stored_path')) + ? Storage::url($record->getCustomProperty('stored_path')) + : $record->getUrl() + ) + ->height(40) + ->width(40), + TextColumn::make('name') + ->searchable(), + TextColumn::make('file_name') + ->searchable(), + TextColumn::make('collection_name') + ->badge(), + TextColumn::make('mime_type'), + TextColumn::make('size') + ->formatStateUsing(fn ($state) => number_format($state / 1024, 2).' KB'), + TextColumn::make('created_at') + ->dateTime(), + ]) + ->filters([ + SelectFilter::make('collection_name') + ->options([ + 'images' => 'Images', + 'documents' => 'Documents', + ]), + ]) + ->recordActions([ + EditAction::make(), + DeleteAction::make() + ->action(function (Media $record) { + // Delete the actual stored file path if we saved one, otherwise fall back to the Spatie path. + $stored = $record->getCustomProperty('stored_path'); + if ($stored) { + Storage::disk($record->disk)->delete($stored); + } else { + Storage::disk($record->disk)->delete($record->getPath()); + } + + $record->delete(); + }), + ]) + ->toolbarActions([ + BulkActionGroup::make([ + DeleteBulkAction::make() + ->action(function (Collection $records) { + $records->each(function (Media $record) { + $stored = $record->getCustomProperty('stored_path'); + if ($stored) { + Storage::disk($record->disk)->delete($stored); + } else { + Storage::disk($record->disk)->delete($record->getPath()); + } + $record->delete(); + }); + }), + ]), + ]); + } +} diff --git a/app/Livewire/GalleryPicker.php b/app/Livewire/GalleryPicker.php new file mode 100644 index 0000000..c1da485 --- /dev/null +++ b/app/Livewire/GalleryPicker.php @@ -0,0 +1,108 @@ +entryId = $entryId; + $this->loadMediaItems(); + $this->showModal = true; + } + + public function loadMediaItems(): void + { + $this->mediaItems = Media::where('model_type', 'temp') + ->where('model_id', 0) + ->where('disk', 'public') + ->latest() + ->limit(30) + ->get(['id', 'file_name', 'name', 'disk']) + ->toArray(); + } + + public function selectMedia($mediaId): void + { + $this->selectedMediaId = $mediaId; + } + + public function copyToEntry(): void + { + if (!$this->selectedMediaId || !$this->entryId) { + $this->dispatch('notify-error', ['message' => 'Please select an image']); + return; + } + + $sourceMedia = Media::find($this->selectedMediaId); + if (!$sourceMedia) { + $this->dispatch('notify-error', ['message' => 'Media not found']); + return; + } + + try { + // Get the entry + $entry = \App\Models\Entry::find($this->entryId); + if (!$entry) { + $this->dispatch('notify-error', ['message' => 'Entry not found']); + return; + } + + // Get source file + $sourceFile = $sourceMedia->getPath(); + if (!file_exists($sourceFile)) { + $this->dispatch('notify-error', ['message' => 'Source file not found']); + return; + } + + // Create temp copy + $tempCopy = sys_get_temp_dir() . '/' . uniqid() . '_' . $sourceMedia->file_name; + copy($sourceFile, $tempCopy); + + try { + // Clear existing featured image + $entry->clearMediaCollection('featured-image'); + + // Add to entry + $newMedia = $entry->addMedia($tempCopy) + ->usingName($sourceMedia->name ?: pathinfo($sourceMedia->file_name, PATHINFO_FILENAME)) + ->usingFileName($sourceMedia->file_name) + ->toMediaCollection('featured-image', 'public'); + + // Close modal and notify + $this->showModal = false; + $this->selectedMediaId = null; + $this->dispatch('media-selected', ['mediaId' => $newMedia->id, 'fileName' => $newMedia->file_name]); + $this->dispatch('notify-success', ['message' => 'Image added to entry']); + } finally { + if (file_exists($tempCopy)) { + unlink($tempCopy); + } + } + } catch (\Exception $e) { + $this->dispatch('notify-error', ['message' => 'Error: ' . $e->getMessage()]); + } + } + + public function closePicker(): void + { + $this->showModal = false; + $this->selectedMediaId = null; + } + + public function render() + { + return view('livewire.gallery-picker'); + } +} diff --git a/app/Models/Entry.php b/app/Models/Entry.php index f9b7c1d..d0036af 100644 --- a/app/Models/Entry.php +++ b/app/Models/Entry.php @@ -2,10 +2,19 @@ namespace App\Models; +use Filament\Forms\Components\RichEditor\FileAttachmentProviders\SpatieMediaLibraryFileAttachmentProvider; +use Filament\Forms\Components\RichEditor\Models\Concerns\InteractsWithRichContent; +use Filament\Forms\Components\RichEditor\Models\Contracts\HasRichContent; use Illuminate\Database\Eloquent\Model; +use Spatie\MediaLibrary\HasMedia; +use Spatie\MediaLibrary\InteractsWithMedia; + +class Entry extends Model implements HasRichContent, HasMedia -class Entry extends Model { + + use InteractsWithMedia, InteractsWithRichContent; + protected $fillable = [ 'title', 'slug', @@ -15,4 +24,19 @@ class Entry extends Model 'published_at', 'content', ]; + + + /** + * Set up rich content configuration for media library integration + */ + public function setUpRichContent(): void + { + $this->registerRichContent('content') + ->fileAttachmentProvider( + SpatieMediaLibraryFileAttachmentProvider::make() + ->collection('content-attachments') + ->preserveFilenames() + ); + } + } diff --git a/app/Providers/Filament/AdminPanelProvider.php b/app/Providers/Filament/AdminPanelProvider.php index 373a4be..81aa43d 100644 --- a/app/Providers/Filament/AdminPanelProvider.php +++ b/app/Providers/Filament/AdminPanelProvider.php @@ -10,6 +10,8 @@ use Filament\Pages\Dashboard; use Filament\Panel; use Filament\PanelProvider; use Filament\Support\Colors\Color; +use Filament\Support\Facades\FilamentView; +use Filament\View\PanelsRenderHook; use Filament\Widgets\AccountWidget; use Filament\Widgets\FilamentInfoWidget; use Illuminate\Cookie\Middleware\AddQueuedCookiesToResponse; @@ -56,4 +58,12 @@ class AdminPanelProvider extends PanelProvider Authenticate::class, ]); } + + public function boot(): void + { + FilamentView::registerRenderHook( + PanelsRenderHook::BODY_END, + fn (): string => \Illuminate\Support\Facades\Blade::render('@vite("resources/js/app.js")'), + ); + } } diff --git a/composer.json b/composer.json index f49e35f..c67e0d1 100644 --- a/composer.json +++ b/composer.json @@ -11,7 +11,8 @@ "laravel/fortify": "^1.30", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", - "livewire/flux": "^2.9.0" + "livewire/flux": "^2.9.0", + "spatie/laravel-medialibrary": "^11.17" }, "require-dev": { "fakerphp/faker": "^1.23", diff --git a/composer.lock b/composer.lock index 1b34af6..010c9b8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "ed91eaf8381afba35eea6bfdd94e4e18", + "content-hash": "bec347dcc3a450fc682920a766cbe019", "packages": [ { "name": "anourvalar/eloquent-serialize", @@ -565,6 +565,83 @@ ], "time": "2024-07-16T11:13:48+00:00" }, + { + "name": "composer/semver", + "version": "3.4.4", + "source": { + "type": "git", + "url": "https://github.com/composer/semver.git", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/composer/semver/zipball/198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "reference": "198166618906cb2de69b95d7d47e5fa8aa1b2b95", + "shasum": "" + }, + "require": { + "php": "^5.3.2 || ^7.0 || ^8.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.11", + "symfony/phpunit-bridge": "^3 || ^7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.x-dev" + } + }, + "autoload": { + "psr-4": { + "Composer\\Semver\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nils Adermann", + "email": "naderman@naderman.de", + "homepage": "http://www.naderman.de" + }, + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be" + }, + { + "name": "Rob Bast", + "email": "rob.bast@gmail.com", + "homepage": "http://robbast.nl" + } + ], + "description": "Semver library that offers utilities, version constraint parsing and validation.", + "keywords": [ + "semantic", + "semver", + "validation", + "versioning" + ], + "support": { + "irc": "ircs://irc.libera.chat:6697/composer", + "issues": "https://github.com/composer/semver/issues", + "source": "https://github.com/composer/semver/tree/3.4.4" + }, + "funding": [ + { + "url": "https://packagist.com", + "type": "custom" + }, + { + "url": "https://github.com/composer", + "type": "github" + } + ], + "time": "2025-08-20T19:15:30+00:00" + }, { "name": "danharrin/date-format-converter", "version": "v0.3.1", @@ -1432,6 +1509,43 @@ }, "time": "2025-12-30T13:02:44+00:00" }, + { + "name": "filament/spatie-laravel-media-library-plugin", + "version": "v4.4.0", + "source": { + "type": "git", + "url": "https://github.com/filamentphp/spatie-laravel-media-library-plugin.git", + "reference": "73748df28a9c2e8c34d2c02f9314c330602e1830" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/filamentphp/spatie-laravel-media-library-plugin/zipball/73748df28a9c2e8c34d2c02f9314c330602e1830", + "reference": "73748df28a9c2e8c34d2c02f9314c330602e1830", + "shasum": "" + }, + "require": { + "filament/support": "self.version", + "php": "^8.2", + "spatie/laravel-medialibrary": "^11.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Filament\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Filament support for `spatie/laravel-medialibrary`.", + "homepage": "https://github.com/filamentphp/filament", + "support": { + "issues": "https://github.com/filamentphp/filament/issues", + "source": "https://github.com/filamentphp/filament" + }, + "time": "2025-12-09T09:54:02+00:00" + }, { "name": "filament/support", "version": "v4.4.0", @@ -3535,6 +3649,84 @@ ], "time": "2025-12-19T02:00:29+00:00" }, + { + "name": "maennchen/zipstream-php", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/maennchen/ZipStream-PHP.git", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/maennchen/ZipStream-PHP/zipball/682f1098a8fddbaf43edac2306a691c7ad508ec5", + "reference": "682f1098a8fddbaf43edac2306a691c7ad508ec5", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "ext-zlib": "*", + "php-64bit": "^8.3" + }, + "require-dev": { + "brianium/paratest": "^7.7", + "ext-zip": "*", + "friendsofphp/php-cs-fixer": "^3.86", + "guzzlehttp/guzzle": "^7.5", + "mikey179/vfsstream": "^1.6", + "php-coveralls/php-coveralls": "^2.5", + "phpunit/phpunit": "^12.0", + "vimeo/psalm": "^6.0" + }, + "suggest": { + "guzzlehttp/psr7": "^2.4", + "psr/http-message": "^2.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "ZipStream\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Paul Duncan", + "email": "pabs@pablotron.org" + }, + { + "name": "Jonatan Männchen", + "email": "jonatan@maennchen.ch" + }, + { + "name": "Jesse Donat", + "email": "donatj@gmail.com" + }, + { + "name": "András Kolesár", + "email": "kolesar@kolesar.hu" + } + ], + "description": "ZipStream is a library for dynamically streaming dynamic zip files from PHP without writing to the disk at all on the server.", + "keywords": [ + "stream", + "zip" + ], + "support": { + "issues": "https://github.com/maennchen/ZipStream-PHP/issues", + "source": "https://github.com/maennchen/ZipStream-PHP/tree/3.2.1" + }, + "funding": [ + { + "url": "https://github.com/maennchen", + "type": "github" + } + ], + "time": "2025-12-10T09:58:31+00:00" + }, { "name": "masterminds/html5", "version": "2.10.0", @@ -3604,16 +3796,16 @@ }, { "name": "monolog/monolog", - "version": "3.9.0", + "version": "3.10.0", "source": { "type": "git", "url": "https://github.com/Seldaek/monolog.git", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6" + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Seldaek/monolog/zipball/10d85740180ecba7896c87e06a166e0c95a0e3b6", - "reference": "10d85740180ecba7896c87e06a166e0c95a0e3b6", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/b321dd6749f0bf7189444158a3ce785cc16d69b0", + "reference": "b321dd6749f0bf7189444158a3ce785cc16d69b0", "shasum": "" }, "require": { @@ -3631,7 +3823,7 @@ "graylog2/gelf-php": "^1.4.2 || ^2.0", "guzzlehttp/guzzle": "^7.4.5", "guzzlehttp/psr7": "^2.2", - "mongodb/mongodb": "^1.8", + "mongodb/mongodb": "^1.8 || ^2.0", "php-amqplib/php-amqplib": "~2.4 || ^3", "php-console/php-console": "^3.1.8", "phpstan/phpstan": "^2", @@ -3691,7 +3883,7 @@ ], "support": { "issues": "https://github.com/Seldaek/monolog/issues", - "source": "https://github.com/Seldaek/monolog/tree/3.9.0" + "source": "https://github.com/Seldaek/monolog/tree/3.10.0" }, "funding": [ { @@ -3703,7 +3895,7 @@ "type": "tidelift" } ], - "time": "2025-03-24T10:02:05+00:00" + "time": "2026-01-02T08:56:05+00:00" }, { "name": "nesbot/carbon", @@ -5382,6 +5574,134 @@ ], "time": "2022-12-17T21:53:22+00:00" }, + { + "name": "spatie/image", + "version": "3.8.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/image.git", + "reference": "4d35db207c4b317bc221d02ab7ba94aa78b44c24" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/image/zipball/4d35db207c4b317bc221d02ab7ba94aa78b44c24", + "reference": "4d35db207c4b317bc221d02ab7ba94aa78b44c24", + "shasum": "" + }, + "require": { + "ext-exif": "*", + "ext-json": "*", + "ext-mbstring": "*", + "php": "^8.2", + "spatie/image-optimizer": "^1.7.5", + "spatie/temporary-directory": "^2.2", + "symfony/process": "^6.4|^7.0|^8.0" + }, + "require-dev": { + "ext-gd": "*", + "ext-imagick": "*", + "laravel/sail": "^1.34", + "pestphp/pest": "^3.0|^4.0", + "phpstan/phpstan": "^1.10.50", + "spatie/pest-plugin-snapshots": "^2.1", + "spatie/pixelmatch-php": "^1.0", + "spatie/ray": "^1.40.1", + "symfony/var-dumper": "^6.4|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\Image\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Manipulate images with an expressive API", + "homepage": "https://github.com/spatie/image", + "keywords": [ + "image", + "spatie" + ], + "support": { + "source": "https://github.com/spatie/image/tree/3.8.7" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-11-24T15:10:50+00:00" + }, + { + "name": "spatie/image-optimizer", + "version": "1.8.1", + "source": { + "type": "git", + "url": "https://github.com/spatie/image-optimizer.git", + "reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/image-optimizer/zipball/2ad9ac7c19501739183359ae64ea6c15869c23d9", + "reference": "2ad9ac7c19501739183359ae64ea6c15869c23d9", + "shasum": "" + }, + "require": { + "ext-fileinfo": "*", + "php": "^7.3|^8.0", + "psr/log": "^1.0 | ^2.0 | ^3.0", + "symfony/process": "^4.2|^5.0|^6.0|^7.0|^8.0" + }, + "require-dev": { + "pestphp/pest": "^1.21|^2.0|^3.0|^4.0", + "phpunit/phpunit": "^8.5.21|^9.4.4|^10.0|^11.0|^12.0", + "symfony/var-dumper": "^4.2|^5.0|^6.0|^7.0|^8.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\ImageOptimizer\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily optimize images using PHP", + "homepage": "https://github.com/spatie/image-optimizer", + "keywords": [ + "image-optimizer", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/image-optimizer/issues", + "source": "https://github.com/spatie/image-optimizer/tree/1.8.1" + }, + "time": "2025-11-26T10:57:19+00:00" + }, { "name": "spatie/invade", "version": "2.1.0", @@ -5441,6 +5761,116 @@ ], "time": "2024-05-17T09:06:10+00:00" }, + { + "name": "spatie/laravel-medialibrary", + "version": "11.17.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-medialibrary.git", + "reference": "237f34f70ae97523c1a99cad7176e229b8d6f0b6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-medialibrary/zipball/237f34f70ae97523c1a99cad7176e229b8d6f0b6", + "reference": "237f34f70ae97523c1a99cad7176e229b8d6f0b6", + "shasum": "" + }, + "require": { + "composer/semver": "^3.4", + "ext-exif": "*", + "ext-fileinfo": "*", + "ext-json": "*", + "illuminate/bus": "^10.2|^11.0|^12.0", + "illuminate/conditionable": "^10.2|^11.0|^12.0", + "illuminate/console": "^10.2|^11.0|^12.0", + "illuminate/database": "^10.2|^11.0|^12.0", + "illuminate/pipeline": "^10.2|^11.0|^12.0", + "illuminate/support": "^10.2|^11.0|^12.0", + "maennchen/zipstream-php": "^3.1", + "php": "^8.2", + "spatie/image": "^3.3.2", + "spatie/laravel-package-tools": "^1.16.1", + "spatie/temporary-directory": "^2.2", + "symfony/console": "^6.4.1|^7.0|^8.0" + }, + "conflict": { + "php-ffmpeg/php-ffmpeg": "<0.6.1" + }, + "require-dev": { + "aws/aws-sdk-php": "^3.293.10", + "ext-imagick": "*", + "ext-pdo_sqlite": "*", + "ext-zip": "*", + "guzzlehttp/guzzle": "^7.8.1", + "larastan/larastan": "^2.7|^3.0", + "league/flysystem-aws-s3-v3": "^3.22", + "mockery/mockery": "^1.6.7", + "orchestra/testbench": "^8.36|^9.15|^10.8", + "pestphp/pest": "^2.36|^3.0|^4.0", + "phpstan/extension-installer": "^1.3.1", + "spatie/laravel-ray": "^1.33", + "spatie/pdf-to-image": "^2.2|^3.0", + "spatie/pest-expectations": "^1.13", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "suggest": { + "league/flysystem-aws-s3-v3": "Required to use AWS S3 file storage", + "php-ffmpeg/php-ffmpeg": "Required for generating video thumbnails", + "spatie/pdf-to-image": "Required for generating thumbnails of PDFs and SVGs" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Spatie\\MediaLibrary\\MediaLibraryServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Spatie\\MediaLibrary\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Associate files with Eloquent models", + "homepage": "https://github.com/spatie/laravel-medialibrary", + "keywords": [ + "cms", + "conversion", + "downloads", + "images", + "laravel", + "laravel-medialibrary", + "media", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-medialibrary/issues", + "source": "https://github.com/spatie/laravel-medialibrary/tree/11.17.7" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-12-15T08:51:55+00:00" + }, { "name": "spatie/laravel-package-tools", "version": "1.92.7", @@ -5567,6 +5997,67 @@ ], "time": "2025-02-21T14:16:57+00:00" }, + { + "name": "spatie/temporary-directory", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/spatie/temporary-directory.git", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/temporary-directory/zipball/580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "reference": "580eddfe9a0a41a902cac6eeb8f066b42e65a32b", + "shasum": "" + }, + "require": { + "php": "^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\TemporaryDirectory\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alex Vanderbist", + "email": "alex@spatie.be", + "homepage": "https://spatie.be", + "role": "Developer" + } + ], + "description": "Easily create, use and destroy temporary directories", + "homepage": "https://github.com/spatie/temporary-directory", + "keywords": [ + "php", + "spatie", + "temporary-directory" + ], + "support": { + "issues": "https://github.com/spatie/temporary-directory/issues", + "source": "https://github.com/spatie/temporary-directory/tree/2.3.0" + }, + "funding": [ + { + "url": "https://spatie.be/open-source/support-us", + "type": "custom" + }, + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-01-13T13:04:43+00:00" + }, { "name": "symfony/clock", "version": "v7.4.0", diff --git a/config/media-library.php b/config/media-library.php new file mode 100644 index 0000000..33d558a --- /dev/null +++ b/config/media-library.php @@ -0,0 +1,303 @@ + env('MEDIA_DISK', 'public'), + + /* + * The maximum file size of an item in bytes. + * Adding a larger file will result in an exception. + */ + 'max_file_size' => 1024 * 1024 * 10, // 10MB + + /* + * This queue connection will be used to generate derived and responsive images. + * Leave empty to use the default queue connection. + */ + 'queue_connection_name' => env('QUEUE_CONNECTION', 'sync'), + + /* + * This queue will be used to generate derived and responsive images. + * Leave empty to use the default queue. + */ + 'queue_name' => env('MEDIA_QUEUE', ''), + + /* + * By default all conversions will be performed on a queue. + */ + 'queue_conversions_by_default' => env('QUEUE_CONVERSIONS_BY_DEFAULT', true), + + /* + * Should database transactions be run after database commits? + */ + 'queue_conversions_after_database_commit' => env('QUEUE_CONVERSIONS_AFTER_DB_COMMIT', true), + + /* + * The fully qualified class name of the media model. + */ + 'media_model' => Spatie\MediaLibrary\MediaCollections\Models\Media::class, + + /* + * The fully qualified class name of the media observer. + */ + 'media_observer' => Spatie\MediaLibrary\MediaCollections\Models\Observers\MediaObserver::class, + + /* + * When enabled, media collections will be serialised using the default + * laravel model serialization behaviour. + * + * Keep this option disabled if using Media Library Pro components (https://medialibrary.pro) + */ + 'use_default_collection_serialization' => false, + + /* + * The fully qualified class name of the model used for temporary uploads. + * + * This model is only used in Media Library Pro (https://medialibrary.pro) + */ + 'temporary_upload_model' => Spatie\MediaLibraryPro\Models\TemporaryUpload::class, + + /* + * When enabled, Media Library Pro will only process temporary uploads that were uploaded + * in the same session. You can opt to disable this for stateless usage of + * the pro components. + */ + 'enable_temporary_uploads_session_affinity' => true, + + /* + * When enabled, Media Library pro will generate thumbnails for uploaded file. + */ + 'generate_thumbnails_for_temporary_uploads' => true, + + /* + * This is the class that is responsible for naming generated files. + */ + 'file_namer' => Spatie\MediaLibrary\Support\FileNamer\DefaultFileNamer::class, + + /* + * The class that contains the strategy for determining a media file's path. + */ + 'path_generator' => Spatie\MediaLibrary\Support\PathGenerator\DefaultPathGenerator::class, + + /* + * The class that contains the strategy for determining how to remove files. + */ + 'file_remover_class' => Spatie\MediaLibrary\Support\FileRemover\DefaultFileRemover::class, + + /* + * Here you can specify which path generator should be used for the given class. + */ + 'custom_path_generators' => [ + // Model::class => PathGenerator::class + // or + // 'model_morph_alias' => PathGenerator::class + ], + + /* + * When urls to files get generated, this class will be called. Use the default + * if your files are stored locally above the site root or on s3. + */ + 'url_generator' => Spatie\MediaLibrary\Support\UrlGenerator\DefaultUrlGenerator::class, + + /* + * Moves media on updating to keep path consistent. Enable it only with a custom + * PathGenerator that uses, for example, the media UUID. + */ + 'moves_media_on_update' => false, + + /* + * Whether to activate versioning when urls to files get generated. + * When activated, this attaches a ?v=xx query string to the URL. + */ + 'version_urls' => false, + + /* + * The media library will try to optimize all converted images by removing + * metadata and applying a little bit of compression. These are + * the optimizers that will be used by default. + */ + 'image_optimizers' => [ + Spatie\ImageOptimizer\Optimizers\Jpegoptim::class => [ + '-m85', // set maximum quality to 85% + '--force', // ensure that progressive generation is always done also if a little bigger + '--strip-all', // this strips out all text information such as comments and EXIF data + '--all-progressive', // this will make sure the resulting image is a progressive one + ], + Spatie\ImageOptimizer\Optimizers\Pngquant::class => [ + '--force', // required parameter for this package + ], + Spatie\ImageOptimizer\Optimizers\Optipng::class => [ + '-i0', // this will result in a non-interlaced, progressive scanned image + '-o2', // this set the optimization level to two (multiple IDAT compression trials) + '-quiet', // required parameter for this package + ], + Spatie\ImageOptimizer\Optimizers\Svgo::class => [ + '--disable=cleanupIDs', // disabling because it is known to cause troubles + ], + Spatie\ImageOptimizer\Optimizers\Gifsicle::class => [ + '-b', // required parameter for this package + '-O3', // this produces the slowest but best results + ], + Spatie\ImageOptimizer\Optimizers\Cwebp::class => [ + '-m 6', // for the slowest compression method in order to get the best compression. + '-pass 10', // for maximizing the amount of analysis pass. + '-mt', // multithreading for some speed improvements. + '-q 90', // quality factor that brings the least noticeable changes. + ], + Spatie\ImageOptimizer\Optimizers\Avifenc::class => [ + '-a cq-level=23', // constant quality level, lower values mean better quality and greater file size (0-63). + '-j all', // number of jobs (worker threads, "all" uses all available cores). + '--min 0', // min quantizer for color (0-63). + '--max 63', // max quantizer for color (0-63). + '--minalpha 0', // min quantizer for alpha (0-63). + '--maxalpha 63', // max quantizer for alpha (0-63). + '-a end-usage=q', // rate control mode set to Constant Quality mode. + '-a tune=ssim', // SSIM as tune the encoder for distortion metric. + ], + ], + + /* + * These generators will be used to create an image of media files. + */ + 'image_generators' => [ + Spatie\MediaLibrary\Conversions\ImageGenerators\Image::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Webp::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Avif::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Pdf::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Svg::class, + Spatie\MediaLibrary\Conversions\ImageGenerators\Video::class, + ], + + /* + * The path where to store temporary files while performing image conversions. + * If set to null, storage_path('media-library/temp') will be used. + */ + 'temporary_directory_path' => null, + + /* + * The engine that should perform the image conversions. + * Should be either `gd` or `imagick`. + */ + 'image_driver' => env('IMAGE_DRIVER', 'gd'), + + /* + * FFMPEG & FFProbe binaries paths, only used if you try to generate video + * thumbnails and have installed the php-ffmpeg/php-ffmpeg composer + * dependency. + */ + 'ffmpeg_path' => env('FFMPEG_PATH', '/usr/bin/ffmpeg'), + 'ffprobe_path' => env('FFPROBE_PATH', '/usr/bin/ffprobe'), + + /* + * The timeout (in seconds) that will be used when generating video + * thumbnails via FFMPEG. + */ + 'ffmpeg_timeout' => env('FFMPEG_TIMEOUT', 900), + + /* + * The number of threads that FFMPEG should use. 0 means that FFMPEG + * may decide itself. + */ + 'ffmpeg_threads' => env('FFMPEG_THREADS', 0), + + /* + * Here you can override the class names of the jobs used by this package. Make sure + * your custom jobs extend the ones provided by the package. + */ + 'jobs' => [ + 'perform_conversions' => Spatie\MediaLibrary\Conversions\Jobs\PerformConversionsJob::class, + 'generate_responsive_images' => Spatie\MediaLibrary\ResponsiveImages\Jobs\GenerateResponsiveImagesJob::class, + ], + + /* + * When using the addMediaFromUrl method you may want to replace the default downloader. + * This is particularly useful when the url of the image is behind a firewall and + * need to add additional flags, possibly using curl. + */ + 'media_downloader' => Spatie\MediaLibrary\Downloaders\DefaultDownloader::class, + + /* + * When using the addMediaFromUrl method the SSL is verified by default. + * This is option disables SSL verification when downloading remote media. + * Please note that this is a security risk and should only be false in a local environment. + */ + 'media_downloader_ssl' => env('MEDIA_DOWNLOADER_SSL', true), + + /* + * The default lifetime in minutes for temporary urls. + * This is used when you call the `getLastTemporaryUrl` or `getLastTemporaryUrl` method on a media item. + */ + 'temporary_url_default_lifetime' => env('MEDIA_TEMPORARY_URL_DEFAULT_LIFETIME', 5), + + 'remote' => [ + /* + * Any extra headers that should be included when uploading media to + * a remote disk. Even though supported headers may vary between + * different drivers, a sensible default has been provided. + * + * Supported by S3: CacheControl, Expires, StorageClass, + * ServerSideEncryption, Metadata, ACL, ContentEncoding + */ + 'extra_headers' => [ + 'CacheControl' => 'max-age=604800', + ], + ], + + 'responsive_images' => [ + /* + * This class is responsible for calculating the target widths of the responsive + * images. By default we optimize for filesize and create variations that each are 30% + * smaller than the previous one. More info in the documentation. + * + * https://docs.spatie.be/laravel-medialibrary/v9/advanced-usage/generating-responsive-images + */ + 'width_calculator' => Spatie\MediaLibrary\ResponsiveImages\WidthCalculator\FileSizeOptimizedWidthCalculator::class, + + /* + * By default rendering media to a responsive image will add some javascript and a tiny placeholder. + * This ensures that the browser can already determine the correct layout. + * When disabled, no tiny placeholder is generated. + */ + 'use_tiny_placeholders' => true, + + /* + * This class will generate the tiny placeholder used for progressive image loading. By default + * the media library will use a tiny blurred jpg image. + */ + 'tiny_placeholder_generator' => Spatie\MediaLibrary\ResponsiveImages\TinyPlaceholderGenerator\Blurred::class, + ], + + /* + * When enabling this option, a route will be registered that will enable + * the Media Library Pro Vue and React components to move uploaded files + * in a S3 bucket to their right place. + */ + 'enable_vapor_uploads' => env('ENABLE_MEDIA_LIBRARY_VAPOR_UPLOADS', false), + + /* + * When converting Media instances to response the media library will add + * a `loading` attribute to the `img` tag. Here you can set the default + * value of that attribute. + * + * Possible values: 'lazy', 'eager', 'auto' or null if you don't want to set any loading instruction. + * + * More info: https://css-tricks.com/native-lazy-loading/ + */ + 'default_loading_attribute_value' => null, + + /* + * You can specify a prefix for that is used for storing all media. + * If you set this to `/my-subdir`, all your media will be stored in a `/my-subdir` directory. + */ + 'prefix' => env('MEDIA_PREFIX', ''), + + /* + * When forcing lazy loading, media will be loaded even if you don't eager load media and you have + * disabled lazy loading globally in the service provider. + */ + 'force_lazy_loading' => env('FORCE_MEDIA_LIBRARY_LAZY_LOADING', true), +]; diff --git a/database/migrations/2026_01_02_160151_create_media_table.php b/database/migrations/2026_01_02_160151_create_media_table.php new file mode 100644 index 0000000..47a4be9 --- /dev/null +++ b/database/migrations/2026_01_02_160151_create_media_table.php @@ -0,0 +1,32 @@ +id(); + + $table->morphs('model'); + $table->uuid()->nullable()->unique(); + $table->string('collection_name'); + $table->string('name'); + $table->string('file_name'); + $table->string('mime_type')->nullable(); + $table->string('disk'); + $table->string('conversions_disk')->nullable(); + $table->unsignedBigInteger('size'); + $table->json('manipulations'); + $table->json('custom_properties'); + $table->json('generated_conversions'); + $table->json('responsive_images'); + $table->unsignedInteger('order_column')->nullable()->index(); + + $table->nullableTimestamps(); + }); + } +}; diff --git a/docs/decisions/004-spatie-media-library b/docs/decisions/004-spatie-media-library new file mode 100644 index 0000000..e1dc122 --- /dev/null +++ b/docs/decisions/004-spatie-media-library @@ -0,0 +1,10 @@ +# 2026-01-02 + +```bash +php artisan storage:link +composer require "spatie/laravel-medialibrary" +composer require filament/spatie-laravel-media-library-plugin:"^4.0" -W +php artisan vendor:publish --provider="Spatie\MediaLibrary\MediaLibraryServiceProvider" --tag="medialibrary-config" +php artisan migrate +``` + diff --git a/resources/js/app.js b/resources/js/app.js index e69de29..22e1c9c 100644 --- a/resources/js/app.js +++ b/resources/js/app.js @@ -0,0 +1,48 @@ +console.log('App.js is loaded'); + +document.addEventListener('livewire:init', () => { + Livewire.on('insert-editor-content', (data) => { + console.log('Received insert-editor-content data:', data); + // Handle if data is an array + const payload = Array.isArray(data) ? data[0] : data; + const statePath = payload.statePath; + const html = payload.html; + console.log('Extracted statePath:', statePath, 'html:', html); + // 1. Find the editor by its statePath + const container = document.querySelector(`[wire\\:model="${statePath}"]`) || document.querySelector(`[statepath="${statePath}"]`); + console.log('Container found:', container); + const editorElement = container ? container.querySelector('.tiptap') : null; + console.log('Editor element found:', editorElement); + + if (editorElement && editorElement.editor) { + console.log('Inserting content:', html); + // 2. Insert the HTML (the tag) into the editor + setTimeout(() => { + editorElement.editor.chain().focus().insertContent(html).run(); + }, 500); + } else { + console.log('Editor not found or not initialized'); + // Fallback: try to find any .tiptap on the page + const anyTiptap = document.querySelector('.tiptap'); + console.log('Any tiptap found:', anyTiptap); + if (anyTiptap && anyTiptap.editor) { + console.log('Inserting to any tiptap'); + setTimeout(() => { + anyTiptap.editor.chain().focus().insertContent(html).run(); + }, 500); + } + } + }); + + Livewire.on('featured-image-added', (data) => { + console.log('Received featured-image-added event:', data); + const payload = Array.isArray(data) ? data[0] : data; + + // Reload the page to show the updated featured image + setTimeout(() => { + window.location.reload(); + }, 500); + }); +}); + +console.log('Testing if app.js is still running'); \ No newline at end of file diff --git a/resources/views/components/featured-image-display.blade.php b/resources/views/components/featured-image-display.blade.php new file mode 100644 index 0000000..437552f --- /dev/null +++ b/resources/views/components/featured-image-display.blade.php @@ -0,0 +1,32 @@ +
+ @php + $record = $this->data; + if ($record && isset($record['id'])) { + $entry = \App\Models\Entry::find($record['id']); + $media = $entry ? $entry->getMedia('featured-image') : []; + } else { + $media = []; + } + @endphp + +
+ + @if(count($media) > 0) +
+ @foreach($media as $item) +
+
+ +
+

{{ $item->name }}

+

{{ $item->file_name }}

+
+
+
+ @endforeach +
+ @else +

No featured image selected

+ @endif +
+
diff --git a/resources/views/livewire/gallery-picker.blade.php b/resources/views/livewire/gallery-picker.blade.php new file mode 100644 index 0000000..103b66b --- /dev/null +++ b/resources/views/livewire/gallery-picker.blade.php @@ -0,0 +1,72 @@ +
+ @if($showModal ?? false) +
+
+ +
+ + +
+
+
+

+ Select Image from Gallery +

+ +
+ + @if(empty($mediaItems ?? [])) +

No images in gallery

+ @else +
+ @foreach($mediaItems ?? [] as $item) + + @endforeach +
+ @endif +
+ +
+ + +
+
+
+
+ @endif +