Compare commits
2 Commits
1d41dea9fa
...
dd341ed642
| Author | SHA1 | Date | |
|---|---|---|---|
| dd341ed642 | |||
| 26551964b1 |
12
.env.example
12
.env.example
@@ -62,3 +62,15 @@ AWS_BUCKET=
|
||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||
|
||||
VITE_APP_NAME="${APP_NAME}"
|
||||
|
||||
REVERB_APP_ID=937919
|
||||
REVERB_APP_KEY=5ljogsvykrp9zocjl63r
|
||||
REVERB_APP_SECRET=urnthocgauio2cfzdlu7
|
||||
REVERB_HOST="localhost"
|
||||
REVERB_PORT=8080
|
||||
REVERB_SCHEME=http
|
||||
|
||||
VITE_REVERB_APP_KEY="${REVERB_APP_KEY}"
|
||||
VITE_REVERB_HOST="${REVERB_HOST}"
|
||||
VITE_REVERB_PORT="${REVERB_PORT}"
|
||||
VITE_REVERB_SCHEME="${REVERB_SCHEME}"
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
## About Laravel
|
||||
|
||||
Requires the php gd extensions
|
||||
|
||||
Todo:
|
||||
|
||||
Cleanup deleted Photos Job
|
||||
|
||||
Laravel is a web application framework with expressive, elegant syntax. We believe development must be an enjoyable and creative experience to be truly fulfilling. Laravel takes the pain out of development by easing common tasks used in many web projects, such as:
|
||||
|
||||
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||
|
||||
@@ -39,7 +39,7 @@ class ImageController extends Controller
|
||||
*/
|
||||
public function show(Image $image) : BinaryFileResponse
|
||||
{
|
||||
return response()->file(Storage::disk('images')->path($image->album->id . '/original/' . $image->id));
|
||||
return response()->file(Storage::disk('images')->path($image->album->id . '/thumbnail/' . $image->id . '.avif'));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
34
app/Importers/Image.php
Normal file
34
app/Importers/Image.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace App\Importers;
|
||||
|
||||
use App\ImportsMedia;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use App\Importers\Image\Jobs\{ImportImage, GenerateFullscreen, GenerateThumbnail};
|
||||
use App\Models\Image as ImageModel;
|
||||
use App\Models\Album;
|
||||
|
||||
class Image implements ImportsMedia {
|
||||
|
||||
public static function supports(UploadedFile $file): bool {
|
||||
$supportedMimes = config('gallery.image.suffixes', ['png', 'gif', 'bmp', 'svg', 'avi', 'jpg', 'jpeg']);
|
||||
return in_array($file->guessExtension(), $supportedMimes);
|
||||
}
|
||||
|
||||
public function __construct(private UploadedFile $file, private Album $location)
|
||||
{
|
||||
}
|
||||
|
||||
public function import(): array {
|
||||
$image = ImageModel::create([
|
||||
'album_id' => $this->location->id
|
||||
]);
|
||||
$imageId = $image->id;
|
||||
|
||||
return [
|
||||
new ImportImage($this->file->getPathname(), $image),
|
||||
new GenerateFullscreen($image),
|
||||
new GenerateThumbnail($image),
|
||||
];
|
||||
}
|
||||
}
|
||||
58
app/Importers/Image/Jobs/GenerateFullscreen.php
Normal file
58
app/Importers/Image/Jobs/GenerateFullscreen.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Importers\Image\Jobs;
|
||||
|
||||
use App\Models\Image;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||
use \Throwable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GenerateFullscreen implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||
|
||||
public string $source;
|
||||
public string $destination;
|
||||
|
||||
public function __construct(public Image $image)
|
||||
{
|
||||
$this->source = Storage::disk('images')->path($this->image->album_id . '/original/' . $this->image->id . '.avif');
|
||||
$this->destination = $this->image->album_id . '/lightbox/' . $this->image->id . '.avif';
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = InterventionImage::read($this->source);
|
||||
if($image->width() >= $image->height()) {
|
||||
// landscape
|
||||
$image->scaleDown(width: config('gallery.image.fullscreen.maxWidth', 2000));
|
||||
} else {
|
||||
// portrait
|
||||
$image->scaleDown(height: config('gallery.image.fullscreen.maxHeight', 2000));
|
||||
}
|
||||
|
||||
Storage::disk('images')->put($this->destination, $image->toAvif(config('gallery.image.quality', 80)));
|
||||
}
|
||||
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
$this->image->delete();
|
||||
|
||||
Log::error($exception, [
|
||||
'image' => $this->image,
|
||||
'source' => $this->source,
|
||||
'destination' => $this->destination
|
||||
]);
|
||||
}
|
||||
}
|
||||
58
app/Importers/Image/Jobs/GenerateThumbnail.php
Normal file
58
app/Importers/Image/Jobs/GenerateThumbnail.php
Normal file
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace App\Importers\Image\Jobs;
|
||||
|
||||
use App\Models\Image;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||
use \Throwable;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class GenerateThumbnail implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||
|
||||
public string $source;
|
||||
public string $destination;
|
||||
|
||||
public function __construct(public Image $image)
|
||||
{
|
||||
$this->source = Storage::disk('images')->path($this->image->album_id . '/original/' . $this->image->id . '.avif');
|
||||
$this->destination = $this->image->album_id . '/thumbnail/' . $this->image->id . '.avif';
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$image = InterventionImage::read($this->source);
|
||||
if($image->width() >= $image->height()) {
|
||||
// landscape
|
||||
$image->scaleDown(width: config('gallery.image.thumbnail.maxWidth', 150));
|
||||
} else {
|
||||
// portrait
|
||||
$image->scaleDown(height: config('gallery.image.thumbnail.maxHeight', 150));
|
||||
}
|
||||
|
||||
Storage::disk('images')->put($this->destination, $image->toAvif(config('gallery.image.quality', 80)));
|
||||
}
|
||||
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
$this->image->delete();
|
||||
|
||||
Log::error($exception, [
|
||||
'image' => $this->image,
|
||||
'source' => $this->source,
|
||||
'destination' => $this->destination
|
||||
]);
|
||||
}
|
||||
}
|
||||
40
app/Importers/Image/Jobs/ImportImage.php
Normal file
40
app/Importers/Image/Jobs/ImportImage.php
Normal file
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace App\Importers\Image\Jobs;
|
||||
|
||||
use App\Models\Image;
|
||||
use Illuminate\Bus\Batchable;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||
use \Throwable;
|
||||
|
||||
class ImportImage implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||
|
||||
public function __construct(public string $tmpFile, public Image $image)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if ($this->batch()->cancelled()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$encoded = InterventionImage::read($this->tmpFile)->toAvif(config('gallery.image.quality', 80));
|
||||
Storage::disk('images')->put("{$this->image->album->id}/original/{$this->image->id}.avif", $encoded);
|
||||
}
|
||||
|
||||
public function failed(?Throwable $exception): void
|
||||
{
|
||||
Log::error($exception, ['image' => $this->image, 'tmpFile' => $this->tmpFile]);
|
||||
$this->image->delete();
|
||||
}
|
||||
}
|
||||
13
app/ImportsMedia.php
Normal file
13
app/ImportsMedia.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
namespace App;
|
||||
|
||||
use App\Models\Album;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
|
||||
interface ImportsMedia
|
||||
{
|
||||
public static function supports(UploadedFile $file): bool;
|
||||
public function __construct(UploadedFile $file, Album $location);
|
||||
public function import(): array;
|
||||
}
|
||||
37
app/Jobs/ImportMediaJob.php
Normal file
37
app/Jobs/ImportMediaJob.php
Normal file
@@ -0,0 +1,37 @@
|
||||
<?php
|
||||
|
||||
namespace App\Jobs;
|
||||
|
||||
use App\Models\Album;
|
||||
use App\Models\Image;
|
||||
use Illuminate\Bus\Queueable;
|
||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||
use Illuminate\Foundation\Bus\Dispatchable;
|
||||
use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||
use Livewire\Features\SupportFileUploads\TemporaryUploadedFile;
|
||||
|
||||
class ImportMediaJob implements ShouldQueue
|
||||
{
|
||||
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
|
||||
|
||||
public function __construct(private TemporaryUploadedFile $file, private Album $album)
|
||||
{
|
||||
}
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if($this->isImage()) {
|
||||
$image = Image::create([
|
||||
'album' => $this->album,
|
||||
'isCover' => false
|
||||
]);
|
||||
$encoded = InterventionImage::read($this->file)->toAvif(config('gallery.original.quality', 80));
|
||||
Storage::disk('images')->put("{$$this->album->id}/original/{$image->id}.avif");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
namespace App\Livewire\Drawer\Album;
|
||||
|
||||
use App\Models\BatchMutation;
|
||||
use App\Services\MediaImporter;
|
||||
use App\Models\Album;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Attributes\On;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Component;
|
||||
use Livewire\Features\SupportFileUploads\WithFileUploads;
|
||||
|
||||
@@ -9,9 +18,37 @@ class AddImage extends Component
|
||||
{
|
||||
use WithFileUploads;
|
||||
|
||||
#[Locked]
|
||||
public Album $album;
|
||||
|
||||
#[Locked]
|
||||
public bool $processing = false;
|
||||
|
||||
#[Validate(['media.*' => 'image|max:8192'])] // max:8MB
|
||||
public $media = [];
|
||||
|
||||
public function render()
|
||||
public function save(MediaImporter $importer) : void {
|
||||
$this->validate();
|
||||
|
||||
$jobs = array_map(fn($file) => $importer->import($file, $this->album), $this->media);
|
||||
$batch = Bus::batch($jobs)
|
||||
->name('Media import in ' . $this->album->name)
|
||||
->allowFailures()
|
||||
->dispatch();
|
||||
|
||||
BatchMutation::create([
|
||||
'album_id' => $this->album->id,
|
||||
'batch_id' => $batch->id,
|
||||
]);
|
||||
|
||||
$this->redirect(route('album.show', $this->album), navigate: true);
|
||||
}
|
||||
|
||||
public function mount(Album $album) : void {
|
||||
$this->album = $album;
|
||||
}
|
||||
|
||||
public function render() : View|Factory
|
||||
{
|
||||
return view('livewire.drawer.album.add-image');
|
||||
}
|
||||
|
||||
56
app/Livewire/Drawer/Album/ProgressMonitor.php
Normal file
56
app/Livewire/Drawer/Album/ProgressMonitor.php
Normal file
@@ -0,0 +1,56 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Drawer\Album;
|
||||
|
||||
use App\Models\BatchMutation;
|
||||
use App\Models\MutationBatch;
|
||||
use Illuminate\Contracts\View\Factory;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
use Livewire\Attributes\Locked;
|
||||
use Livewire\Component;
|
||||
|
||||
class ProgressMonitor extends Component
|
||||
{
|
||||
#[Locked]
|
||||
public BatchMutation $mutation;
|
||||
|
||||
#[Locked]
|
||||
public float $progress;
|
||||
|
||||
#[Locked]
|
||||
public int $processedJobs;
|
||||
|
||||
#[Locked]
|
||||
public int $totalJobs;
|
||||
|
||||
#[Locked]
|
||||
public int $failedJobs;
|
||||
|
||||
#[Locked]
|
||||
public string $currentUrl;
|
||||
|
||||
public function mounted(BatchMutation $mutation) : void {
|
||||
$this->mutation = $mutation;
|
||||
$this->currentUrl = url()->current();
|
||||
}
|
||||
|
||||
public function monitorProgress() : void {
|
||||
$batch = Bus::findBatch($this->mutation->batch_id);
|
||||
$this->progress = $batch->progress();
|
||||
$this->totalJobs = $batch->totalJobs;
|
||||
$this->processedJobs = $batch->processedJobs();
|
||||
$this->failedJobs = $batch->failedJobs;
|
||||
|
||||
if($batch == null || $batch->finished()) {
|
||||
$this->mutation->delete();
|
||||
$this->redirect($this->currentUrl, navigate: true);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View|Factory
|
||||
{
|
||||
$this->monitorProgress();
|
||||
return view('livewire.drawer.album.progress-monitor');
|
||||
}
|
||||
}
|
||||
@@ -20,6 +20,10 @@ class Album extends Model
|
||||
return $this->hasMany(Image::class);
|
||||
}
|
||||
|
||||
public function mutations() : HasMany {
|
||||
return $this->hasMany(BatchMutation::class);
|
||||
}
|
||||
|
||||
public function media() : Collection {
|
||||
return $this->images;
|
||||
}
|
||||
|
||||
24
app/Models/BatchMutation.php
Normal file
24
app/Models/BatchMutation.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Bus\Batch;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||
use Illuminate\Support\Facades\Bus;
|
||||
|
||||
class BatchMutation extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
public $fillable = ['batch_id', 'album_id'];
|
||||
|
||||
public function album() : BelongsTo {
|
||||
return $this->belongsTo(Album::class);
|
||||
}
|
||||
|
||||
public function getBatchAttribute() : Batch {
|
||||
return Bus::findBatch($this->batchId);
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,22 @@ class Image extends Model implements HasThumbnail
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The model's default values for attributes.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attributes = [
|
||||
'isCover' => false,
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $fillable = ['album_id'];
|
||||
|
||||
public function album(): BelongsTo
|
||||
{
|
||||
return $this->belongsTo(Album::class);
|
||||
|
||||
@@ -11,7 +11,10 @@ class AppServiceProvider extends ServiceProvider
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
if ($this->app->environment('local')) {
|
||||
$this->app->register(\Laravel\Telescope\TelescopeServiceProvider::class);
|
||||
$this->app->register(TelescopeServiceProvider::class);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
36
app/Providers/HorizonServiceProvider.php
Normal file
36
app/Providers/HorizonServiceProvider.php
Normal file
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Laravel\Horizon\Horizon;
|
||||
use Laravel\Horizon\HorizonApplicationServiceProvider;
|
||||
|
||||
class HorizonServiceProvider extends HorizonApplicationServiceProvider
|
||||
{
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
parent::boot();
|
||||
|
||||
// Horizon::routeSmsNotificationsTo('15556667777');
|
||||
// Horizon::routeMailNotificationsTo('example@example.com');
|
||||
// Horizon::routeSlackNotificationsTo('slack-webhook-url', '#channel');
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the Horizon gate.
|
||||
*
|
||||
* This gate determines who can access Horizon in non-local environments.
|
||||
*/
|
||||
protected function gate(): void
|
||||
{
|
||||
Gate::define('viewHorizon', function ($user) {
|
||||
return in_array($user->email, [
|
||||
//
|
||||
]);
|
||||
});
|
||||
}
|
||||
}
|
||||
25
app/Providers/MediaImporterServiceProvider.php
Normal file
25
app/Providers/MediaImporterServiceProvider.php
Normal file
@@ -0,0 +1,25 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use App\Services\MediaImporter;
|
||||
use Illuminate\Contracts\Foundation\Application;
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class MediaImporterServiceProvider extends ServiceProvider
|
||||
{
|
||||
public function register(): void
|
||||
{
|
||||
$this->app->singleton(MediaImporter::class, function (Application $app) {
|
||||
return new MediaImporter();
|
||||
});
|
||||
}
|
||||
|
||||
public function boot(MediaImporter $service): void
|
||||
{
|
||||
$importers = config('gallery.importers');
|
||||
foreach ($importers as $importer) {
|
||||
$service->register($importer);
|
||||
}
|
||||
}
|
||||
}
|
||||
29
app/Services/MediaImporter.php
Normal file
29
app/Services/MediaImporter.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\ImportsMedia;
|
||||
use App\Models\Album;
|
||||
use Illuminate\Foundation\Bus\PendingChain;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
use Symfony\Component\HttpKernel\Exception\UnsupportedMediaTypeHttpException;
|
||||
|
||||
class MediaImporter {
|
||||
private array $importers = [];
|
||||
|
||||
public function register(string $importer) : void {
|
||||
if(in_array(ImportsMedia::class, class_implements($importer))) {
|
||||
array_push($this->importers, $importer);
|
||||
}
|
||||
}
|
||||
|
||||
public function import(UploadedFile $file, Album $location): array {
|
||||
foreach ($this->importers as $importer) {
|
||||
if($importer::supports($file)) {
|
||||
return (new $importer($file, $location))->import();
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedMediaTypeHttpException("The provided MediaType is not supported");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
channels: __DIR__.'/../routes/channels.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware) {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
return [
|
||||
App\Providers\AppServiceProvider::class,
|
||||
App\Providers\HorizonServiceProvider::class,
|
||||
App\Providers\LivewireAssetProvider::class,
|
||||
App\Providers\TelescopeServiceProvider::class,
|
||||
App\Providers\MediaImporterServiceProvider::class,
|
||||
];
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
"license": "MIT",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"intervention/image-laravel": "^1.2",
|
||||
"laravel/framework": "^11.0",
|
||||
"laravel/horizon": "^5.24",
|
||||
"laravel/reverb": "@beta",
|
||||
"laravel/telescope": "^5.0",
|
||||
"laravel/tinker": "^2.9",
|
||||
"livewire/livewire": "^3.4"
|
||||
@@ -51,7 +54,9 @@
|
||||
},
|
||||
"extra": {
|
||||
"laravel": {
|
||||
"dont-discover": []
|
||||
"dont-discover": [
|
||||
"laravel/telescope"
|
||||
]
|
||||
}
|
||||
},
|
||||
"config": {
|
||||
|
||||
1867
composer.lock
generated
1867
composer.lock
generated
File diff suppressed because it is too large
Load Diff
82
config/broadcasting.php
Normal file
82
config/broadcasting.php
Normal file
@@ -0,0 +1,82 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Broadcaster
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default broadcaster that will be used by the
|
||||
| framework when an event needs to be broadcast. You may set this to
|
||||
| any of the connections defined in the "connections" array below.
|
||||
|
|
||||
| Supported: "reverb", "pusher", "ably", "redis", "log", "null"
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('BROADCAST_CONNECTION', 'null'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Broadcast Connections
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define all of the broadcast connections that will be used
|
||||
| to broadcast events to other systems or over WebSockets. Samples of
|
||||
| each available type of connection are provided inside this array.
|
||||
|
|
||||
*/
|
||||
|
||||
'connections' => [
|
||||
|
||||
'reverb' => [
|
||||
'driver' => 'reverb',
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT', 443),
|
||||
'scheme' => env('REVERB_SCHEME', 'https'),
|
||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'client_options' => [
|
||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||
],
|
||||
],
|
||||
|
||||
'pusher' => [
|
||||
'driver' => 'pusher',
|
||||
'key' => env('PUSHER_APP_KEY'),
|
||||
'secret' => env('PUSHER_APP_SECRET'),
|
||||
'app_id' => env('PUSHER_APP_ID'),
|
||||
'options' => [
|
||||
'cluster' => env('PUSHER_APP_CLUSTER'),
|
||||
'host' => env('PUSHER_HOST') ?: 'api-'.env('PUSHER_APP_CLUSTER', 'mt1').'.pusher.com',
|
||||
'port' => env('PUSHER_PORT', 443),
|
||||
'scheme' => env('PUSHER_SCHEME', 'https'),
|
||||
'encrypted' => true,
|
||||
'useTLS' => env('PUSHER_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'client_options' => [
|
||||
// Guzzle client options: https://docs.guzzlephp.org/en/stable/request-options.html
|
||||
],
|
||||
],
|
||||
|
||||
'ably' => [
|
||||
'driver' => 'ably',
|
||||
'key' => env('ABLY_KEY'),
|
||||
],
|
||||
|
||||
'log' => [
|
||||
'driver' => 'log',
|
||||
],
|
||||
|
||||
'null' => [
|
||||
'driver' => 'null',
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -39,7 +39,7 @@ return [
|
||||
'images' => [
|
||||
'driver' => 'local',
|
||||
'root' => storage_path('app/images'),
|
||||
'throw' => false,
|
||||
'throw' => true,
|
||||
],
|
||||
|
||||
'public' => [
|
||||
|
||||
11
config/gallery.php
Normal file
11
config/gallery.php
Normal file
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
'importers' => [
|
||||
\App\Importers\Image::class,
|
||||
],
|
||||
'image' => [
|
||||
'suffixes' => ['png', 'gif', 'bmp', 'svg', 'avi', 'jpg', 'jpeg'],
|
||||
'quality' => 80,
|
||||
]
|
||||
];
|
||||
213
config/horizon.php
Normal file
213
config/horizon.php
Normal file
@@ -0,0 +1,213 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Domain
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the subdomain where Horizon will be accessible from. If this
|
||||
| setting is null, Horizon will reside under the same domain as the
|
||||
| application. Otherwise, this value will serve as the subdomain.
|
||||
|
|
||||
*/
|
||||
|
||||
'domain' => env('HORIZON_DOMAIN'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Path
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the URI path where Horizon will be accessible from. Feel free
|
||||
| to change this path to anything you like. Note that the URI will not
|
||||
| affect the paths of its internal API that aren't exposed to users.
|
||||
|
|
||||
*/
|
||||
|
||||
'path' => env('HORIZON_PATH', 'horizon'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Connection
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This is the name of the Redis connection where Horizon will store the
|
||||
| meta information required for it to function. It includes the list
|
||||
| of supervisors, failed jobs, job metrics, and other information.
|
||||
|
|
||||
*/
|
||||
|
||||
'use' => 'default',
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Redis Prefix
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This prefix will be used when storing all Horizon data in Redis. You
|
||||
| may modify the prefix when you are running multiple installations
|
||||
| of Horizon on the same server so that they don't have problems.
|
||||
|
|
||||
*/
|
||||
|
||||
'prefix' => env(
|
||||
'HORIZON_PREFIX',
|
||||
Str::slug(env('APP_NAME', 'laravel'), '_').'_horizon:'
|
||||
),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Horizon Route Middleware
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| These middleware will get attached onto each Horizon route, giving you
|
||||
| the chance to add your own middleware to this list or change any of
|
||||
| the existing middleware. Or, you can simply stick with this list.
|
||||
|
|
||||
*/
|
||||
|
||||
'middleware' => ['web'],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Wait Time Thresholds
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option allows you to configure when the LongWaitDetected event
|
||||
| will be fired. Every connection / queue combination may have its
|
||||
| own, unique threshold (in seconds) before this event is fired.
|
||||
|
|
||||
*/
|
||||
|
||||
'waits' => [
|
||||
'redis:default' => 60,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Job Trimming Times
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure for how long (in minutes) you desire Horizon to
|
||||
| persist the recent and failed jobs. Typically, recent jobs are kept
|
||||
| for one hour while all failed jobs are stored for an entire week.
|
||||
|
|
||||
*/
|
||||
|
||||
'trim' => [
|
||||
'recent' => 60,
|
||||
'pending' => 60,
|
||||
'completed' => 60,
|
||||
'recent_failed' => 10080,
|
||||
'failed' => 10080,
|
||||
'monitored' => 10080,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Silenced Jobs
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Silencing a job will instruct Horizon to not place the job in the list
|
||||
| of completed jobs within the Horizon dashboard. This setting may be
|
||||
| used to fully remove any noisy jobs from the completed jobs list.
|
||||
|
|
||||
*/
|
||||
|
||||
'silenced' => [
|
||||
// App\Jobs\ExampleJob::class,
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Metrics
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you can configure how many snapshots should be kept to display in
|
||||
| the metrics graph. This will get used in combination with Horizon's
|
||||
| `horizon:snapshot` schedule to define how long to retain metrics.
|
||||
|
|
||||
*/
|
||||
|
||||
'metrics' => [
|
||||
'trim_snapshots' => [
|
||||
'job' => 24,
|
||||
'queue' => 24,
|
||||
],
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Fast Termination
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| When this option is enabled, Horizon's "terminate" command will not
|
||||
| wait on all of the workers to terminate unless the --wait option
|
||||
| is provided. Fast termination can shorten deployment delay by
|
||||
| allowing a new instance of Horizon to start while the last
|
||||
| instance will continue to terminate each of its workers.
|
||||
|
|
||||
*/
|
||||
|
||||
'fast_termination' => false,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Memory Limit (MB)
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This value describes the maximum amount of memory the Horizon master
|
||||
| supervisor may consume before it is terminated and restarted. For
|
||||
| configuring these limits on your workers, see the next section.
|
||||
|
|
||||
*/
|
||||
|
||||
'memory_limit' => 64,
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Queue Worker Configuration
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define the queue worker settings used by your application
|
||||
| in all environments. These supervisors and settings handle all your
|
||||
| queued jobs and will be provisioned by Horizon during deployment.
|
||||
|
|
||||
*/
|
||||
|
||||
'defaults' => [
|
||||
'supervisor-1' => [
|
||||
'connection' => 'redis',
|
||||
'queue' => ['default'],
|
||||
'balance' => 'auto',
|
||||
'autoScalingStrategy' => 'time',
|
||||
'maxProcesses' => 1,
|
||||
'maxTime' => 0,
|
||||
'maxJobs' => 0,
|
||||
'memory' => 128,
|
||||
'tries' => 1,
|
||||
'timeout' => 60,
|
||||
'nice' => 0,
|
||||
],
|
||||
],
|
||||
|
||||
'environments' => [
|
||||
'production' => [
|
||||
'supervisor-1' => [
|
||||
'maxProcesses' => 10,
|
||||
'balanceMaxShift' => 1,
|
||||
'balanceCooldown' => 3,
|
||||
],
|
||||
],
|
||||
|
||||
'local' => [
|
||||
'supervisor-1' => [
|
||||
'maxProcesses' => 3,
|
||||
],
|
||||
],
|
||||
],
|
||||
];
|
||||
21
config/image.php
Normal file
21
config/image.php
Normal file
@@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Image Driver
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Intervention Image supports “GD Library” and “Imagick” to process images
|
||||
| internally. Depending on your PHP setup, you can choose one of them.
|
||||
|
|
||||
| Included options:
|
||||
| - \Intervention\Image\Drivers\Gd\Driver::class
|
||||
| - \Intervention\Image\Drivers\Imagick\Driver::class
|
||||
|
|
||||
*/
|
||||
|
||||
'driver' => \Intervention\Image\Drivers\Gd\Driver::class
|
||||
|
||||
];
|
||||
91
config/reverb.php
Normal file
91
config/reverb.php
Normal file
@@ -0,0 +1,91 @@
|
||||
<?php
|
||||
|
||||
return [
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Default Reverb Server
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| This option controls the default server used by Reverb to handle
|
||||
| incoming messages as well as broadcasting message to all your
|
||||
| connected clients. At this time only "reverb" is supported.
|
||||
|
|
||||
*/
|
||||
|
||||
'default' => env('REVERB_SERVER', 'reverb'),
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Servers
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define details for each of the supported Reverb servers.
|
||||
| Each server has its own configuration options that are defined in
|
||||
| the array below. You should ensure all the options are present.
|
||||
|
|
||||
*/
|
||||
|
||||
'servers' => [
|
||||
|
||||
'reverb' => [
|
||||
'host' => env('REVERB_SERVER_HOST', '0.0.0.0'),
|
||||
'port' => env('REVERB_SERVER_PORT', 8080),
|
||||
'hostname' => env('REVERB_HOST'),
|
||||
'options' => [
|
||||
'tls' => [],
|
||||
],
|
||||
'max_request_size' => env('REVERB_MAX_REQUEST_SIZE', 10_000),
|
||||
'scaling' => [
|
||||
'enabled' => env('REVERB_SCALING_ENABLED', false),
|
||||
'channel' => env('REVERB_SCALING_CHANNEL', 'reverb'),
|
||||
'server' => [
|
||||
'url' => env('REDIS_URL'),
|
||||
'host' => env('REDIS_HOST', '127.0.0.1'),
|
||||
'port' => env('REDIS_PORT', '6379'),
|
||||
'username' => env('REDIS_USERNAME'),
|
||||
'password' => env('REDIS_PASSWORD'),
|
||||
'database' => env('REDIS_DB', '0'),
|
||||
],
|
||||
],
|
||||
'pulse_ingest_interval' => env('REVERB_PULSE_INGEST_INTERVAL', 15),
|
||||
'telescope_ingest_interval' => env('REVERB_TELESCOPE_INGEST_INTERVAL', 15),
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
/*
|
||||
|--------------------------------------------------------------------------
|
||||
| Reverb Applications
|
||||
|--------------------------------------------------------------------------
|
||||
|
|
||||
| Here you may define how Reverb applications are managed. If you choose
|
||||
| to use the "config" provider, you may define an array of apps which
|
||||
| your server will support, including their connection credentials.
|
||||
|
|
||||
*/
|
||||
|
||||
'apps' => [
|
||||
|
||||
'provider' => 'config',
|
||||
|
||||
'apps' => [
|
||||
[
|
||||
'key' => env('REVERB_APP_KEY'),
|
||||
'secret' => env('REVERB_APP_SECRET'),
|
||||
'app_id' => env('REVERB_APP_ID'),
|
||||
'options' => [
|
||||
'host' => env('REVERB_HOST'),
|
||||
'port' => env('REVERB_PORT', 443),
|
||||
'scheme' => env('REVERB_SCHEME', 'https'),
|
||||
'useTLS' => env('REVERB_SCHEME', 'https') === 'https',
|
||||
],
|
||||
'allowed_origins' => ['*'],
|
||||
'ping_interval' => env('REVERB_APP_PING_INTERVAL', 60),
|
||||
'max_message_size' => env('REVERB_APP_MAX_MESSAGE_SIZE', 10_000),
|
||||
],
|
||||
],
|
||||
|
||||
],
|
||||
|
||||
];
|
||||
@@ -7,7 +7,9 @@ use App\Models\Image;
|
||||
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Illuminate\Support\Str;
|
||||
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||
use \App\Importers\Image\Jobs\GenerateThumbnail;
|
||||
use \App\Importers\Image\Jobs\GenerateFullscreen;
|
||||
|
||||
/**
|
||||
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Image>
|
||||
@@ -28,7 +30,11 @@ class ImageFactory extends Factory
|
||||
$height = rand(2000, 4000);
|
||||
$width = rand(2000, 4000);
|
||||
$image_content = Http::get("https://picsum.photos/{$width}/{$height}")->body();
|
||||
Storage::disk('images')->put($image->album->id . '/original/' . $image->id, $image_content);
|
||||
$encoded = InterventionImage::read($image_content)->toAvif(config('gallery.image.quality'));
|
||||
Storage::disk('images')->put($image->album->id . '/original/' . $image->id . '.avif', $encoded);
|
||||
|
||||
GenerateThumbnail::dispatch($image);
|
||||
GenerateFullscreen::dispatch($image);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -25,6 +25,6 @@ return new class extends Migration
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('categories_tags');
|
||||
Schema::dropIfExists('category_tag');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
use App\Models\Album;
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('batch_mutations', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->timestamps();
|
||||
$table->string('batch_id');
|
||||
$table->integer('files');
|
||||
$table->foreignIdFor(Album::class);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('import_batch');
|
||||
}
|
||||
};
|
||||
26
package-lock.json
generated
26
package-lock.json
generated
@@ -16,8 +16,10 @@
|
||||
"@tailwindcss/forms": "^0.5.7",
|
||||
"autoprefixer": "^10.4.19",
|
||||
"axios": "^1.6.4",
|
||||
"laravel-echo": "^1.16.1",
|
||||
"laravel-vite-plugin": "^1.0",
|
||||
"postcss": "^8.4.38",
|
||||
"pusher-js": "^8.4.0-rc2",
|
||||
"tailwindcss": "^3.4.3",
|
||||
"vite": "^5.0"
|
||||
}
|
||||
@@ -1450,6 +1452,15 @@
|
||||
"jiti": "bin/jiti.js"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-echo": {
|
||||
"version": "1.16.1",
|
||||
"resolved": "https://registry.npmjs.org/laravel-echo/-/laravel-echo-1.16.1.tgz",
|
||||
"integrity": "sha512-++Ylb6M3ariC9Rk5WE5gZjj6wcEV5kvLF8b+geJ5/rRIfdoOA+eG6b9qJPrarMD9rY28Apx+l3eelIrCc2skVg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/laravel-vite-plugin": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/laravel-vite-plugin/-/laravel-vite-plugin-1.0.2.tgz",
|
||||
@@ -1862,6 +1873,15 @@
|
||||
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/pusher-js": {
|
||||
"version": "8.4.0-rc2",
|
||||
"resolved": "https://registry.npmjs.org/pusher-js/-/pusher-js-8.4.0-rc2.tgz",
|
||||
"integrity": "sha512-d87GjOEEl9QgO5BWmViSqW0LOzPvybvX6WA9zLUstNdB57jVJuR27zHkRnrav2a3+zAMlHbP2Og8wug+rG8T+g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"tweetnacl": "^1.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/queue-microtask": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||
@@ -2236,6 +2256,12 @@
|
||||
"integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/tweetnacl": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz",
|
||||
"integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/update-browserslist-db": {
|
||||
"version": "1.0.13",
|
||||
"resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz",
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
"filepond-plugin-file-validate-size": "^2.2.8",
|
||||
"filepond-plugin-file-validate-type": "^1.2.9",
|
||||
"filepond-plugin-image-exif-orientation": "^1.0.11",
|
||||
"filepond-plugin-image-preview": "^4.6.12",
|
||||
"filepond-plugin-image-transform": "^3.8.7"
|
||||
"filepond-plugin-image-preview": "^4.6.12"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,3 +30,4 @@
|
||||
.filepond--drop-label {
|
||||
@apply text-gray-900 dark:text-white;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,13 +4,11 @@ import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orien
|
||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
||||
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
|
||||
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
||||
import FilePondPluginImageTransform from 'filepond-plugin-image-transform';
|
||||
|
||||
FilePond.registerPlugin(FilePondPluginImageExifOrientation);
|
||||
FilePond.registerPlugin(FilePondPluginImagePreview);
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
||||
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
||||
FilePond.registerPlugin(FilePondPluginImageTransform);
|
||||
|
||||
window.FilePond = FilePond;
|
||||
|
||||
@@ -18,8 +16,7 @@ document.addEventListener('alpine:init', () => {
|
||||
Alpine.store('uploader', {
|
||||
states: {},
|
||||
setState(state, value) {
|
||||
console.log(state, value);
|
||||
this.states[state] = value;
|
||||
},
|
||||
})
|
||||
});
|
||||
});
|
||||
@@ -25,24 +25,28 @@
|
||||
<h1 class="mb-4 mx-8 text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white">
|
||||
{{ $album->name }}
|
||||
</h1>
|
||||
<div class="m-8 flex flex-wrap flex-row gap-4">
|
||||
@foreach ($album->images as $image)
|
||||
<figure class="relative group rounded-lg cursor-pointer h-80 flex-grow overflow-hidden">
|
||||
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||
src="{{ $image->getThumbnail() }}" alt="{{ $album->name }} Bild">
|
||||
<div class="opacity-0 group-hover:opacity-40 absolute inset-0 w-full h-full bg-black flex items-center justify-center transition-opacity">
|
||||
<svg class="w-1/2 h-1/2 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-width="2" d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6Z"/>
|
||||
<path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</figure>
|
||||
@endforeach
|
||||
</div>
|
||||
<x-drawer name="image-add" >
|
||||
<x-slot:title>Neue Bilder zu {{ $album->name }} hinzufügen</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:drawer.album.addImage></livewire:drawer.album.addImage>
|
||||
</x-slot:content>
|
||||
</x-drawer>
|
||||
@if($album->mutations->count() > 0)
|
||||
<livewire:drawer.album.progress-monitor :mutation="$album->mutations->first()"/>
|
||||
@else
|
||||
<div class="m-8 flex flex-wrap flex-row gap-4">
|
||||
@foreach ($album->images as $image)
|
||||
<figure class="relative group rounded-lg cursor-pointer h-80 flex-grow overflow-hidden">
|
||||
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||
src="{{ $image->getThumbnail() }}" alt="{{ $album->name }} Bild">
|
||||
<div class="opacity-0 group-hover:opacity-40 absolute inset-0 w-full h-full bg-black flex items-center justify-center transition-opacity">
|
||||
<svg class="w-1/2 h-1/2 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke="currentColor" stroke-width="2" d="M21 12c0 1.2-4.03 6-9 6s-9-4.8-9-6c0-1.2 4.03-6 9-6s9 4.8 9 6Z"/>
|
||||
<path stroke="currentColor" stroke-width="2" d="M15 12a3 3 0 1 1-6 0 3 3 0 0 1 6 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
</figure>
|
||||
@endforeach
|
||||
</div>
|
||||
<x-drawer name="image-add" >
|
||||
<x-slot:title>Neue Bilder zu {{ $album->name }} hinzufügen</x-slot:title>
|
||||
<x-slot:content>
|
||||
<livewire:drawer.album.addImage :album="$album"></livewire:drawer.album.addImage>
|
||||
</x-slot:content>
|
||||
</x-drawer>
|
||||
@endif
|
||||
</x-layout>
|
||||
@@ -3,8 +3,8 @@
|
||||
x-data="{ show: false }"
|
||||
class="overflow-y-auto overflow-x-hidden fixed top-0 right-0 left-0 justify-center items-center w-full md:inset-0 h-screen max-h-full flex backdrop-blur-md bg-white/30 dark:bg-gray-800/30"
|
||||
:class="{ 'translate-y-full': !show }"
|
||||
@click="$dispatch('drawer-close-{{ $name }}')"
|
||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
||||
@click.self="$dispatch('drawer-close-{{ $name }}')"
|
||||
x-on:touchstart.self="$dispatch('drawer-close-{{ $name }}')"
|
||||
>
|
||||
<div
|
||||
id="drawer-{{ $name }}"
|
||||
@@ -16,16 +16,21 @@
|
||||
:class="{ 'translate-y-full': !show, 'xl:-bottom-full': !show }"
|
||||
>
|
||||
<div
|
||||
class="p-4 cursor-pointer max-xl:hover:bg-gray-50 max-xl:dark:hover:bg-gray-700 xl:border-b dark:border-gray-600 xl:flex xl:items-center xl:justify-between xl:p-4"
|
||||
@click="$dispatch('drawer-close-{{ $name }}')"
|
||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
||||
>
|
||||
<span class="xl:hidden absolute w-8 h-1 -translate-x-1/2 bg-gray-300 rounded-lg top-3 left-1/2 dark:bg-gray-600">
|
||||
class="p-4 cursor-pointer max-xl:hover:bg-gray-50 max-xl:dark:hover:bg-gray-700 xl:border-b dark:border-gray-600 xl:flex xl:items-center xl:justify-between xl:p-4">
|
||||
<span
|
||||
class="xl:hidden absolute w-8 h-1 -translate-x-1/2 bg-gray-300 rounded-lg top-3 left-1/2 dark:bg-gray-600"
|
||||
@click="$dispatch('drawer-close-{{ $name }}')"
|
||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
||||
>
|
||||
</span>
|
||||
<h5 class="inline-flex items-center text-base text-gray-500 dark:text-gray-400 font-medium">
|
||||
{{ $title }}
|
||||
</h5>
|
||||
<button class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white">
|
||||
<button
|
||||
class="end-2.5 text-gray-400 bg-transparent hover:bg-gray-200 hover:text-gray-900 rounded-lg text-sm w-8 h-8 ms-auto inline-flex justify-center items-center dark:hover:bg-gray-600 dark:hover:text-white"
|
||||
@click="$dispatch('drawer-close-{{ $name }}')"
|
||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
||||
>
|
||||
<svg class="max-xl:hidden w-3 h-3" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 14 14">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m1 1 6 6m0 0 6 6M7 7l6-6M7 7l-6 6"/>
|
||||
</svg>
|
||||
|
||||
@@ -68,6 +68,28 @@
|
||||
load();
|
||||
},
|
||||
},
|
||||
imageResizeTargetWidth: 600,
|
||||
imageCropAspectRatio: 1,
|
||||
imageTransformVariants: {
|
||||
thumb_medium_: (transforms) => {
|
||||
transforms.resize = {
|
||||
size: {
|
||||
width: 384,
|
||||
height: 384,
|
||||
},
|
||||
};
|
||||
return transforms;
|
||||
},
|
||||
thumb_small_: (transforms) => {
|
||||
transforms.resize = {
|
||||
size: {
|
||||
width: 128,
|
||||
height: 128,
|
||||
},
|
||||
};
|
||||
return transforms;
|
||||
},
|
||||
},
|
||||
allowImagePreview: {{ $preview ? 'true' : 'false' }},
|
||||
styleItemPanelAspectRatio: '0.5625',
|
||||
allowFileTypeValidation: {{ $validate ? 'true' : 'false' }},
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
<form class="flex h-1/3 bg-cover relative">
|
||||
<div class="relative m-auto w-1/2">
|
||||
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Suchen</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
@persist('search')
|
||||
<form class="flex h-[300px] relative overflow-hidden mb-8">
|
||||
<img class="absolute object-cover" src="/placeholder.jpg" />
|
||||
<div class="relative m-auto w-1/2">
|
||||
<label for="default-search" class="mb-2 text-sm font-medium text-gray-900 sr-only dark:text-white">Suchen</label>
|
||||
<div class="relative">
|
||||
<div class="absolute inset-y-0 start-0 flex items-center ps-3 pointer-events-none">
|
||||
<svg class="w-4 h-4 text-gray-500 dark:text-gray-400" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 20 20">
|
||||
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m19 19-4-4m0-7A7 7 0 1 1 1 8a7 7 0 0 1 14 0Z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<input type="search" id="default-search" class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Nach Alben suchen" required />
|
||||
<button type="submit" class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Suchen</button>
|
||||
</div>
|
||||
<input type="search" id="default-search" class="block w-full p-4 ps-10 text-sm text-gray-900 border border-gray-300 rounded-lg bg-gray-50 focus:ring-blue-500 focus:border-blue-500 dark:bg-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white dark:focus:ring-blue-500 dark:focus:border-blue-500" placeholder="Nach Alben suchen" required />
|
||||
<button type="submit" class="text-white absolute end-2.5 bottom-2.5 bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-4 py-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Suchen</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</form>
|
||||
@endpersist
|
||||
3
resources/views/components/icons/success.blade.php
Normal file
3
resources/views/components/icons/success.blade.php
Normal file
@@ -0,0 +1,3 @@
|
||||
<svg {{ $attributes->merge(['class' => "flex-shrink-0 text-green-500 dark:text-green-400"]) }} aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path d="M10 .5a9.5 9.5 0 1 0 9.5 9.5A9.51 9.51 0 0 0 10 .5Zm3.707 8.207-4 4a1 1 0 0 1-1.414 0l-2-2a1 1 0 0 1 1.414-1.414L9 10.586l3.293-3.293a1 1 0 0 1 1.414 1.414Z" />
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 370 B |
@@ -2,7 +2,7 @@
|
||||
<html
|
||||
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
|
||||
x-data="{ darkMode: $persist(false) }"
|
||||
:class="{'dark': darkMode }"
|
||||
:class="{'dark': darkMode}"
|
||||
x-init="
|
||||
if (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
||||
localStorage.setItem('darkMode', JSON.stringify(true));
|
||||
@@ -17,7 +17,7 @@
|
||||
@vite('resources/js/app.js')
|
||||
<title>{{ $title ?? config('app.name') }}</title>
|
||||
</head>
|
||||
<body class="bg-white dark:bg-gray-800">
|
||||
<body class="bg-white dark:bg-gray-800 min-h-screen flex flex-col">
|
||||
@persist('theme-switcher')
|
||||
<x-theme-switcher />
|
||||
@endpersist
|
||||
|
||||
7
resources/views/components/ui/spinner.blade.php
Normal file
7
resources/views/components/ui/spinner.blade.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<div role="status">
|
||||
<svg aria-hidden="true" {{ $attributes->merge(['class' => "animate-spin fill-blue-600 text-gray-200 dark:text-gray-600"]) }} viewBox="0 0 100 101" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z" fill="currentColor" />
|
||||
<path d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z" fill="currentFill" />
|
||||
</svg>
|
||||
<span class="sr-only">Loading...</span>
|
||||
</div>
|
||||
@@ -1,4 +1,9 @@
|
||||
<form wire:submit="save" class="p-4">
|
||||
<form
|
||||
wire:submit="save"
|
||||
class="p-4"
|
||||
x-data="{loading: false, message: ''}"
|
||||
x-on:import-progress-report="message = $event.detail.message"
|
||||
>
|
||||
<x-form.upload wire:model="media" name="media" label="Medien" multiple />
|
||||
|
||||
<button x-transition x-show="$store.uploader.states && $store.uploader.states.media == 3" disabled type="button" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:outline-none focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 text-center me-2 dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800 inline-flex items-center">
|
||||
@@ -17,5 +22,5 @@
|
||||
<span class="font-medium">Dateien mit Fehlern vorhanden!</span> Du kannst nicht speichern, bis die Fehler behoben sind.
|
||||
</div>
|
||||
</div>
|
||||
<button x-transition.delay.500ms x-show="$store.uploader.states && $store.uploader.states.media == 4" type="submit" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Speichern</button>
|
||||
</div>
|
||||
<button x-transition.delay.500ms x-show="$store.uploader.states && $store.uploader.states.media == 4" type="submit" @click="loading = true" class="text-white bg-blue-700 hover:bg-blue-800 focus:ring-4 focus:ring-blue-300 font-medium rounded-lg text-sm px-5 py-2.5 me-2 mb-2 dark:bg-blue-600 dark:hover:bg-blue-700 focus:outline-none dark:focus:ring-blue-800">Speichern</button>
|
||||
</form>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<div class="m-auto min-w-md text-center" wire:poll>
|
||||
<img class="inline" src="https://placehold.co/200x200" />
|
||||
<div class="p-5">
|
||||
<h5 class="mb-2 text-2xl font-bold tracking-tight text-gray-900 dark:text-white">
|
||||
Album wird verarbeitet
|
||||
</h5>
|
||||
<p class="text-start mb-3 font-normal text-gray-700 dark:text-gray-400">
|
||||
Die Dateien in diesem Album wurden geändert. <br>
|
||||
Bitte warte einen moment bis Verarbeitung abgeschlossen ist.
|
||||
</p>
|
||||
|
||||
<div class="flex justify-between mb-1">
|
||||
<span class="text-base font-medium text-blue-700 dark:text-white">Aufgabe {{ $processedJobs }} von {{ $totalJobs }}</span>
|
||||
<span class="text-sm font-medium text-blue-700 dark:text-white">{{ $progress }}%</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2.5 dark:bg-gray-700">
|
||||
<div class="bg-blue-600 h-2.5 rounded-full" style="width: {{ $progress }}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
7
routes/channels.php
Normal file
7
routes/channels.php
Normal file
@@ -0,0 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Support\Facades\Broadcast;
|
||||
|
||||
Broadcast::channel('App.Models.User.{id}', function ($user, $id) {
|
||||
return (int) $user->id === (int) $id;
|
||||
});
|
||||
@@ -1,8 +1,7 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote')->hourly();
|
||||
Schedule::command('queue:prune-batches')->daily();
|
||||
Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
||||
Reference in New Issue
Block a user