Compare commits
9 Commits
fc2b66528b
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 071eb18792 | |||
| 813874a847 | |||
| 0ce904a7d8 | |||
| 154e79aacd | |||
|
d01c7d3868
|
|||
| dd341ed642 | |||
| 26551964b1 | |||
|
1d41dea9fa
|
|||
| 3f26df05b5 |
12
.env.example
12
.env.example
@@ -62,3 +62,15 @@ AWS_BUCKET=
|
|||||||
AWS_USE_PATH_STYLE_ENDPOINT=false
|
AWS_USE_PATH_STYLE_ENDPOINT=false
|
||||||
|
|
||||||
VITE_APP_NAME="${APP_NAME}"
|
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
|
## 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:
|
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).
|
- [Simple, fast routing engine](https://laravel.com/docs/routing).
|
||||||
|
|||||||
8
app/HasThumbnail.php
Normal file
8
app/HasThumbnail.php
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App;
|
||||||
|
|
||||||
|
interface HasThumbnail
|
||||||
|
{
|
||||||
|
public function getThumbnail() : string;
|
||||||
|
}
|
||||||
119
app/Http/Controllers/ImageController.php
Normal file
119
app/Http/Controllers/ImageController.php
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Controllers;
|
||||||
|
|
||||||
|
use App\Http\Requests\StoreImageRequest;
|
||||||
|
use App\Http\Requests\UpdateImageRequest;
|
||||||
|
use App\Models\Image;
|
||||||
|
use Illuminate\Http\Response;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
class ImageController extends Controller
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Display a listing of the resource.
|
||||||
|
*/
|
||||||
|
public function index()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for creating a new resource.
|
||||||
|
*/
|
||||||
|
public function create()
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Store a newly created resource in storage.
|
||||||
|
*/
|
||||||
|
public function store(StoreImageRequest $request)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the specified resource.
|
||||||
|
*/
|
||||||
|
public function show(Image $image, string $size = 'original') : BinaryFileResponse|Response
|
||||||
|
{
|
||||||
|
$server = request()->server;
|
||||||
|
$headerEtag = md5($image->updated_at->format('U') . $image->id . '_' . $size);
|
||||||
|
$headerExpires = $image->updated_at->addYear()->toRfc2822String();
|
||||||
|
$headerLastModified = $image->updated_at->toRfc2822String();
|
||||||
|
|
||||||
|
$requestModifiedSince =
|
||||||
|
$server->has('HTTP_IF_MODIFIED_SINCE') &&
|
||||||
|
$server->get('HTTP_IF_MODIFIED_SINCE') === $headerLastModified;
|
||||||
|
|
||||||
|
$requestNoneMatch =
|
||||||
|
$server->has('HTTP_IF_NONE_MATCH') &&
|
||||||
|
$server->get('HTTP_IF_NONE_MATCH') === $headerEtag;
|
||||||
|
|
||||||
|
$headers = [
|
||||||
|
'Cache-Control' => 'max-age=0, must-revalidate, private',
|
||||||
|
'Content-Disposition' => sprintf('inline; filename="%s"', $image->id . '_' . $size),
|
||||||
|
'Etag' => $headerEtag,
|
||||||
|
'Expires' => $headerExpires,
|
||||||
|
'Last-Modified' => $headerLastModified,
|
||||||
|
'Pragma' => 'no-cache',
|
||||||
|
];
|
||||||
|
|
||||||
|
if ($requestModifiedSince || $requestNoneMatch) {
|
||||||
|
return response('', 304)->withHeaders($headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
return response()->file(Storage::disk('images')->path($image->album->id . '/' . $size . '/' . $image->id . '.avif'), $headers);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the thumbnail of the specified resource.
|
||||||
|
*/
|
||||||
|
public function thumbnail(Image $image) : BinaryFileResponse|Response
|
||||||
|
{
|
||||||
|
return $this->show($image, 'thumbnail');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the lightbox of the specified resource.
|
||||||
|
*/
|
||||||
|
public function lightbox(Image $image) : BinaryFileResponse|Response
|
||||||
|
{
|
||||||
|
return $this->show($image, 'lightbox');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display the lightbox of the specified resource.
|
||||||
|
*/
|
||||||
|
public function download(Image $image)
|
||||||
|
{
|
||||||
|
return Storage::disk('images')->download($image->album_id . '/original/' . $image->id . '.avif', name: $image->album->name . '_' . $image->id . '.avif');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show the form for editing the specified resource.
|
||||||
|
*/
|
||||||
|
public function edit(Image $image)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the specified resource in storage.
|
||||||
|
*/
|
||||||
|
public function update(UpdateImageRequest $request, Image $image)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the specified resource from storage.
|
||||||
|
*/
|
||||||
|
public function destroy(Image $image)
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/StoreImageRequest.php
Normal file
28
app/Http/Requests/StoreImageRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class StoreImageRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
28
app/Http/Requests/UpdateImageRequest.php
Normal file
28
app/Http/Requests/UpdateImageRequest.php
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Http\Requests;
|
||||||
|
|
||||||
|
use Illuminate\Foundation\Http\FormRequest;
|
||||||
|
|
||||||
|
class UpdateImageRequest extends FormRequest
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine if the user is authorized to make this request.
|
||||||
|
*/
|
||||||
|
public function authorize(): bool
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the validation rules that apply to the request.
|
||||||
|
*
|
||||||
|
* @return array<string, \Illuminate\Contracts\Validation\ValidationRule|array<mixed>|string>
|
||||||
|
*/
|
||||||
|
public function rules(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
//
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Importers/Image.php
Normal file
39
app/Importers/Image.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Importers;
|
||||||
|
|
||||||
|
use App\Importers\Image\Jobs\FinishImageModification;
|
||||||
|
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;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
|
||||||
|
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(): void {
|
||||||
|
$image = ImageModel::create([
|
||||||
|
'album_id' => $this->location->id
|
||||||
|
]);
|
||||||
|
$imageId = $image->id;
|
||||||
|
|
||||||
|
Bus::chain([
|
||||||
|
new ImportImage($this->file->getPathname(), $image),
|
||||||
|
Bus::batch([
|
||||||
|
new GenerateFullscreen($image),
|
||||||
|
new GenerateThumbnail($image),
|
||||||
|
]),
|
||||||
|
new FinishImageModification($image),
|
||||||
|
])->dispatch();
|
||||||
|
}
|
||||||
|
}
|
||||||
51
app/Importers/Image/Jobs/FinishImageModification.php
Normal file
51
app/Importers/Image/Jobs/FinishImageModification.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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 \Throwable;
|
||||||
|
use Illuminate\Support\Facades\Log;
|
||||||
|
|
||||||
|
class FinishImageModification implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||||
|
|
||||||
|
public function __construct(public Image $image)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void
|
||||||
|
{
|
||||||
|
if (method_exists($this, 'batch') && $this->batch()?->cancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->image->fill([
|
||||||
|
'isCover' => $this->getCoverFlag(),
|
||||||
|
'isProcessing' => false,
|
||||||
|
]);
|
||||||
|
$this->image->save();
|
||||||
|
|
||||||
|
// TODO: Dispatch image cleaned event
|
||||||
|
}
|
||||||
|
|
||||||
|
private function getCoverFlag() : bool {
|
||||||
|
if(!$this->image->album->hasCover && $this->image->album->images->sortBy('id')->first()->id == $this->image->id) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(?Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error($exception, [
|
||||||
|
'image' => $this->image,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
53
app/Importers/Image/Jobs/GenerateFullscreen.php
Normal file
53
app/Importers/Image/Jobs/GenerateFullscreen.php
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
<?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 (method_exists($this, 'batch') && $this->batch()?->cancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$lightbox = InterventionImage::read($this->source);
|
||||||
|
$lightbox = $lightbox->scaleDown(height: config('gallery.image.fullscreen.height', 2000));
|
||||||
|
|
||||||
|
Storage::disk('images')->put($this->destination, $lightbox->toAvif(config('gallery.image.quality', 80)));
|
||||||
|
$this->image->setLightboxSize($lightbox->width(), $lightbox->height());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(?Throwable $exception): void
|
||||||
|
{
|
||||||
|
$this->image->delete();
|
||||||
|
|
||||||
|
Log::error($exception, [
|
||||||
|
'image' => $this->image,
|
||||||
|
'source' => $this->source,
|
||||||
|
'destination' => $this->destination
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
52
app/Importers/Image/Jobs/GenerateThumbnail.php
Normal file
52
app/Importers/Image/Jobs/GenerateThumbnail.php
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
<?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 (method_exists($this, 'batch') && $this->batch()?->cancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$thumbnail = InterventionImage::read($this->source);
|
||||||
|
$thumbnail = $thumbnail->scaleDown(height: config('gallery.image.thumbnail.height', 640), width: config('gallery.image.thumbnail.width', 640));
|
||||||
|
|
||||||
|
Storage::disk('images')->put($this->destination, $thumbnail->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 (method_exists($this, 'batch') && $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();
|
||||||
|
}
|
||||||
|
}
|
||||||
49
app/Importers/Image/Jobs/RotateImage.php
Normal file
49
app/Importers/Image/Jobs/RotateImage.php
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
<?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 RotateImage implements ShouldQueue
|
||||||
|
{
|
||||||
|
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels, Batchable;
|
||||||
|
|
||||||
|
public string $source;
|
||||||
|
public string $destination;
|
||||||
|
|
||||||
|
public function __construct(public Image $image, public int $degrees)
|
||||||
|
{
|
||||||
|
$this->source = Storage::disk('images')->path($this->image->album_id . '/original/' . $this->image->id . '.avif');
|
||||||
|
$this->destination = $this->image->album_id . '/original/' . $this->image->id . '.avif';
|
||||||
|
}
|
||||||
|
|
||||||
|
public function handle(): void {
|
||||||
|
if (method_exists($this, 'batch') && $this->batch()?->cancelled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$rotated = InterventionImage::read($this->source);
|
||||||
|
$rotated = $rotated->rotate($this->degrees);
|
||||||
|
|
||||||
|
Storage::disk('images')->put($this->destination, $rotated->toAvif(config('gallery.image.quality', 80)));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function failed(?Throwable $exception): void
|
||||||
|
{
|
||||||
|
Log::error($exception, [
|
||||||
|
'image' => $this->image,
|
||||||
|
'source' => $this->source,
|
||||||
|
'destination' => $this->destination
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
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(): void;
|
||||||
|
}
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
69
app/Livewire/Album/Show.php
Normal file
69
app/Livewire/Album/Show.php
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Album;
|
||||||
|
|
||||||
|
use App\Importers\Image\Jobs\FinishImageModification;
|
||||||
|
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\Component;
|
||||||
|
use App\Models\Album;
|
||||||
|
use App\Models\Image;
|
||||||
|
use App\Importers\Image\Jobs\RotateImage;
|
||||||
|
use App\Importers\Image\Jobs\GenerateFullscreen;
|
||||||
|
use App\Importers\Image\Jobs\GenerateThumbnail;
|
||||||
|
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
|
||||||
|
|
||||||
|
class Show extends Component
|
||||||
|
{
|
||||||
|
#[Locked]
|
||||||
|
public Album $album;
|
||||||
|
|
||||||
|
#[Locked]
|
||||||
|
public EloquentCollection $images;
|
||||||
|
|
||||||
|
public function mount(Album $album): void {
|
||||||
|
$this->album = $album;
|
||||||
|
$this->images = $album->images;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('image.rotate')]
|
||||||
|
public function rotate(int $image_id, string $direction):void {
|
||||||
|
$degrees = match ($direction) {
|
||||||
|
'cw' => -90,
|
||||||
|
'ccw' => 90,
|
||||||
|
default => 0,
|
||||||
|
};
|
||||||
|
$this->dispatchRotateJob(Image::findOrFail($image_id), $degrees);
|
||||||
|
|
||||||
|
$this->redirect(route('album.show', $this->album), navigate: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[On('image.makeCover')]
|
||||||
|
public function makeCover(int $image_id):void {
|
||||||
|
Image::findOrFail($image_id)->makeCover();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function dispatchRotateJob(Image $image, int $degrees) : void {
|
||||||
|
$image->update([
|
||||||
|
'isProcessing' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
Bus::chain([
|
||||||
|
new RotateImage($image, $degrees),
|
||||||
|
Bus::batch([
|
||||||
|
new GenerateFullscreen($image),
|
||||||
|
new GenerateThumbnail($image),
|
||||||
|
]),
|
||||||
|
new FinishImageModification($image),
|
||||||
|
])->dispatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View|Factory
|
||||||
|
{
|
||||||
|
return view('livewire.album.show')
|
||||||
|
->title($this->album->name);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,15 +2,22 @@
|
|||||||
|
|
||||||
namespace App\Livewire;
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
use Livewire\Component;
|
use Livewire\Component;
|
||||||
use App\Models\Tag;
|
use App\Models\Tag;
|
||||||
use App\Models\Category;
|
use App\Models\Category;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
|
||||||
class CategoryFilter extends Component
|
class CategoryFilter extends Component
|
||||||
{
|
{
|
||||||
public ?Tag $filter = null;
|
public ?Tag $filter = null;
|
||||||
|
|
||||||
public function setFilter(int $filter) {
|
#[Computed]
|
||||||
|
public function categories() : Collection {
|
||||||
|
return $this->filter?->categories ?? Category::all();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setFilter(int $filter) : void {
|
||||||
$this->filter = Tag::find($filter);
|
$this->filter = Tag::find($filter);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -18,7 +25,6 @@ class CategoryFilter extends Component
|
|||||||
{
|
{
|
||||||
return view('livewire.category-filter', [
|
return view('livewire.category-filter', [
|
||||||
'tags' => Tag::all(),
|
'tags' => Tag::all(),
|
||||||
'categories' => Category::all(),
|
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
46
app/Livewire/Drawer/Album/AddImage.php
Normal file
46
app/Livewire/Drawer/Album/AddImage.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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\Validate;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Features\SupportFileUploads\WithFileUploads;
|
||||||
|
|
||||||
|
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 save(MediaImporter $importer) : void {
|
||||||
|
$this->validate();
|
||||||
|
|
||||||
|
foreach ($this->media as $file) {
|
||||||
|
$importer->import($file, $this->album);
|
||||||
|
}
|
||||||
|
$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');
|
||||||
|
}
|
||||||
|
}
|
||||||
39
app/Livewire/Drawer/Album/Create.php
Normal file
39
app/Livewire/Drawer/Album/Create.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Drawer\Album;
|
||||||
|
|
||||||
|
use App\Models\Category;
|
||||||
|
use Carbon\Carbon;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Attributes\Validate;
|
||||||
|
use Livewire\Attributes\Locked;
|
||||||
|
use Livewire\Attributes\Computed;
|
||||||
|
|
||||||
|
class Create extends Component
|
||||||
|
{
|
||||||
|
public Category $category;
|
||||||
|
|
||||||
|
#[Validate('required|min:3')]
|
||||||
|
public string $name;
|
||||||
|
|
||||||
|
#[Validate('required|date')]
|
||||||
|
public string $capture_date_string;
|
||||||
|
|
||||||
|
#[Computed]
|
||||||
|
public function captureDate() : Carbon {
|
||||||
|
return Carbon::parse($this->capture_date_string);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(Category $category) : void {
|
||||||
|
$this->category = $category;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function save() : void {
|
||||||
|
$this->validate();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.drawer.album.create');
|
||||||
|
}
|
||||||
|
}
|
||||||
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');
|
||||||
|
}
|
||||||
|
}
|
||||||
32
app/Livewire/Image/Grid.php
Normal file
32
app/Livewire/Image/Grid.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire\Image;
|
||||||
|
|
||||||
|
use Illuminate\Contracts\View\Factory;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\Support\Collection;
|
||||||
|
use Livewire\Component;
|
||||||
|
use Livewire\Attributes\On;
|
||||||
|
|
||||||
|
class Grid extends Component
|
||||||
|
{
|
||||||
|
public Collection $images;
|
||||||
|
|
||||||
|
#[On('image.rotate')]
|
||||||
|
public function rotate(int id, string $direction) : void {
|
||||||
|
$degree = match ($direction) {
|
||||||
|
'ccw' => -90,
|
||||||
|
'cw' => 90,
|
||||||
|
default => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function mount(Collection $images): void {
|
||||||
|
$this->images = $images;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render(): View|Factory
|
||||||
|
{
|
||||||
|
return view('livewire.image.grid');
|
||||||
|
}
|
||||||
|
}
|
||||||
19
app/Livewire/Menu.php
Normal file
19
app/Livewire/Menu.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Livewire;
|
||||||
|
|
||||||
|
use Livewire\Component;
|
||||||
|
|
||||||
|
class Menu extends Component
|
||||||
|
{
|
||||||
|
public string $title;
|
||||||
|
|
||||||
|
public function mount(?string $title) : void {
|
||||||
|
$this->title = $title ?? config('app.name');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function render()
|
||||||
|
{
|
||||||
|
return view('livewire.menu');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -20,10 +20,22 @@ class Album extends Model
|
|||||||
return $this->hasMany(Image::class);
|
return $this->hasMany(Image::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function mutations() : HasMany {
|
||||||
|
return $this->hasMany(BatchMutation::class);
|
||||||
|
}
|
||||||
|
|
||||||
public function media() : Collection {
|
public function media() : Collection {
|
||||||
return $this->images;
|
return $this->images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getHasCoverAttribute() : bool {
|
||||||
|
return $this->images()->where('isCover', 1)->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHasProcessingMediaAttribute() : bool {
|
||||||
|
return $this->images()->where('isProcessing', 1)->count() > 0;
|
||||||
|
}
|
||||||
|
|
||||||
public function getThumbnailAttribute() : ?string {
|
public function getThumbnailAttribute() : ?string {
|
||||||
return $this->images()->where('isCover', 1)->first()?->getThumbnail();
|
return $this->images()->where('isCover', 1)->first()?->getThumbnail();
|
||||||
}
|
}
|
||||||
|
|||||||
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
65
app/Models/Image.php
Normal file
65
app/Models/Image.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Models;
|
||||||
|
|
||||||
|
use App\HasThumbnail;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||||
|
use Illuminate\Database\Eloquent\Model;
|
||||||
|
use Illuminate\Database\Eloquent\Relations\BelongsTo;
|
||||||
|
|
||||||
|
class Image extends Model implements HasThumbnail
|
||||||
|
{
|
||||||
|
use HasFactory;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The model's default values for attributes.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $attributes = [
|
||||||
|
'isCover' => false,
|
||||||
|
'isProcessing' => true,
|
||||||
|
'lightboxWidth' => 0,
|
||||||
|
'lightboxHeight' => 0,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The attributes that are mass assignable.
|
||||||
|
*
|
||||||
|
* @var array
|
||||||
|
*/
|
||||||
|
protected $fillable = ['album_id', 'lightboxWidth', 'lightboxHeight', 'isCover', 'isProcessing'];
|
||||||
|
|
||||||
|
public function album(): BelongsTo
|
||||||
|
{
|
||||||
|
return $this->belongsTo(Album::class);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getThumbnail() : string {
|
||||||
|
return route('image.thumbnail', $this) . '?cacheBuster3000=' . $this->updated_at->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDownload() : string {
|
||||||
|
return route('image.download', $this) . '?cacheBuster3000=' . $this->updated_at->timestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setLightboxSize(int $width, int $height) : void {
|
||||||
|
$this->lightboxWidth = $width;
|
||||||
|
$this->lightboxHeight = $height;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function makeCover() : void {
|
||||||
|
Image::where('isCover', 1)->where('album_id', $this->album_id)->update(['isCover' => 0]);
|
||||||
|
$this->isCover = true;
|
||||||
|
$this->save();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getLightboxAttribute() : array {
|
||||||
|
return [
|
||||||
|
'location' => route('image.lightbox', $this) . '?cacheBuster3000=' . $this->updated_at->timestamp,
|
||||||
|
'width' => $this->lightboxWidth,
|
||||||
|
'height' => $this->lightboxHeight,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
66
app/Policies/ImagePolicy.php
Normal file
66
app/Policies/ImagePolicy.php
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Policies;
|
||||||
|
|
||||||
|
use App\Models\Image;
|
||||||
|
use App\Models\User;
|
||||||
|
use Illuminate\Auth\Access\Response;
|
||||||
|
|
||||||
|
class ImagePolicy
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view any models.
|
||||||
|
*/
|
||||||
|
public function viewAny(User $user): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can view the model.
|
||||||
|
*/
|
||||||
|
public function view(User $user, Image $image): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can create models.
|
||||||
|
*/
|
||||||
|
public function create(User $user): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can update the model.
|
||||||
|
*/
|
||||||
|
public function update(User $user, Image $image): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can delete the model.
|
||||||
|
*/
|
||||||
|
public function delete(User $user, Image $image): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can restore the model.
|
||||||
|
*/
|
||||||
|
public function restore(User $user, Image $image): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine whether the user can permanently delete the model.
|
||||||
|
*/
|
||||||
|
public function forceDelete(User $user, Image $image): bool
|
||||||
|
{
|
||||||
|
//
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -11,7 +11,10 @@ class AppServiceProvider extends ServiceProvider
|
|||||||
*/
|
*/
|
||||||
public function register(): void
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
64
app/Providers/TelescopeServiceProvider.php
Normal file
64
app/Providers/TelescopeServiceProvider.php
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Providers;
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\Gate;
|
||||||
|
use Laravel\Telescope\IncomingEntry;
|
||||||
|
use Laravel\Telescope\Telescope;
|
||||||
|
use Laravel\Telescope\TelescopeApplicationServiceProvider;
|
||||||
|
|
||||||
|
class TelescopeServiceProvider extends TelescopeApplicationServiceProvider
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Register any application services.
|
||||||
|
*/
|
||||||
|
public function register(): void
|
||||||
|
{
|
||||||
|
// Telescope::night();
|
||||||
|
|
||||||
|
$this->hideSensitiveRequestDetails();
|
||||||
|
|
||||||
|
$isLocal = $this->app->environment('local');
|
||||||
|
|
||||||
|
Telescope::filter(function (IncomingEntry $entry) use ($isLocal) {
|
||||||
|
return $isLocal ||
|
||||||
|
$entry->isReportableException() ||
|
||||||
|
$entry->isFailedRequest() ||
|
||||||
|
$entry->isFailedJob() ||
|
||||||
|
$entry->isScheduledTask() ||
|
||||||
|
$entry->hasMonitoredTag();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent sensitive request details from being logged by Telescope.
|
||||||
|
*/
|
||||||
|
protected function hideSensitiveRequestDetails(): void
|
||||||
|
{
|
||||||
|
if ($this->app->environment('local')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Telescope::hideRequestParameters(['_token']);
|
||||||
|
|
||||||
|
Telescope::hideRequestHeaders([
|
||||||
|
'cookie',
|
||||||
|
'x-csrf-token',
|
||||||
|
'x-xsrf-token',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register the Telescope gate.
|
||||||
|
*
|
||||||
|
* This gate determines who can access Telescope in non-local environments.
|
||||||
|
*/
|
||||||
|
protected function gate(): void
|
||||||
|
{
|
||||||
|
Gate::define('viewTelescope', function ($user) {
|
||||||
|
return in_array($user->email, [
|
||||||
|
//
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
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\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): void {
|
||||||
|
foreach ($this->importers as $importer) {
|
||||||
|
if($importer::supports($file)) {
|
||||||
|
(new $importer($file, $location))->import();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new UnsupportedMediaTypeHttpException("The provided MediaType is not supported");
|
||||||
|
}
|
||||||
|
}
|
||||||
74
app/View/Components/Form/Upload.php
Normal file
74
app/View/Components/Form/Upload.php
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\View\Components\Form;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Illuminate\Contracts\View\View;
|
||||||
|
use Illuminate\View\Component;
|
||||||
|
|
||||||
|
class Upload extends Component
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Create a new component instance.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
public string $name = 'file',
|
||||||
|
public bool|int $multiple = false,
|
||||||
|
public bool|int $validate = true,
|
||||||
|
public bool|int $preview = true,
|
||||||
|
public bool|int $required = false,
|
||||||
|
public bool|int $disabled = false,
|
||||||
|
public array|string $accept = ['image/png', 'image/jpeg', 'image/webp', 'image/avif'],
|
||||||
|
public string $size = '8MB',
|
||||||
|
public int $number = 10,
|
||||||
|
public string $label = '',
|
||||||
|
public string $sizeHuman = '',
|
||||||
|
public array|string $acceptHuman = [],
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the view / contents that represent the component.
|
||||||
|
*/
|
||||||
|
public function render(): View|Closure|string
|
||||||
|
{
|
||||||
|
// Set boolean values
|
||||||
|
if (! $this->multiple) {
|
||||||
|
$this->multiple = 0;
|
||||||
|
}
|
||||||
|
if (! $this->validate) {
|
||||||
|
$this->validate = 0;
|
||||||
|
}
|
||||||
|
if (! $this->preview) {
|
||||||
|
$this->preview = 0;
|
||||||
|
}
|
||||||
|
if (! $this->required) {
|
||||||
|
$this->required = 0;
|
||||||
|
}
|
||||||
|
if (! $this->disabled) {
|
||||||
|
$this->disabled = 0;
|
||||||
|
}
|
||||||
|
// Prepare accept files to JSON
|
||||||
|
if (is_string($this->accept)) {
|
||||||
|
$this->accept = explode(',', $this->accept);
|
||||||
|
}
|
||||||
|
$this->accept = array_map('trim', $this->accept);
|
||||||
|
$this->accept = array_filter($this->accept);
|
||||||
|
$this->accept = array_unique($this->accept);
|
||||||
|
$this->accept = array_values($this->accept);
|
||||||
|
$this->accept = array_map('strtolower', $this->accept);
|
||||||
|
$fileTypes = $this->accept;
|
||||||
|
$this->accept = json_encode($this->accept);
|
||||||
|
// Set size human for UI
|
||||||
|
$this->sizeHuman = $this->size;
|
||||||
|
// Prepare files types for UI
|
||||||
|
foreach ($fileTypes as $type) {
|
||||||
|
$new = explode('/', $type);
|
||||||
|
if (array_key_exists(1, $new)) {
|
||||||
|
$this->acceptHuman[] = ".{$new[1]}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$this->acceptHuman = implode(', ', $this->acceptHuman);
|
||||||
|
return view('components.form.upload');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,6 +8,7 @@ return Application::configure(basePath: dirname(__DIR__))
|
|||||||
->withRouting(
|
->withRouting(
|
||||||
web: __DIR__.'/../routes/web.php',
|
web: __DIR__.'/../routes/web.php',
|
||||||
commands: __DIR__.'/../routes/console.php',
|
commands: __DIR__.'/../routes/console.php',
|
||||||
|
channels: __DIR__.'/../routes/channels.php',
|
||||||
health: '/up',
|
health: '/up',
|
||||||
)
|
)
|
||||||
->withMiddleware(function (Middleware $middleware) {
|
->withMiddleware(function (Middleware $middleware) {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
return [
|
return [
|
||||||
App\Providers\AppServiceProvider::class,
|
App\Providers\AppServiceProvider::class,
|
||||||
|
App\Providers\HorizonServiceProvider::class,
|
||||||
App\Providers\LivewireAssetProvider::class,
|
App\Providers\LivewireAssetProvider::class,
|
||||||
App\Providers\TelescopeServiceProvider::class,
|
App\Providers\MediaImporterServiceProvider::class,
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -6,7 +6,10 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
|
"intervention/image-laravel": "^1.2",
|
||||||
"laravel/framework": "^11.0",
|
"laravel/framework": "^11.0",
|
||||||
|
"laravel/horizon": "^5.24",
|
||||||
|
"laravel/reverb": "@beta",
|
||||||
"laravel/telescope": "^5.0",
|
"laravel/telescope": "^5.0",
|
||||||
"laravel/tinker": "^2.9",
|
"laravel/tinker": "^2.9",
|
||||||
"livewire/livewire": "^3.4"
|
"livewire/livewire": "^3.4"
|
||||||
@@ -51,7 +54,9 @@
|
|||||||
},
|
},
|
||||||
"extra": {
|
"extra": {
|
||||||
"laravel": {
|
"laravel": {
|
||||||
"dont-discover": []
|
"dont-discover": [
|
||||||
|
"laravel/telescope"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"config": {
|
"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' => [
|
'images' => [
|
||||||
'driver' => 'local',
|
'driver' => 'local',
|
||||||
'root' => storage_path('app/images'),
|
'root' => storage_path('app/images'),
|
||||||
'throw' => false,
|
'throw' => true,
|
||||||
],
|
],
|
||||||
|
|
||||||
'public' => [
|
'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
|
||||||
|
|
||||||
|
];
|
||||||
159
config/livewire.php
Normal file
159
config/livewire.php
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Class Namespace
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value sets the root class namespace for Livewire component classes in
|
||||||
|
| your application. This value will change where component auto-discovery
|
||||||
|
| finds components. It's also referenced by the file creation commands.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'class_namespace' => 'App\\Livewire',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| View Path
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value is used to specify where Livewire component Blade templates are
|
||||||
|
| stored when running file creation commands like `artisan make:livewire`.
|
||||||
|
| It is also used if you choose to omit a component's render() method.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'view_path' => resource_path('views/livewire'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Layout
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| The view that will be used as the layout when rendering a single component
|
||||||
|
| as an entire page via `Route::get('/post/create', CreatePost::class);`.
|
||||||
|
| In this case, the view returned by CreatePost will render into $slot.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'layout' => 'components.layouts.app',
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Lazy Loading Placeholder
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Livewire allows you to lazy load components that would otherwise slow down
|
||||||
|
| the initial page load. Every component can have a custom placeholder or
|
||||||
|
| you can define the default placeholder view for all components below.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'lazy_placeholder' => null,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Temporary File Uploads
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Livewire handles file uploads by storing uploads in a temporary directory
|
||||||
|
| before the file is stored permanently. All file uploads are directed to
|
||||||
|
| a global endpoint for temporary storage. You may configure this below:
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'temporary_file_upload' => [
|
||||||
|
'disk' => null, // Example: 'local', 's3' | Default: 'default'
|
||||||
|
'rules' => null, // Example: ['file', 'mimes:png,jpg'] | Default: ['required', 'file', 'max:12288'] (12MB)
|
||||||
|
'directory' => null, // Example: 'tmp' | Default: 'livewire-tmp'
|
||||||
|
'middleware' => null, // Example: 'throttle:5,1' | Default: 'throttle:60,1'
|
||||||
|
'preview_mimes' => [ // Supported file types for temporary pre-signed file URLs...
|
||||||
|
'png', 'gif', 'bmp', 'svg', 'wav', 'mp4',
|
||||||
|
'mov', 'avi', 'wmv', 'mp3', 'm4a',
|
||||||
|
'jpg', 'jpeg', 'mpga', 'webp', 'wma',
|
||||||
|
],
|
||||||
|
'max_upload_time' => 5, // Max duration (in minutes) before an upload is invalidated...
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Render On Redirect
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This value determines if Livewire will run a component's `render()` method
|
||||||
|
| after a redirect has been triggered using something like `redirect(...)`
|
||||||
|
| Setting this to true will render the view once more before redirecting
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'render_on_redirect' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Eloquent Model Binding
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Previous versions of Livewire supported binding directly to eloquent model
|
||||||
|
| properties using wire:model by default. However, this behavior has been
|
||||||
|
| deemed too "magical" and has therefore been put under a feature flag.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'legacy_model_binding' => false,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Auto-inject Frontend Assets
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By default, Livewire automatically injects its JavaScript and CSS into the
|
||||||
|
| <head> and <body> of pages containing Livewire components. By disabling
|
||||||
|
| this behavior, you need to use @livewireStyles and @livewireScripts.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'inject_assets' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Navigate (SPA mode)
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| By adding `wire:navigate` to links in your Livewire application, Livewire
|
||||||
|
| will prevent the default link handling and instead request those pages
|
||||||
|
| via AJAX, creating an SPA-like effect. Configure this behavior here.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'navigate' => [
|
||||||
|
'show_progress_bar' => true,
|
||||||
|
'progress_bar_color' => '#2299dd',
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| HTML Morph Markers
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| Livewire intelligently "morphs" existing HTML into the newly rendered HTML
|
||||||
|
| after each update. To make this process more reliable, Livewire injects
|
||||||
|
| "markers" into the rendered Blade surrounding @if, @class & @foreach.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'inject_morph_markers' => true,
|
||||||
|
|
||||||
|
/*
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
| Pagination Theme
|
||||||
|
|---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| When enabling Livewire's pagination feature by using the `WithPagination`
|
||||||
|
| trait, Livewire will use Tailwind templates to render pagination views
|
||||||
|
| on the page. If you want Bootstrap CSS, you can specify: "bootstrap"
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'pagination_theme' => 'tailwind',
|
||||||
|
];
|
||||||
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),
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
],
|
||||||
|
|
||||||
|
];
|
||||||
205
config/telescope.php
Normal file
205
config/telescope.php
Normal file
@@ -0,0 +1,205 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Laravel\Telescope\Http\Middleware\Authorize;
|
||||||
|
use Laravel\Telescope\Watchers;
|
||||||
|
|
||||||
|
return [
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Master Switch
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This option may be used to disable all Telescope watchers regardless
|
||||||
|
| of their individual configuration, which simply provides a single
|
||||||
|
| and convenient way to enable or disable Telescope data storage.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'enabled' => env('TELESCOPE_ENABLED', true),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Domain
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the subdomain where Telescope will be accessible from. If the
|
||||||
|
| setting is null, Telescope will reside under the same domain as the
|
||||||
|
| application. Otherwise, this value will be used as the subdomain.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'domain' => env('TELESCOPE_DOMAIN'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Path
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This is the URI path where Telescope 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('TELESCOPE_PATH', 'telescope'),
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Storage Driver
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration options determines the storage driver that will
|
||||||
|
| be used to store Telescope's data. In addition, you may set any
|
||||||
|
| custom options as needed by the particular driver you choose.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'driver' => env('TELESCOPE_DRIVER', 'database'),
|
||||||
|
|
||||||
|
'storage' => [
|
||||||
|
'database' => [
|
||||||
|
'connection' => env('DB_CONNECTION', 'mysql'),
|
||||||
|
'chunk' => 1000,
|
||||||
|
],
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Queue
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| This configuration options determines the queue connection and queue
|
||||||
|
| which will be used to process ProcessPendingUpdate jobs. This can
|
||||||
|
| be changed if you would prefer to use a non-default connection.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'queue' => [
|
||||||
|
'connection' => env('TELESCOPE_QUEUE_CONNECTION', null),
|
||||||
|
'queue' => env('TELESCOPE_QUEUE', null),
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Route Middleware
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| These middleware will be assigned to every Telescope 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',
|
||||||
|
Authorize::class,
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Allowed / Ignored Paths & Commands
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following array lists the URI paths and Artisan commands that will
|
||||||
|
| not be watched by Telescope. In addition to this list, some Laravel
|
||||||
|
| commands, like migrations and queue commands, are always ignored.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'only_paths' => [
|
||||||
|
// 'api/*'
|
||||||
|
],
|
||||||
|
|
||||||
|
'ignore_paths' => [
|
||||||
|
'livewire*',
|
||||||
|
'nova-api*',
|
||||||
|
'pulse*',
|
||||||
|
],
|
||||||
|
|
||||||
|
'ignore_commands' => [
|
||||||
|
//
|
||||||
|
],
|
||||||
|
|
||||||
|
/*
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
| Telescope Watchers
|
||||||
|
|--------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
| The following array lists the "watchers" that will be registered with
|
||||||
|
| Telescope. The watchers gather the application's profile data when
|
||||||
|
| a request or task is executed. Feel free to customize this list.
|
||||||
|
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
'watchers' => [
|
||||||
|
Watchers\BatchWatcher::class => env('TELESCOPE_BATCH_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\CacheWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_CACHE_WATCHER', true),
|
||||||
|
'hidden' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\ClientRequestWatcher::class => env('TELESCOPE_CLIENT_REQUEST_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\CommandWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_COMMAND_WATCHER', true),
|
||||||
|
'ignore' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\DumpWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_DUMP_WATCHER', true),
|
||||||
|
'always' => env('TELESCOPE_DUMP_WATCHER_ALWAYS', false),
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\EventWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_EVENT_WATCHER', true),
|
||||||
|
'ignore' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\ExceptionWatcher::class => env('TELESCOPE_EXCEPTION_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\GateWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_GATE_WATCHER', true),
|
||||||
|
'ignore_abilities' => [],
|
||||||
|
'ignore_packages' => true,
|
||||||
|
'ignore_paths' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\JobWatcher::class => env('TELESCOPE_JOB_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\LogWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_LOG_WATCHER', true),
|
||||||
|
'level' => 'error',
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\MailWatcher::class => env('TELESCOPE_MAIL_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\ModelWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_MODEL_WATCHER', true),
|
||||||
|
'events' => ['eloquent.*'],
|
||||||
|
'hydrations' => true,
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\NotificationWatcher::class => env('TELESCOPE_NOTIFICATION_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\QueryWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_QUERY_WATCHER', true),
|
||||||
|
'ignore_packages' => true,
|
||||||
|
'ignore_paths' => [],
|
||||||
|
'slow' => 100,
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\RedisWatcher::class => env('TELESCOPE_REDIS_WATCHER', true),
|
||||||
|
|
||||||
|
Watchers\RequestWatcher::class => [
|
||||||
|
'enabled' => env('TELESCOPE_REQUEST_WATCHER', true),
|
||||||
|
'size_limit' => env('TELESCOPE_RESPONSE_SIZE_LIMIT', 64),
|
||||||
|
'ignore_http_methods' => [],
|
||||||
|
'ignore_status_codes' => [],
|
||||||
|
],
|
||||||
|
|
||||||
|
Watchers\ScheduleWatcher::class => env('TELESCOPE_SCHEDULE_WATCHER', true),
|
||||||
|
Watchers\ViewWatcher::class => env('TELESCOPE_VIEW_WATCHER', true),
|
||||||
|
],
|
||||||
|
];
|
||||||
56
database/factories/ImageFactory.php
Normal file
56
database/factories/ImageFactory.php
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Factories;
|
||||||
|
|
||||||
|
use App\Importers\Image\Jobs\FinishImageModification;
|
||||||
|
use App\Models\Album;
|
||||||
|
use App\Models\Image;
|
||||||
|
use Illuminate\Database\Eloquent\Factories\Factory;
|
||||||
|
use Illuminate\Support\Facades\Http;
|
||||||
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Intervention\Image\Laravel\Facades\Image as InterventionImage;
|
||||||
|
use \App\Importers\Image\Jobs\GenerateThumbnail;
|
||||||
|
use \App\Importers\Image\Jobs\GenerateFullscreen;
|
||||||
|
use Illuminate\Support\Facades\Bus;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Image>
|
||||||
|
*/
|
||||||
|
class ImageFactory extends Factory
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Configure the model factory.
|
||||||
|
*/
|
||||||
|
public function configure(): static
|
||||||
|
{
|
||||||
|
return $this->afterCreating(function (Image $image) {
|
||||||
|
$height = rand(2000, 4000);
|
||||||
|
$width = rand(2000, 4000);
|
||||||
|
$image_content = Http::get("https://picsum.photos/{$width}/{$height}")->body();
|
||||||
|
$encoded = InterventionImage::read($image_content)->toAvif(config('gallery.image.quality'));
|
||||||
|
$image_path = $image->album_id . '/original/' . $image->id . '.avif';
|
||||||
|
Storage::disk('images')->put($image_path, $encoded);
|
||||||
|
|
||||||
|
Bus::chain([
|
||||||
|
Bus::batch([
|
||||||
|
new GenerateThumbnail($image),
|
||||||
|
new GenerateFullscreen($image),
|
||||||
|
]),
|
||||||
|
new FinishImageModification($image),
|
||||||
|
])->dispatch();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Define the model's default state.
|
||||||
|
*
|
||||||
|
* @return array<string, mixed>
|
||||||
|
*/
|
||||||
|
public function definition(): array
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
'album_id' => Album::all()->random(1)->first()->id,
|
||||||
|
'isCover' => false,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,6 +25,6 @@ return new class extends Migration
|
|||||||
*/
|
*/
|
||||||
public function down(): void
|
public function down(): void
|
||||||
{
|
{
|
||||||
Schema::dropIfExists('categories_tags');
|
Schema::dropIfExists('category_tag');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Illuminate\Database\Migrations\Migration;
|
||||||
|
use Illuminate\Database\Schema\Blueprint;
|
||||||
|
use Illuminate\Support\Facades\Schema;
|
||||||
|
|
||||||
|
return new class extends Migration
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Get the migration connection name.
|
||||||
|
*/
|
||||||
|
public function getConnection(): ?string
|
||||||
|
{
|
||||||
|
return config('telescope.storage.database.connection');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run the migrations.
|
||||||
|
*/
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
$schema = Schema::connection($this->getConnection());
|
||||||
|
|
||||||
|
$schema->create('telescope_entries', function (Blueprint $table) {
|
||||||
|
$table->bigIncrements('sequence');
|
||||||
|
$table->uuid('uuid');
|
||||||
|
$table->uuid('batch_id');
|
||||||
|
$table->string('family_hash')->nullable();
|
||||||
|
$table->boolean('should_display_on_index')->default(true);
|
||||||
|
$table->string('type', 20);
|
||||||
|
$table->longText('content');
|
||||||
|
$table->dateTime('created_at')->nullable();
|
||||||
|
|
||||||
|
$table->unique('uuid');
|
||||||
|
$table->index('batch_id');
|
||||||
|
$table->index('family_hash');
|
||||||
|
$table->index('created_at');
|
||||||
|
$table->index(['type', 'should_display_on_index']);
|
||||||
|
});
|
||||||
|
|
||||||
|
$schema->create('telescope_entries_tags', function (Blueprint $table) {
|
||||||
|
$table->uuid('entry_uuid');
|
||||||
|
$table->string('tag');
|
||||||
|
|
||||||
|
$table->primary(['entry_uuid', 'tag']);
|
||||||
|
$table->index('tag');
|
||||||
|
|
||||||
|
$table->foreign('entry_uuid')
|
||||||
|
->references('uuid')
|
||||||
|
->on('telescope_entries')
|
||||||
|
->onDelete('cascade');
|
||||||
|
});
|
||||||
|
|
||||||
|
$schema->create('telescope_monitoring', function (Blueprint $table) {
|
||||||
|
$table->string('tag')->primary();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
$schema = Schema::connection($this->getConnection());
|
||||||
|
|
||||||
|
$schema->dropIfExists('telescope_entries_tags');
|
||||||
|
$schema->dropIfExists('telescope_entries');
|
||||||
|
$schema->dropIfExists('telescope_monitoring');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
<?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('images', function (Blueprint $table) {
|
||||||
|
$table->id();
|
||||||
|
$table->timestamps();
|
||||||
|
$table->boolean('isCover');
|
||||||
|
$table->boolean('isProcessing');
|
||||||
|
$table->bigInteger(column: 'lightboxWidth', unsigned: true)->default(0);
|
||||||
|
$table->bigInteger(column: 'lightboxHeight', unsigned: true)->default(0);
|
||||||
|
$table->foreignIdFor(Album::class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::dropIfExists('images');
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
};
|
||||||
19
database/seeders/ImageSeeder.php
Normal file
19
database/seeders/ImageSeeder.php
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Database\Seeders;
|
||||||
|
|
||||||
|
use App\Models\Image;
|
||||||
|
use Illuminate\Database\Seeder;
|
||||||
|
|
||||||
|
class ImageSeeder extends Seeder
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Run the database seeds.
|
||||||
|
*/
|
||||||
|
public function run(): void
|
||||||
|
{
|
||||||
|
Image::factory()
|
||||||
|
->count(100)
|
||||||
|
->create();
|
||||||
|
}
|
||||||
|
}
|
||||||
53
package-lock.json
generated
53
package-lock.json
generated
@@ -5,13 +5,13 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@marcreichel/alpine-auto-animate": "^1.1.0",
|
||||||
"filepond": "^4.31.1",
|
"filepond": "^4.31.1",
|
||||||
"filepond-plugin-file-validate-size": "^2.2.8",
|
"filepond-plugin-file-validate-size": "^2.2.8",
|
||||||
"filepond-plugin-file-validate-type": "^1.2.9",
|
"filepond-plugin-file-validate-type": "^1.2.9",
|
||||||
"filepond-plugin-image-edit": "^1.6.3",
|
|
||||||
"filepond-plugin-image-exif-orientation": "^1.0.11",
|
"filepond-plugin-image-exif-orientation": "^1.0.11",
|
||||||
"filepond-plugin-image-preview": "^4.6.12",
|
"filepond-plugin-image-preview": "^4.6.12",
|
||||||
"filepond-plugin-image-transform": "^3.8.7"
|
"photoswipe": "^5.4.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@tailwindcss/forms": "^0.5.7",
|
"@tailwindcss/forms": "^0.5.7",
|
||||||
@@ -403,6 +403,23 @@
|
|||||||
"node": ">=12"
|
"node": ">=12"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@formkit/auto-animate": {
|
||||||
|
"version": "1.0.0-pre-alpha.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@formkit/auto-animate/-/auto-animate-1.0.0-pre-alpha.3.tgz",
|
||||||
|
"integrity": "sha512-lMVZ3LFUIu0RIxCEwmV8nUUJQ46M2bv2NDU3hrhZivViuR1EheC8Mj5sx/ACqK5QLK8XB8z7GDIZBUGdU/9OZQ==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.8.0",
|
||||||
|
"vue": "^3.0.0"
|
||||||
|
},
|
||||||
|
"peerDependenciesMeta": {
|
||||||
|
"react": {
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"vue": {
|
||||||
|
"optional": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@isaacs/cliui": {
|
"node_modules/@isaacs/cliui": {
|
||||||
"version": "8.0.2",
|
"version": "8.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||||
@@ -468,6 +485,14 @@
|
|||||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@marcreichel/alpine-auto-animate": {
|
||||||
|
"version": "1.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@marcreichel/alpine-auto-animate/-/alpine-auto-animate-1.1.0.tgz",
|
||||||
|
"integrity": "sha512-ulKU3TAmZ/YkpO34j+tpGFSNSyvXAcprWkO6laFuLODx28zfJBi61TPkD3Tw9BQAl57jL91yybMFhRT3VHRPlQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"@formkit/auto-animate": "^1.0.0-beta.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@nodelib/fs.scandir": {
|
"node_modules/@nodelib/fs.scandir": {
|
||||||
"version": "2.1.5",
|
"version": "2.1.5",
|
||||||
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
|
||||||
@@ -1187,14 +1212,6 @@
|
|||||||
"filepond": ">=1.x <5.x"
|
"filepond": ">=1.x <5.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filepond-plugin-image-edit": {
|
|
||||||
"version": "1.6.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/filepond-plugin-image-edit/-/filepond-plugin-image-edit-1.6.3.tgz",
|
|
||||||
"integrity": "sha512-5q3RDaVlfvyI346ckF1DfKw4uN5rfAmUCv7HCG30jBZcGmepg8hFyjVM71uZXYeb4AgLhpCkSPP8Immwig8bzw==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"filepond": ">=3.7.2 <5.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/filepond-plugin-image-exif-orientation": {
|
"node_modules/filepond-plugin-image-exif-orientation": {
|
||||||
"version": "1.0.11",
|
"version": "1.0.11",
|
||||||
"resolved": "https://registry.npmjs.org/filepond-plugin-image-exif-orientation/-/filepond-plugin-image-exif-orientation-1.0.11.tgz",
|
"resolved": "https://registry.npmjs.org/filepond-plugin-image-exif-orientation/-/filepond-plugin-image-exif-orientation-1.0.11.tgz",
|
||||||
@@ -1211,14 +1228,6 @@
|
|||||||
"filepond": ">=4.x <5.x"
|
"filepond": ">=4.x <5.x"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/filepond-plugin-image-transform": {
|
|
||||||
"version": "3.8.7",
|
|
||||||
"resolved": "https://registry.npmjs.org/filepond-plugin-image-transform/-/filepond-plugin-image-transform-3.8.7.tgz",
|
|
||||||
"integrity": "sha512-vgKwyIDG2y5twanf7YpqZvxkaLudTjwd9vRcoq5sQDB8egUlX5/NA0bQ0823pocrm0fjbFeetICu44mkqeDkIA==",
|
|
||||||
"peerDependencies": {
|
|
||||||
"filepond": ">=3.6.0 <5.x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/fill-range": {
|
"node_modules/fill-range": {
|
||||||
"version": "7.0.1",
|
"version": "7.0.1",
|
||||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||||
@@ -1680,6 +1689,14 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/photoswipe": {
|
||||||
|
"version": "5.4.4",
|
||||||
|
"resolved": "https://registry.npmjs.org/photoswipe/-/photoswipe-5.4.4.tgz",
|
||||||
|
"integrity": "sha512-WNFHoKrkZNnvFFhbHL93WDkW3ifwVOXSW3w1UuZZelSmgXpIGiZSNlZJq37rR8YejqME2rHs9EhH9ZvlvFH2NA==",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 0.12.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/picocolors": {
|
"node_modules/picocolors": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
|
||||||
|
|||||||
@@ -15,11 +15,12 @@
|
|||||||
"vite": "^5.0"
|
"vite": "^5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@marcreichel/alpine-auto-animate": "^1.1.0",
|
||||||
"filepond": "^4.31.1",
|
"filepond": "^4.31.1",
|
||||||
"filepond-plugin-file-validate-size": "^2.2.8",
|
"filepond-plugin-file-validate-size": "^2.2.8",
|
||||||
"filepond-plugin-file-validate-type": "^1.2.9",
|
"filepond-plugin-file-validate-type": "^1.2.9",
|
||||||
"filepond-plugin-image-exif-orientation": "^1.0.11",
|
"filepond-plugin-image-exif-orientation": "^1.0.11",
|
||||||
"filepond-plugin-image-preview": "^4.6.12",
|
"filepond-plugin-image-preview": "^4.6.12",
|
||||||
"filepond-plugin-image-transform": "^3.8.7"
|
"photoswipe": "^5.4.4"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
8
public/vendor/telescope/app-dark.css
vendored
Normal file
8
public/vendor/telescope/app-dark.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
public/vendor/telescope/app.css
vendored
Normal file
7
public/vendor/telescope/app.css
vendored
Normal file
File diff suppressed because one or more lines are too long
2
public/vendor/telescope/app.js
vendored
Normal file
2
public/vendor/telescope/app.js
vendored
Normal file
File diff suppressed because one or more lines are too long
BIN
public/vendor/telescope/favicon.ico
vendored
Normal file
BIN
public/vendor/telescope/favicon.ico
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
5
public/vendor/telescope/mix-manifest.json
vendored
Normal file
5
public/vendor/telescope/mix-manifest.json
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"/app.js": "/app.js?id=7049e92a398e816f8cd53a915eaea592",
|
||||||
|
"/app-dark.css": "/app-dark.css?id=1ea407db56c5163ae29311f1f38eb7b9",
|
||||||
|
"/app.css": "/app.css?id=de4c978567bfd90b38d186937dee5ccf"
|
||||||
|
}
|
||||||
4
resources/assets/Wildsau.svg
Normal file
4
resources/assets/Wildsau.svg
Normal file
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 14 KiB |
@@ -1,5 +1,6 @@
|
|||||||
@import 'filepond/dist/filepond.min.css';
|
@import 'filepond/dist/filepond.min.css';
|
||||||
@import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
|
@import 'filepond-plugin-image-preview/dist/filepond-plugin-image-preview.css';
|
||||||
|
@import 'photoswipe/dist/photoswipe.css';
|
||||||
|
|
||||||
@tailwind base;
|
@tailwind base;
|
||||||
@tailwind components;
|
@tailwind components;
|
||||||
@@ -29,4 +30,68 @@
|
|||||||
|
|
||||||
.filepond--drop-label {
|
.filepond--drop-label {
|
||||||
@apply text-gray-900 dark:text-white;
|
@apply text-gray-900 dark:text-white;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp__bg {
|
||||||
|
@apply backdrop-blur-md bg-white/30 dark:bg-gray-800/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp__top-bar {
|
||||||
|
@apply fixed bottom-2 top-auto w-auto left-1/2 -translate-x-1/2 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800 border border-gray-200 dark:border-gray-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp__button {
|
||||||
|
width: 60px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp--zoomed-in .pswp__zoom-icn-bar-b {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp__preloader {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pswp__counter {
|
||||||
|
@apply text-base text-gray-900 dark:text-white content-center px-2;
|
||||||
|
text-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster {
|
||||||
|
z-index: 100001;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster[data-expanded="true"] {
|
||||||
|
height: var(--full-height);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster>div {
|
||||||
|
--scale: var(--index) * 0.05 + 1;
|
||||||
|
transform: translate(var(--swipe-amount, 0px), calc(14px * var(--index) + var(--front-toast-height) - 100%)) scale(calc(-1 * var(--scale)));
|
||||||
|
touch-action: none;
|
||||||
|
will-change: transform, opacity;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster>div[data-swiping="true"] {
|
||||||
|
cursor: grabbing;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster>div[data-removed="true"],
|
||||||
|
#toaster>div[data-hidden="true"] {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster[data-expanded="true"]>div[data-hidden="true"] {
|
||||||
|
opacity: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster[data-expanded="true"]>div[data-front="true"],
|
||||||
|
#toaster:hover>div[data-front="true"] {
|
||||||
|
transform: translate(var(--swipe-amount, 0px), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#toaster[data-expanded="true"]>div,
|
||||||
|
#toaster:hover>div {
|
||||||
|
transform: translate(var(--swipe-amount, 0px), calc(var(--index) * 14px + var(--offset))) scale(1);
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,8 @@
|
|||||||
import './bootstrap';
|
import './bootstrap';
|
||||||
import * as FilePond from 'filepond';
|
import './lightbox';
|
||||||
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation';
|
import './filepond';
|
||||||
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
import './notification';
|
||||||
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);
|
document.addEventListener('livewire:init', () => {
|
||||||
FilePond.registerPlugin(FilePondPluginImagePreview);
|
ToastinTakin.init();
|
||||||
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
|
||||||
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
|
||||||
FilePond.registerPlugin(FilePondPluginImageTransform);
|
|
||||||
|
|
||||||
window.FilePond = FilePond;
|
|
||||||
|
|
||||||
document.addEventListener('alpine:init', () => {
|
|
||||||
Alpine.store('uploader', {
|
|
||||||
states: {},
|
|
||||||
setState(state, value) {
|
|
||||||
console.log(state, value);
|
|
||||||
this.states[state] = value;
|
|
||||||
},
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
21
resources/js/filepond.js
Normal file
21
resources/js/filepond.js
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import * as FilePond from 'filepond';
|
||||||
|
import FilePondPluginImageExifOrientation from 'filepond-plugin-image-exif-orientation';
|
||||||
|
import FilePondPluginImagePreview from 'filepond-plugin-image-preview';
|
||||||
|
import FilePondPluginFileValidateSize from 'filepond-plugin-file-validate-size';
|
||||||
|
import FilePondPluginFileValidateType from 'filepond-plugin-file-validate-type';
|
||||||
|
|
||||||
|
FilePond.registerPlugin(FilePondPluginImageExifOrientation);
|
||||||
|
FilePond.registerPlugin(FilePondPluginImagePreview);
|
||||||
|
FilePond.registerPlugin(FilePondPluginFileValidateSize);
|
||||||
|
FilePond.registerPlugin(FilePondPluginFileValidateType);
|
||||||
|
|
||||||
|
window.FilePond = FilePond;
|
||||||
|
|
||||||
|
document.addEventListener('alpine:init', () => {
|
||||||
|
Alpine.store('uploader', {
|
||||||
|
states: {},
|
||||||
|
setState(state, value) {
|
||||||
|
this.states[state] = value;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
100
resources/js/lightbox.js
Normal file
100
resources/js/lightbox.js
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import PhotoSwipeLightbox from 'photoswipe/lightbox';
|
||||||
|
|
||||||
|
document.addEventListener('livewire:navigated', () => {
|
||||||
|
const lightbox = new PhotoSwipeLightbox({
|
||||||
|
gallery: '#album',
|
||||||
|
children: 'a',
|
||||||
|
bgOpacity: 1,
|
||||||
|
arrowPrevSVG: '<svg class="w-12 h-12 text-gray-800 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-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 19-7-7 7-7"/></svg>',
|
||||||
|
arrowNextSVG: '<svg class="w-12 h-12 text-gray-800 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-linecap="round" stroke-linejoin="round" stroke-width="2" d="m9 5 7 7-7 7"/></svg>',
|
||||||
|
closeSVG: '<div class="bg-transparent w-full h-full justify-around inline-flex items-center hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="w-6 h-6 text-gray-800 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-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18 17.94 6M18 18 6.06 6"/></svg></div>',
|
||||||
|
zoomSVG: '<div class="bg-transparent w-full h-full justify-around inline-flex items-center hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="pswp__zoom-icn-bar-v w-6 h-6 text-gray-800 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-linecap="round" stroke-width="2" d="m21 21-3.5-3.5M10 7v6m-3-3h6m4 0a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg><svg class="pswp__zoom-icn-bar-b hidden w-6 h-6 text-gray-800 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-linecap="round" stroke-width="2" d="m21 21-3.5-3.5M7 10h6m4 0a7 7 0 1 1-14 0 7 7 0 0 1 14 0Z"/></svg></div>',
|
||||||
|
pswpModule: () => import('photoswipe')
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.on('uiRegister', function() {
|
||||||
|
lightbox.pswp.ui.registerElement({
|
||||||
|
name: 'download-button',
|
||||||
|
order: 9,
|
||||||
|
isButton: true,
|
||||||
|
tagName: 'button',
|
||||||
|
html: '<div class="bg-transparent w-full h-full justify-around inline-flex items-center hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="w-6 h-6 text-gray-800 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-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 13V4M7 14H5a1 1 0 0 0-1 1v4a1 1 0 0 0 1 1h14a1 1 0 0 0 1-1v-4a1 1 0 0 0-1-1h-2m-1-5-4 5-4-5m9 8h.01"/></svg></div>',
|
||||||
|
|
||||||
|
onInit: (el, pswp) => {
|
||||||
|
el.setAttribute('download', '');
|
||||||
|
el.setAttribute('target', '_blank');
|
||||||
|
el.setAttribute('rel', 'noopener');
|
||||||
|
|
||||||
|
pswp.on('change', () => {
|
||||||
|
el.href = pswp.currSlide.data.element.dataset.pswpDownload;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.pswp.ui.registerElement({
|
||||||
|
name: 'makeCover',
|
||||||
|
ariaLabel: 'Make this Image the Album Cover',
|
||||||
|
order: 8,
|
||||||
|
isButton: true,
|
||||||
|
html: '<div class="w-full h-full bg-transparent inline-flex items-center justify-around hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg class="no-cover w-6 h-6 text-gray-800 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="M11.083 5.104c.35-.8 1.485-.8 1.834 0l1.752 4.022a1 1 0 0 0 .84.597l4.463.342c.9.069 1.255 1.2.556 1.771l-3.33 2.723a1 1 0 0 0-.337 1.016l1.03 4.119c.214.858-.71 1.552-1.474 1.106l-3.913-2.281a1 1 0 0 0-1.008 0L7.583 20.8c-.764.446-1.688-.248-1.474-1.106l1.03-4.119A1 1 0 0 0 6.8 14.56l-3.33-2.723c-.698-.571-.342-1.702.557-1.771l4.462-.342a1 1 0 0 0 .84-.597l1.753-4.022Z"/></svg><svg class="is-cover hidden w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24"><path d="M13.849 4.22c-.684-1.626-3.014-1.626-3.698 0L8.397 8.387l-4.552.361c-1.775.14-2.495 2.331-1.142 3.477l3.468 2.937-1.06 4.392c-.413 1.713 1.472 3.067 2.992 2.149L12 19.35l3.897 2.354c1.52.918 3.405-.436 2.992-2.15l-1.06-4.39 3.468-2.938c1.353-1.146.633-3.336-1.142-3.477l-4.552-.36-1.754-4.17Z"/></svg></div>',
|
||||||
|
onInit: (el, pswp) => {
|
||||||
|
pswp.on('change', () => {
|
||||||
|
el.querySelector('.is-cover').classList.toggle('hidden', pswp.currSlide.data.element.dataset.isCover === 'false')
|
||||||
|
el.querySelector('.no-cover').classList.toggle('hidden', pswp.currSlide.data.element.dataset.isCover === 'true')
|
||||||
|
});
|
||||||
|
},
|
||||||
|
onClick: (event, el) => {
|
||||||
|
Livewire.dispatch(`image.makeCover`, {
|
||||||
|
image_id: pswp.currSlide.data.element.dataset.id
|
||||||
|
});
|
||||||
|
pswp.currSlide.data.element.dataset.isCover = 'true';
|
||||||
|
el.querySelector('.is-cover').classList.toggle('hidden', pswp.currSlide.data.element.dataset.isCover === 'false')
|
||||||
|
el.querySelector('.no-cover').classList.toggle('hidden', pswp.currSlide.data.element.dataset.isCover === 'true')
|
||||||
|
|
||||||
|
ToastinTakin.success("Das Cover wurde ausgetauscht");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.pswp.ui.registerElement({
|
||||||
|
name: 'rotate-button-cw',
|
||||||
|
ariaLabel: 'Rotate clockwise',
|
||||||
|
order: 7,
|
||||||
|
isButton: true,
|
||||||
|
html: '<div class="bg-transparent w-full h-full justify-around inline-flex items-center hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" class="w-6 h-6 text-gray-800 dark:text-white" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="-1 -1 23 23"><path d="M19.407 11.376a7.552 7.552 0 1 1-2.212-5.34"/><path d="M17.555 2.612v4h-4"/></svg></div>',
|
||||||
|
onClick: (event, el) => {
|
||||||
|
pswp.close();
|
||||||
|
Livewire.dispatch(`image.rotate`, {
|
||||||
|
image_id: pswp.currSlide.data.element.dataset.id,
|
||||||
|
direction: 'cw'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.pswp.ui.registerElement({
|
||||||
|
name: 'rotate-button-ccw',
|
||||||
|
ariaLabel: 'Rotate counter-clockwise',
|
||||||
|
order: 6,
|
||||||
|
isButton: true,
|
||||||
|
html: '<div class="bg-transparent w-full h-full justify-around inline-flex items-center hover:bg-gray-100 rounded-lg dark:bg-gray-800 dark:hover:bg-gray-700"><svg xmlns="http://www.w3.org/2000/svg" fill="none" aria-hidden="true" class="w-6 h-6 text-gray-800 dark:text-white scale-x-[-1]" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" viewBox="-1 -1 23 23"><path d="M19.407 11.376a7.552 7.552 0 1 1-2.212-5.34"/><path d="M17.555 2.612v4h-4"/></svg></div>',
|
||||||
|
onClick: (event, el) => {
|
||||||
|
pswp.close();
|
||||||
|
Livewire.dispatch(`image.rotate`, {
|
||||||
|
image_id: pswp.currSlide.data.element.dataset.id,
|
||||||
|
direction: 'cw'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
lightbox.init();
|
||||||
|
|
||||||
|
document.addEventListener('livewire:init', () => {
|
||||||
|
Livewire.hook('morph.added', ({ el }) => {
|
||||||
|
if(el.dataset.pswpWidth !== undefined) {
|
||||||
|
if(lightbox.pswp !== undefined) {
|
||||||
|
lightbox.pswp.options.dataSource.items.push(el);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
144
resources/js/notification.js
Normal file
144
resources/js/notification.js
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
import {renderToaster, renderToast} from './notification/ui';
|
||||||
|
import registerSwipe from './notification/touch';
|
||||||
|
|
||||||
|
const VISIBLE_TOASTS_AMOUNT = 3;
|
||||||
|
const TOAST_LIFETIME = 4000;
|
||||||
|
const GAP = 14;
|
||||||
|
const TIME_BEFORE_UNMOUNT = 200;
|
||||||
|
|
||||||
|
window.ToastinTakin = {
|
||||||
|
init() {
|
||||||
|
if (reinitializeToaster()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderToaster();
|
||||||
|
|
||||||
|
const toaster = document.getElementById("toaster");
|
||||||
|
registerMouseOver(toaster);
|
||||||
|
registerKeyboardShortcuts(toaster);
|
||||||
|
},
|
||||||
|
success(msg) {
|
||||||
|
ToastinTakin.show(msg, "success");
|
||||||
|
},
|
||||||
|
error(msg) {
|
||||||
|
ToastinTakin.show(msg, "error");
|
||||||
|
},
|
||||||
|
info(msg) {
|
||||||
|
ToastinTakin.show(msg, "info");
|
||||||
|
},
|
||||||
|
warning(msg) {
|
||||||
|
ToastinTakin.show(msg, "warning");
|
||||||
|
},
|
||||||
|
show(msg, type) {
|
||||||
|
const list = document.getElementById("toaster");
|
||||||
|
const { toast, id } = renderToast(list, msg, type);
|
||||||
|
|
||||||
|
const el = list.children[0];
|
||||||
|
const height = el.getBoundingClientRect().height;
|
||||||
|
|
||||||
|
el.dataset.initialHeight = height;
|
||||||
|
el.dataset.hidden = false;
|
||||||
|
list.style.setProperty("--front-toast-height", `${height}px`);
|
||||||
|
|
||||||
|
registerSwipe(id);
|
||||||
|
refreshProperties();
|
||||||
|
if(list.dataset.expanded === "false") {
|
||||||
|
registerRemoveTimeout(el);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
remove(id) {
|
||||||
|
const el = document.querySelector(`[data-toast-id="${id}"]`);
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.dataset.removed = true;
|
||||||
|
|
||||||
|
refreshProperties();
|
||||||
|
|
||||||
|
const previousTid = el.dataset.unmountTid;
|
||||||
|
if (previousTid) window.clearTimeout(previousTid);
|
||||||
|
|
||||||
|
const tid = window.setTimeout(function () {
|
||||||
|
el.parentElement?.removeChild(el);
|
||||||
|
}, TIME_BEFORE_UNMOUNT);
|
||||||
|
el.dataset.unmountTid = tid;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
function registerRemoveTimeout(el) {
|
||||||
|
const tid = window.setTimeout(function () {
|
||||||
|
ToastinTakin.remove(el.dataset.toastId);
|
||||||
|
}, TOAST_LIFETIME);
|
||||||
|
el.dataset.removeTid = tid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function reinitializeToaster() {
|
||||||
|
const ol = document.getElementById("toaster");
|
||||||
|
if (!ol) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < ol.children.length; i++) {
|
||||||
|
const el = ol.children[i];
|
||||||
|
const id = el.dataset.toastId;
|
||||||
|
registerSwipe(id);
|
||||||
|
refreshProperties();
|
||||||
|
registerRemoveTimeout(el);
|
||||||
|
}
|
||||||
|
return ol;
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerMouseOver(ol) {
|
||||||
|
ol.addEventListener("mouseenter", function () {
|
||||||
|
ol.dataset.expanded = true;
|
||||||
|
for(let el of ol.children) {
|
||||||
|
clearRemoveTimeout(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
ol.addEventListener("mouseleave", function () {
|
||||||
|
ol.dataset.expanded = false;
|
||||||
|
for(let el of ol.children) {
|
||||||
|
registerRemoveTimeout(el);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function registerKeyboardShortcuts(ol) {
|
||||||
|
window.addEventListener("keydown", function (e) {
|
||||||
|
if (e.altKey && e.code === "KeyT") {
|
||||||
|
if (ol.children.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
ol.dataset.expanded = ol.dataset.expanded === "true" ? "false" : "true";;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function clearRemoveTimeout(el) {
|
||||||
|
const tid = el.dataset.removeTid;
|
||||||
|
if (tid) window.clearTimeout(tid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshProperties() {
|
||||||
|
const list = document.getElementById("toaster");
|
||||||
|
let heightsBefore = 0;
|
||||||
|
let removed = 0;
|
||||||
|
for (let i = 0; i < list.children.length; i++) {
|
||||||
|
const el = list.children[i];
|
||||||
|
if (el.dataset.removed === "true") {
|
||||||
|
removed++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const idx = i - removed;
|
||||||
|
|
||||||
|
el.dataset.index = idx;
|
||||||
|
el.dataset.front = (idx === 0).toString();
|
||||||
|
el.dataset.hidden = (idx > VISIBLE_TOASTS_AMOUNT).toString();
|
||||||
|
el.style.setProperty("--index", idx);
|
||||||
|
el.style.setProperty("--offset", `${GAP * idx + heightsBefore}px`);
|
||||||
|
el.style.setProperty("z-index", list.children.length - i);
|
||||||
|
heightsBefore += Number(el.dataset.initialHeight);
|
||||||
|
}
|
||||||
|
list.style.setProperty('--full-height', `${heightsBefore + GAP * list.children.length}px`);
|
||||||
|
}
|
||||||
6
resources/js/notification/helpers.js
Normal file
6
resources/js/notification/helpers.js
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
export function genId() {
|
||||||
|
return (
|
||||||
|
Date.now().toString(36) +
|
||||||
|
Math.random().toString(36).substring(2, 12).padStart(12, 0)
|
||||||
|
);
|
||||||
|
}
|
||||||
41
resources/js/notification/icons.js
Normal file
41
resources/js/notification/icons.js
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
const SuccessIcon = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-green-500 bg-green-100 rounded-lg dark:bg-green-800 dark:text-green-200">
|
||||||
|
<svg class="w-5 h-5" 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>
|
||||||
|
<span class="sr-only">Check icon</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const WarningIcon = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-orange-500 bg-orange-100 rounded-lg dark:bg-orange-700 dark:text-orange-200">
|
||||||
|
<svg class="w-5 h-5" 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 .5ZM10 15a1 1 0 1 1 0-2 1 1 0 0 1 0 2Zm1-4a1 1 0 0 1-2 0V6a1 1 0 0 1 2 0v5Z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Warning icon</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const InfoIcon = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-blue-500 bg-blue-100 rounded-lg dark:bg-blue-800 dark:text-blue-200">
|
||||||
|
<svg class="w-6 h-6 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="currentColor" viewBox="0 0 24 24">
|
||||||
|
<path fill-rule="evenodd" d="M2 12C2 6.477 6.477 2 12 2s10 4.477 10 10-4.477 10-10 10S2 17.523 2 12Zm9.408-5.5a1 1 0 1 0 0 2h.01a1 1 0 1 0 0-2h-.01ZM10 10a1 1 0 1 0 0 2h1v3h-1a1 1 0 1 0 0 2h4a1 1 0 1 0 0-2h-1v-4a1 1 0 0 0-1-1h-2Z" clip-rule="evenodd"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Error icon</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
const ErrorIcon = `
|
||||||
|
<div class="inline-flex items-center justify-center flex-shrink-0 w-8 h-8 text-red-500 bg-red-100 rounded-lg dark:bg-red-800 dark:text-red-200">
|
||||||
|
<svg class="w-5 h-5" 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 11.793a1 1 0 1 1-1.414 1.414L10 11.414l-2.293 2.293a1 1 0 0 1-1.414-1.414L8.586 10 6.293 7.707a1 1 0 0 1 1.414-1.414L10 8.586l2.293-2.293a1 1 0 0 1 1.414 1.414L11.414 10l2.293 2.293Z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Error icon</span>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
export default function getIcon(type) {
|
||||||
|
switch (type) {
|
||||||
|
case "success": return SuccessIcon;
|
||||||
|
case "info": return InfoIcon;
|
||||||
|
case "warning": return WarningIcon;
|
||||||
|
case "error": return ErrorIcon;
|
||||||
|
default: return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
57
resources/js/notification/touch.js
Normal file
57
resources/js/notification/touch.js
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
const SWIPE_THRESHOLD = 50;
|
||||||
|
|
||||||
|
export default function registerSwipe(id) {
|
||||||
|
const el = document.querySelector(`[data-toast-id="${id}"]`);
|
||||||
|
if (!el) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let dragStartTime = null;
|
||||||
|
let pointerStart = null;
|
||||||
|
|
||||||
|
el.addEventListener("pointerdown", function (event) {
|
||||||
|
dragStartTime = new Date();
|
||||||
|
event.target.setPointerCapture(event.pointerId);
|
||||||
|
if (event.target.tagName === "BUTTON") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
el.dataset.swiping = true;
|
||||||
|
pointerStart = { x: event.clientX, y: event.clientY };
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener("pointerup", function (event) {
|
||||||
|
pointerStart = null;
|
||||||
|
const swipeAmount = Number(
|
||||||
|
el.style.getPropertyValue("--swipe-amount").replace("px", "") || 0,
|
||||||
|
);
|
||||||
|
const timeTaken = new Date().getTime() - dragStartTime.getTime();
|
||||||
|
const velocity = Math.abs(swipeAmount) / timeTaken;
|
||||||
|
|
||||||
|
// Remove only if threshold is met
|
||||||
|
if (Math.abs(swipeAmount) >= SWIPE_THRESHOLD || velocity > 0.11) {
|
||||||
|
ToastinTakin.remove(id);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
el.style.setProperty("--swipe-amount", "0px");
|
||||||
|
el.dataset.swiping = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
el.addEventListener("pointermove", function (event) {
|
||||||
|
if (!pointerStart) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const yPosition = event.clientY - pointerStart.y;
|
||||||
|
const xPosition = event.clientX - pointerStart.x;
|
||||||
|
|
||||||
|
const clampedX = Math.max(0, xPosition);
|
||||||
|
const swipeStartThreshold = event.pointerType === "touch" ? 10 : 2;
|
||||||
|
const isAllowedToSwipe = Math.abs(clampedX) > swipeStartThreshold;
|
||||||
|
|
||||||
|
if (isAllowedToSwipe) {
|
||||||
|
el.style.setProperty("--swipe-amount", `${xPosition}px`);
|
||||||
|
} else if (Math.abs(yPosition) > swipeStartThreshold) {
|
||||||
|
// Swipe is heading into the wrong direction, possibly the user wants to abort.
|
||||||
|
pointerStart = null;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
55
resources/js/notification/ui.js
Normal file
55
resources/js/notification/ui.js
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import getIcon from './icons';
|
||||||
|
import { genId } from './helpers';
|
||||||
|
|
||||||
|
export function renderToaster() {
|
||||||
|
const toaster = document.createElement("div");
|
||||||
|
document.body.appendChild(toaster);
|
||||||
|
toaster.outerHTML = `
|
||||||
|
<div
|
||||||
|
aria-label="Notifications alt+T"
|
||||||
|
tabindex="-1"
|
||||||
|
data-expanded="false"
|
||||||
|
class="fixed right-4 top-4 z-50 w-80"
|
||||||
|
id="toaster"
|
||||||
|
></div>`;
|
||||||
|
return toaster;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderToast(list, msg, type) {
|
||||||
|
const toast = document.createElement("div");
|
||||||
|
const id = genId();
|
||||||
|
const count = list.children.length;
|
||||||
|
const icon = getIcon(type);
|
||||||
|
|
||||||
|
list.prepend(toast);
|
||||||
|
|
||||||
|
toast.outerHTML = `
|
||||||
|
<div
|
||||||
|
aria-live="polite"
|
||||||
|
aria-atomic="true"
|
||||||
|
role="alert"
|
||||||
|
class="absolute select-none transition-opacity transition-transform flex items-center w-full max-w-xs p-4 text-gray-500 bg-white rounded-lg shadow dark:text-gray-400 dark:bg-gray-800"
|
||||||
|
tabindex="0"
|
||||||
|
data-toast-id="${id}"
|
||||||
|
data-removed="false"
|
||||||
|
data-hidden="true"
|
||||||
|
data-index="${0}"
|
||||||
|
data-front="true"
|
||||||
|
data-swiping="false"
|
||||||
|
data-swipe-out="false"
|
||||||
|
style="--index: 0; --offset: 0px; --initial-height: 0px;"
|
||||||
|
>
|
||||||
|
${ icon ? icon : ""}
|
||||||
|
<div class="ms-3 text-sm font-normal">
|
||||||
|
${msg}
|
||||||
|
</div>
|
||||||
|
<button onclick="ToastinTakin.remove('${id}')" type="button" class="ms-auto -mx-1.5 -my-1.5 bg-white text-gray-400 hover:text-gray-900 rounded-lg focus:ring-2 focus:ring-gray-300 p-1.5 hover:bg-gray-100 inline-flex items-center justify-center h-8 w-8 dark:text-gray-500 dark:hover:text-white dark:bg-gray-800 dark:hover:bg-gray-700" data-dismiss-target="#toast-success" aria-label="Close">
|
||||||
|
<span class="sr-only">Close</span>
|
||||||
|
<svg class="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>
|
||||||
|
</button>
|
||||||
|
</div>`;
|
||||||
|
|
||||||
|
return {id, toast};
|
||||||
|
}
|
||||||
@@ -20,28 +20,15 @@
|
|||||||
</div>
|
</div>
|
||||||
@endpush
|
@endpush
|
||||||
|
|
||||||
<x-layout>
|
<x-layouts.app :title="$category->name">
|
||||||
<x-hero-search></x-hero-search>
|
<div class="flex flex-wrap flex-row gap-4">
|
||||||
<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">
|
<x-drawer-trigger target="album-add" action="open" class="relative rounded-lg cursor-pointer h-80 flex-grow flex items-center justify-center bg-gray-300 sm:w-96 dark:bg-gray-700 hover:opacity-50">
|
||||||
{{ $category->name }}
|
<svg class="w-24 h-24 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
</h1>
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7 7V5"/>
|
||||||
<div class="m-8 flex flex-wrap flex-row gap-4">
|
</svg>
|
||||||
|
</x-drawer-trigger>
|
||||||
@foreach ($albums as $album)
|
@foreach ($albums as $album)
|
||||||
<figure class="relative rounded-lg cursor-pointer h-80 flex-grow overflow-hidden group">
|
<x-album.element :album="$album"></x-album.element>
|
||||||
<a href="{{ route('album.show', $album) }}" wire:navigate>
|
|
||||||
<img class="max-h-full min-w-full align-bottom object-cover"
|
|
||||||
src="{{ $album->thumbnail }}" alt="{{ $album->name }} Cover">
|
|
||||||
<figcaption class="absolute p-4 text-lg text-white top-1/2 bottom-0 bg-opacity-20 min-w-full bg-gradient-to-b from-transparent to-slate-900 flex flex-col-reverse">
|
|
||||||
<span class="z-10">{{ $album->name }}</span>
|
|
||||||
</figcaption>
|
|
||||||
<div class="opacity-0 z-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>
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
<x-drawer name="album-add" >
|
<x-drawer name="album-add" >
|
||||||
@@ -50,4 +37,4 @@
|
|||||||
<livewire:drawer.album.create></livewire:drawer.album.create>
|
<livewire:drawer.album.create></livewire:drawer.album.create>
|
||||||
</x-slot:content>
|
</x-slot:content>
|
||||||
</x-drawer>
|
</x-drawer>
|
||||||
</x-layout>
|
</x-layouts.app>
|
||||||
|
|||||||
17
resources/views/components/album/element.blade.php
Normal file
17
resources/views/components/album/element.blade.php
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
@props(['album' => null])
|
||||||
|
|
||||||
|
<figure class="relative rounded-lg cursor-pointer h-80 flex-grow overflow-hidden group" {{ $attributes }}>
|
||||||
|
<a href="{{ route('album.show', $album) }}" wire:navigate>
|
||||||
|
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||||
|
src="{{ $album->thumbnail }}" alt="{{ $album->name }} Cover">
|
||||||
|
<figcaption class="absolute p-4 text-xl text-white font-bold uppercase top-1/2 bottom-0 bg-opacity-20 min-w-full bg-gradient-to-b from-transparent to-black flex flex-col-reverse z-10">
|
||||||
|
<span class="z-1">{{ $album->name }}</span>
|
||||||
|
</figcaption>
|
||||||
|
<div class="opacity-0 z-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 text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" 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>
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
@@ -1,12 +1,14 @@
|
|||||||
@props(['active' => false ])
|
@props(['active' => false ])
|
||||||
|
|
||||||
@if($active)
|
@if($active)
|
||||||
<li {{ $attributes }} class="inline-block m-2 relative after:absolute after:left-0 after:bottom-1 after:border-yellow-pfadi after:-z-10 after:border-4 dark:after:opacity-80 after:w-full text-black dark:text-white">
|
<li {{ $attributes }} class="bg-yellow-pfadi text-gray-800 font-medium px-4 py-1 rounded-lg cursor-pointer">
|
||||||
|
<!--<li {{ $attributes }} class="inline-block m-2 relative after:absolute after:left-0 after:bottom-1 after:border-yellow-pfadi after:-z-10 after:border-4 dark:after:opacity-80 after:w-full text-black dark:text-white">-->
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</li>
|
</li>
|
||||||
@else
|
@else
|
||||||
<li {{ $attributes }} class="inline-block m-2 dark:text-gray-100 dark:hover:text-white text-gray-400 relative hover:text-black after:w-0
|
<li {{ $attributes }} class="bg-gray-100 text-gray-800 font-medium px-4 py-1 rounded-lg dark:bg-gray-700 dark:text-gray-300 cursor-pointer">
|
||||||
cursor-pointer after:border-0 after:absolute after:left-0 after:bottom-1 dark:after:opacity-80 after:border-yellow-pfadi hover:after:border-4 hover:after:w-full after:transition-all after:duration-100 after:ease-out after:-z-10">
|
<!--<li {{ $attributes }} class="inline-block m-2 dark:text-gray-100 dark:hover:text-white text-gray-400 relative hover:text-black after:w-0
|
||||||
|
cursor-pointer after:border-0 after:absolute after:left-0 after:bottom-1 dark:after:opacity-80 after:border-yellow-pfadi hover:after:border-4 hover:after:w-full after:transition-all after:duration-100 after:ease-out after:-z-10">-->
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</li>
|
</li>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
11
resources/views/components/category/element.blade.php
Normal file
11
resources/views/components/category/element.blade.php
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
@props(['category' => null])
|
||||||
|
|
||||||
|
<figure class="relative rounded-lg cursor-pointer h-80 flex-grow overflow-hidden" {{ $attributes }}>
|
||||||
|
<a href="{{ route('category.show', $category) }}">
|
||||||
|
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||||
|
src="{{ url($category->cover) }}" alt="{{ $category->name }} Cover">
|
||||||
|
<figcaption class="absolute p-4 text-xl text-white font-bold uppercase top-1/2 bottom-0 bg-opacity-20 min-w-full bg-gradient-to-b from-transparent to-black flex flex-col-reverse">
|
||||||
|
<span>{{ $category->name }}</span>
|
||||||
|
</figcaption>
|
||||||
|
</a>
|
||||||
|
</figure>
|
||||||
@@ -1,3 +1,3 @@
|
|||||||
<div @click="$dispatch('drawer-{{ $action }}-{{ $target }}')" x-on:touchstart="$dispatch('drawer-{{ $action }}-{{ $target }}')">
|
<div @click="$dispatch('drawer-{{ $action }}-{{ $target }}')" x-on:touchstart="$dispatch('drawer-{{ $action }}-{{ $target }}')" {{ $attributes }}>
|
||||||
{{ $slot }}
|
{{ $slot }}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,31 +1,36 @@
|
|||||||
<template x-teleport="body">
|
<template x-teleport="body">
|
||||||
<div x-cloak
|
<div x-cloak
|
||||||
x-data="{ show: false }"
|
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="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 z-30"
|
||||||
:class="{ 'translate-y-full': !show }"
|
:class="{ 'translate-y-full': !show }"
|
||||||
@click="$dispatch('drawer-close-{{ $name }}')"
|
@click.self="$dispatch('drawer-close-{{ $name }}')"
|
||||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
x-on:touchstart.self="$dispatch('drawer-close-{{ $name }}')"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
id="drawer-{{ $name }}"
|
id="drawer-{{ $name }}"
|
||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
class="fixed z-40 w-full overflow-y-auto bg-white border-t border-gray-200 rounded-t-lg dark:border-gray-700 dark:bg-gray-800 transition-all max-xl:left-0 max-xl:right-0 max-xl:bottom-0 max-h-full xl:m-auto xl:relative xl:p-4 xl:w-max xl:max-w-7xl xl:rounded-lg xl:border"
|
class="fixed z-40 w-full overflow-y-auto bg-white border-t border-gray-200 rounded-t-lg dark:border-gray-700 dark:bg-gray-800 transition-all max-xl:left-0 max-xl:right-0 max-xl:bottom-0 max-h-full xl:m-auto xl:relative xl:p-4 xl:w-max xl:max-w-7xl xl:rounded-lg xl:border z-50"
|
||||||
aria-labelledby="drawer-{{ $name }}"
|
aria-labelledby="drawer-{{ $name }}"
|
||||||
@drawer-open-{{ $name }}.window="$dispatch('menu-hide'); show = true"
|
@drawer-open-{{ $name }}.window="$dispatch('menu-hide'); show = true"
|
||||||
@drawer-close-{{ $name }}.window="show = false; $dispatch('menu-show')"
|
@drawer-close-{{ $name }}.window="show = false; $dispatch('menu-show')"
|
||||||
:class="{ 'translate-y-full': !show, 'xl:-bottom-full': !show }"
|
:class="{ 'translate-y-full': !show, 'xl:-bottom-full': !show }"
|
||||||
>
|
>
|
||||||
<div
|
<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"
|
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 }}')"
|
<span
|
||||||
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
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 }}')"
|
||||||
<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">
|
x-on:touchstart="$dispatch('drawer-close-{{ $name }}')"
|
||||||
|
>
|
||||||
</span>
|
</span>
|
||||||
<h5 class="inline-flex items-center text-base text-gray-500 dark:text-gray-400 font-medium">
|
<h5 class="inline-flex items-center text-base text-gray-500 dark:text-gray-400 font-medium">
|
||||||
{{ $title }}
|
{{ $title }}
|
||||||
</h5>
|
</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">
|
<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"/>
|
<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>
|
</svg>
|
||||||
|
|||||||
20
resources/views/components/form/input.blade.php
Normal file
20
resources/views/components/form/input.blade.php
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
@props([
|
||||||
|
'name' => 'undefined',
|
||||||
|
'label' => 'Undefined',
|
||||||
|
'type' => 'text',
|
||||||
|
'placeholder' => '',
|
||||||
|
])
|
||||||
|
|
||||||
|
<div class="mb-5">
|
||||||
|
<label for="{{ $name }}" class="block mb-2 text-sm font-medium text-gray-900 dark:text-white">{{ $label }}</label>
|
||||||
|
<input type="{{ $type }}" id="{{ $name }}"
|
||||||
|
@class([
|
||||||
|
'border text-sm rounded-lg block w-full p-2.5',
|
||||||
|
'bg-red-50 border-red-500 text-red-900 placeholder-red-700 focus:ring-red-500 dark:bg-gray-700 focus:border-red-500 dark:text-red-500 dark:placeholder-red-500 dark:border-red-500' => $errors->has($name),
|
||||||
|
'bg-gray-50 border-gray-300 text-gray-900 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' => !$errors->has($name),
|
||||||
|
])
|
||||||
|
placeholder="{{ $placeholder }}" {{ $attributes }} />
|
||||||
|
@error($name)
|
||||||
|
<p class="mt-2 text-sm text-red-600 dark:text-red-500"><span class="font-medium">Oops!</span> {{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
145
resources/views/components/form/upload.blade.php
Normal file
145
resources/views/components/form/upload.blade.php
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
<div
|
||||||
|
class="relative xl:min-w-[1000px]"
|
||||||
|
wire:ignore
|
||||||
|
x-cloak
|
||||||
|
x-data="{
|
||||||
|
model: @entangle($attributes->whereStartsWith('wire:model')->first()),
|
||||||
|
isMultiple: {{ $multiple ? 'true' : 'false' }},
|
||||||
|
current: undefined,
|
||||||
|
currentList: [],
|
||||||
|
async URLtoFile(path) {
|
||||||
|
let url = `${window.appUrlStorage}/${path}`;
|
||||||
|
let name = url.split('/').pop();
|
||||||
|
const response = await fetch(url);
|
||||||
|
const data = await response.blob();
|
||||||
|
const metadata = {
|
||||||
|
name: name,
|
||||||
|
size: data.size,
|
||||||
|
type: data.type
|
||||||
|
};
|
||||||
|
let file = new File([data], name, metadata);
|
||||||
|
return {
|
||||||
|
source: file,
|
||||||
|
options: {
|
||||||
|
type: 'local',
|
||||||
|
metadata: {
|
||||||
|
name: name,
|
||||||
|
size: file.size,
|
||||||
|
type: file.type
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}"
|
||||||
|
x-init="async () => {
|
||||||
|
let picture = model;
|
||||||
|
let files = [];
|
||||||
|
let exists = [];
|
||||||
|
if (model) {
|
||||||
|
if (isMultiple) {
|
||||||
|
currentList = model.map((picture) => `${window.appUrlStorage}/${picture}`);
|
||||||
|
await Promise.all(model.map(async (picture) => exists.push(await URLtoFile(picture))));
|
||||||
|
} else {
|
||||||
|
if (picture) {
|
||||||
|
exists.push(await URLtoFile(picture));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
files = exists;
|
||||||
|
let modelName = '{{ $attributes->whereStartsWith('wire:model')->first() }}';
|
||||||
|
|
||||||
|
const pond = FilePond.create($refs.{{ $attributes->get('ref') ?? 'input' }}, { credits: false});
|
||||||
|
|
||||||
|
const uploadWatcher = () => {
|
||||||
|
$store.uploader.setState('{{ $name }}', pond.status);
|
||||||
|
}
|
||||||
|
|
||||||
|
pond.setOptions({
|
||||||
|
allowMultiple: {{ $multiple ? 'true' : 'false' }},
|
||||||
|
server: {
|
||||||
|
process: (fieldName, file, metadata, load, error, progress, abort, transfer, options) => {
|
||||||
|
@this.upload(modelName, file, load, error, progress)
|
||||||
|
},
|
||||||
|
revert: (filename, load) => {
|
||||||
|
@this.removeUpload(modelName, filename, load)
|
||||||
|
},
|
||||||
|
remove: (filename, load) => {
|
||||||
|
@this.removeFile(modelName, filename.name)
|
||||||
|
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' }},
|
||||||
|
acceptedFileTypes: {{ $accept ? $accept : 'null' }},
|
||||||
|
allowFileSizeValidation: {{ $validate ? 'true' : 'false' }},
|
||||||
|
maxFileSize: {!! $size ? "'" . $size . "'" : 'null' !!},
|
||||||
|
maxFiles: {{ $number ? $number : 'null' }},
|
||||||
|
required: {{ $required ? 'true' : 'false' }},
|
||||||
|
disabled: {{ $disabled ? 'true' : 'false' }},
|
||||||
|
onaddfilestart: uploadWatcher,
|
||||||
|
onprocessfile: uploadWatcher
|
||||||
|
});
|
||||||
|
pond.addFiles(files);
|
||||||
|
pond.on('addfile', (error, file) => {
|
||||||
|
if (error) {
|
||||||
|
console.log('Oh no');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
uploadWatcher();
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
@if ($label)
|
||||||
|
<div class="flex items-center justify-between">
|
||||||
|
<label class="block text-sm font-medium leading-6 text-gray-900 dark:text-gray-100"
|
||||||
|
for="{{ $name }}"
|
||||||
|
>
|
||||||
|
{{ $label }}
|
||||||
|
@if ($required)
|
||||||
|
<span class="text-red-500" title="Required">*</span>
|
||||||
|
@endif
|
||||||
|
</label>
|
||||||
|
<div class="text-xs text-gray-400">Size max: {{ $sizeHuman }}</div>
|
||||||
|
</div>
|
||||||
|
@endif
|
||||||
|
<div class="flex items-center justify-between text-xs text-gray-400">
|
||||||
|
<div>Formats: {{ $acceptHuman }}</div>
|
||||||
|
<div>
|
||||||
|
{{ $multiple ? 'Multiple' : 'Single' }}
|
||||||
|
@if ($multiple)
|
||||||
|
<span>({{ $number }} files max)</span>
|
||||||
|
@endif
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-5">
|
||||||
|
<input type="file"
|
||||||
|
x-ref="{{ $attributes->get('ref') ?? 'input' }}"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
@error('image')
|
||||||
|
<p class="mt-2 text-sm text-red-600">{{ $message }}</p>
|
||||||
|
@enderror
|
||||||
|
</div>
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<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>
|
|
||||||
</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>
|
|
||||||
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 |
@@ -1,28 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html
|
|
||||||
lang="{{ str_replace('_', '-', app()->getLocale()) }}"
|
|
||||||
x-data="{ darkMode: $persist(false) }"
|
|
||||||
:class="{'dark': darkMode }"
|
|
||||||
x-init="
|
|
||||||
if (!('darkMode' in localStorage) && window.matchMedia('(prefers-color-scheme: dark)').matches) {
|
|
||||||
localStorage.setItem('darkMode', JSON.stringify(true));
|
|
||||||
}
|
|
||||||
darkMode = JSON.parse(localStorage.getItem('darkMode'));
|
|
||||||
$watch('darkMode', value => localStorage.setItem('darkMode', JSON.stringify(value)))"
|
|
||||||
>
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
||||||
@vite('resources/css/app.css')
|
|
||||||
@vite('resources/js/app.js')
|
|
||||||
<title>{{ $title ?? config('app.name') }}</title>
|
|
||||||
</head>
|
|
||||||
<body class="bg-white dark:bg-gray-800">
|
|
||||||
@persist('theme-switcher')
|
|
||||||
<x-theme-switcher />
|
|
||||||
@endpersist
|
|
||||||
{{ $slot }}
|
|
||||||
|
|
||||||
<x-menu />
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
22
resources/views/components/layouts/app.blade.php
Normal file
22
resources/views/components/layouts/app.blade.php
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<script>
|
||||||
|
const darkMode = localStorage.getItem('darkMode') || matchMedia('(prefers-color-scheme: light)').matches;
|
||||||
|
document.documentElement.dataset.dark = darkMode;
|
||||||
|
</script>
|
||||||
|
|
||||||
|
@vite('resources/css/app.css')
|
||||||
|
@vite('resources/js/app.js')
|
||||||
|
<title>{{ $title ?? config('app.name') }}</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body class="bg-white dark:bg-gray-800 min-h-screen flex flex-col px-12 py-12 px-24">
|
||||||
|
<livewire:menu :title="$title ?? ''"></livewire:menu>
|
||||||
|
|
||||||
|
{{ $slot }}
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
@props(['tooltip' => 'Unknown' ])
|
|
||||||
|
|
||||||
<button type="button" class="inline-flex flex-col items-center justify-center px-5 hover:bg-gray-50 dark:hover:bg-gray-800 group first:rounded-s-full last:rounded-e-full">
|
|
||||||
{{ $slot }}
|
|
||||||
<span class="sr-only">{{ $tooltip }}</span>
|
|
||||||
<div role="tooltip" class="absolute z-10 group-hover:visible group-hover:opacity-100 invisible inline-block px-3 py-2 text-sm font-medium text-white transition-opacity duration-300 bg-gray-900 rounded-lg shadow-sm opacity-0 dark:bg-gray-700 -translate-y-full origin-top-left -mt-2 top-0">
|
|
||||||
{{ $tooltip }}
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
@@ -1,22 +0,0 @@
|
|||||||
<div
|
|
||||||
class="fixed z-50 w-11/12 h-16 max-w-lg -translate-x-1/2 bg-white border border-gray-200 rounded-full left-1/2 dark:bg-gray-700 dark:border-gray-600 transition-all"
|
|
||||||
x-data="{ visible: true }"
|
|
||||||
@menu-hide.window="visible = false"
|
|
||||||
@menu-show.window="visible = true"
|
|
||||||
:class="{ '-bottom-16': !visible, 'bottom-4': visible }"
|
|
||||||
x-cloak
|
|
||||||
>
|
|
||||||
<div class="grid grid-flow-col h-full max-w-lg mx-auto">
|
|
||||||
<x-menu-action tooltip="Startseite">
|
|
||||||
<svg class="w-5 h-5 mb-1 text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="m19.707 9.293-2-2-7-7a1 1 0 0 0-1.414 0l-7 7-2 2a1 1 0 0 0 1.414 1.414L2 10.414V18a2 2 0 0 0 2 2h3a1 1 0 0 0 1-1v-4a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v4a1 1 0 0 0 1 1h3a2 2 0 0 0 2-2v-7.586l.293.293a1 1 0 0 0 1.414-1.414Z"/>
|
|
||||||
</svg>
|
|
||||||
</x-menu-action>
|
|
||||||
@stack('menu')
|
|
||||||
<x-menu-action tooltip="Profil">
|
|
||||||
<svg class="w-5 h-5 mb-1 text-gray-500 dark:text-gray-400 group-hover:text-blue-600 dark:group-hover:text-blue-500" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 0a10 10 0 1 0 10 10A10.011 10.011 0 0 0 10 0Zm0 5a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm0 13a8.949 8.949 0 0 1-4.951-1.488A3.987 3.987 0 0 1 9 13h2a3.987 3.987 0 0 1 3.951 3.512A8.949 8.949 0 0 1 10 18Z"/>
|
|
||||||
</svg>
|
|
||||||
</x-menu-action>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
<button x-cloak id="theme-toggle" data-tooltip-target="tooltip-toggle" type="button"
|
|
||||||
class="text-gray-500 inline-flex items-center justify-center dark:text-gray-400 hover:bg-gray-100 w-10 h-10 dark:hover:bg-gray-700 focus:outline-none rounded-lg text-sm p-2.5 right-0 absolute z-10"
|
|
||||||
@click="darkMode = !darkMode"
|
|
||||||
>
|
|
||||||
<svg x-show="!darkMode" id="theme-toggle-dark-icon" class="w-8 h-8" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 18 20">
|
|
||||||
<path d="M17.8 13.75a1 1 0 0 0-.859-.5A7.488 7.488 0 0 1 10.52 2a1 1 0 0 0 0-.969A1.035 1.035 0 0 0 9.687.5h-.113a9.5 9.5 0 1 0 8.222 14.247 1 1 0 0 0 .004-.997Z"></path>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<svg x-show="darkMode" id="theme-toggle-light-icon" class="w-8 h-8" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 20">
|
|
||||||
<path d="M10 15a5 5 0 1 0 0-10 5 5 0 0 0 0 10Zm0-11a1 1 0 0 0 1-1V1a1 1 0 0 0-2 0v2a1 1 0 0 0 1 1Zm0 12a1 1 0 0 0-1 1v2a1 1 0 1 0 2 0v-2a1 1 0 0 0-1-1ZM4.343 5.757a1 1 0 0 0 1.414-1.414L4.343 2.929a1 1 0 0 0-1.414 1.414l1.414 1.414Zm11.314 8.486a1 1 0 0 0-1.414 1.414l1.414 1.414a1 1 0 0 0 1.414-1.414l-1.414-1.414ZM4 10a1 1 0 0 0-1-1H1a1 1 0 0 0 0 2h2a1 1 0 0 0 1-1Zm15-1h-2a1 1 0 1 0 0 2h2a1 1 0 0 0 0-2ZM4.343 14.243l-1.414 1.414a1 1 0 1 0 1.414 1.414l1.414-1.414a1 1 0 0 0-1.414-1.414ZM14.95 6.05a1 1 0 0 0 .707-.293l1.414-1.414a1 1 0 1 0-1.414-1.414l-1.414 1.414a1 1 0 0 0 .707 1.707Z"></path>
|
|
||||||
</svg>
|
|
||||||
<span x-show="!darkMode" class="sr-only">Dunkle Theme aktivieren</span>
|
|
||||||
<span x-show="darkMode" class="sr-only">Helle Theme aktivieren</span>
|
|
||||||
</button>
|
|
||||||
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>
|
||||||
60
resources/views/livewire/album/show.blade.php
Normal file
60
resources/views/livewire/album/show.blade.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<div>
|
||||||
|
<div id="album">
|
||||||
|
<div class="flex flex-wrap flex-row gap-4" @if($album->hasProcessingMedia) wire:poll @endif>
|
||||||
|
<div class="relative rounded-lg overflow-hidden cursor-pointer h-80 flex-grow flex items-center justify-center sm:w-96 flex flex-col gap-1">
|
||||||
|
@if(!$album->hasProcessingMedia)
|
||||||
|
<x-drawer-trigger target="image-add" action="open" class="flex-grow w-full text-center bg-gray-300 dark:bg-gray-700 hover:opacity-80 flex items-center justify-center transition-opacity">
|
||||||
|
<svg class="w-24 h-24 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14m-7 7V5"/>
|
||||||
|
</svg>
|
||||||
|
</x-drawer-trigger>
|
||||||
|
@endif
|
||||||
|
<x-drawer-trigger target="image-add" action="open" class="flex-grow w-full text-center bg-gray-300 dark:bg-gray-700 hover:opacity-80 flex items-center justify-center transition-opacity">
|
||||||
|
<svg class="w-24 h-24 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-width="2" d="M20 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6h-2m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4m16 6H10m0 0a2 2 0 1 0-4 0m4 0a2 2 0 1 1-4 0m0 0H4"/>
|
||||||
|
</svg>
|
||||||
|
</x-drawer-trigger>
|
||||||
|
</div>
|
||||||
|
@foreach ($images as $image)
|
||||||
|
@if($image->isProcessing)
|
||||||
|
<div wire:transition wire:key="placeholder_{{ $image->id }}" role="status" class="flex items-center justify-center h-80 flex-grow bg-gray-300 rounded-lg animate-pulse dark:bg-gray-700 min-w-80">
|
||||||
|
<div class="min-w-lg">
|
||||||
|
<svg class="w-10 h-10 text-gray-200 dark:text-gray-600" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="currentColor" viewBox="0 0 20 18">
|
||||||
|
<path d="M18 0H2a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h16a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2Zm-5.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3Zm4.376 10.481A1 1 0 0 1 16 15H4a1 1 0 0 1-.895-1.447l3.5-7A1 1 0 0 1 7.468 6a.965.965 0 0 1 .9.5l2.775 4.757 1.546-1.887a1 1 0 0 1 1.618.1l2.541 4a1 1 0 0 1 .028 1.011Z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Vorschaubild wird generiert, bitte warten...</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
@else
|
||||||
|
<a
|
||||||
|
class="relative group rounded-lg cursor-pointer h-80 flex-grow overflow-hidden"
|
||||||
|
href="{{ $image->lightbox['location'] }}"
|
||||||
|
data-pswp-width="{{ $image->lightbox['width'] }}"
|
||||||
|
data-pswp-download="{{ $image->getDownload() }}"
|
||||||
|
data-pswp-height="{{ $image->lightbox['height'] }}"
|
||||||
|
data-cropped="true"
|
||||||
|
data-is-cover="{{ $image->isCover ? 'true' : 'false' }}"
|
||||||
|
data-id="{{ $image->id }}"
|
||||||
|
wire:key="image_{{ $image->id }}"
|
||||||
|
wire:transition
|
||||||
|
>
|
||||||
|
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||||
|
src="{{ $image->getThumbnail() }}" alt="Album image">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
@endif
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
<div class="p-2 mt-8">
|
<div>
|
||||||
<div class="text-center">
|
<div class="mb-8">
|
||||||
<ul class="uppercase font-medium">
|
<ul class="uppercase font-medium flex gap-12 justify-center">
|
||||||
<x-category-filter-pill wire:click="setFilter(-1)" :active="$filter == null">
|
<x-category-filter-pill wire:click="setFilter(-1)" :active="$filter == null">
|
||||||
Alle
|
Alle
|
||||||
</x-category-filter-pill>
|
</x-category-filter-pill>
|
||||||
@@ -13,19 +13,9 @@
|
|||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="m-8 flex flex-wrap flex-row gap-4">
|
<div class="flex flex-wrap flex-row gap-4 transition-opacity" wire:loading.class="opacity-0">
|
||||||
@foreach ($categories as $category)
|
@foreach ($this->categories as $category)
|
||||||
@if($filter == null || $category->tags->contains($filter))
|
<x-category.element :category="$category" wire:key="{{ $category->id }}"></x-category.element>
|
||||||
<figure wire:transition wire:key="{{ $category->id }}" class="relative rounded-lg cursor-pointer h-80 flex-grow overflow-hidden">
|
|
||||||
<a href="{{ route('category.show', $category) }}">
|
|
||||||
<img class="max-h-full min-w-full align-bottom object-cover"
|
|
||||||
src="{{ url($category->cover) }}" alt="{{ $category->name }} Cover">
|
|
||||||
<figcaption class="absolute p-4 text-lg text-white top-1/2 bottom-0 bg-opacity-20 min-w-full bg-gradient-to-b from-transparent to-slate-900 flex flex-col-reverse">
|
|
||||||
<span>{{ $category->name }}</span>
|
|
||||||
</figcaption>
|
|
||||||
</a>
|
|
||||||
</figure>
|
|
||||||
@endif
|
|
||||||
@endforeach
|
@endforeach
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
26
resources/views/livewire/drawer/album/add-image.blade.php
Normal file
26
resources/views/livewire/drawer/album/add-image.blade.php
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
<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">
|
||||||
|
<svg aria-hidden="true" role="status" class="inline w-4 h-4 me-3 text-white animate-spin" 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="#E5E7EB"/>
|
||||||
|
<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="currentColor"/>
|
||||||
|
</svg>
|
||||||
|
Bitte warten ...
|
||||||
|
</button>
|
||||||
|
<div x-transition.delay.500ms x-show="$store.uploader.states && $store.uploader.states.media == 2" class="flex items-center p-4 mb-4 text-sm text-red-800 rounded-lg bg-red-50 dark:bg-gray-800 dark:text-red-400" role="alert">
|
||||||
|
<svg class="flex-shrink-0 inline w-4 h-4 me-3" 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 .5ZM9.5 4a1.5 1.5 0 1 1 0 3 1.5 1.5 0 0 1 0-3ZM12 15H8a1 1 0 0 1 0-2h1v-3H8a1 1 0 0 1 0-2h2a1 1 0 0 1 1 1v4h1a1 1 0 0 1 0 2Z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="sr-only">Info</span>
|
||||||
|
<div>
|
||||||
|
<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" @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>
|
||||||
14
resources/views/livewire/drawer/album/create.blade.php
Normal file
14
resources/views/livewire/drawer/album/create.blade.php
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
<div>
|
||||||
|
<form class="p-4" wire:submit="save">
|
||||||
|
<x-form.input name="name" label="Name" wire:model="name" required/>
|
||||||
|
<x-form.input name="capture_date_string" label="Aufnahmedatum" type="date" wire:model="capture_date_string" required />
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
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 w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Abbrechen</button>
|
||||||
|
|
||||||
|
<button type="submit"
|
||||||
|
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 w-full sm:w-auto px-5 py-2.5 text-center dark:bg-blue-600 dark:hover:bg-blue-700 dark:focus:ring-blue-800">Speichern</button>
|
||||||
|
|
||||||
|
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
@@ -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>
|
||||||
21
resources/views/livewire/image/grid.blade.php
Normal file
21
resources/views/livewire/image/grid.blade.php
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
<div id="album" class="m-8 flex flex-wrap flex-row gap-4">
|
||||||
|
@foreach ($images as $image)
|
||||||
|
<a
|
||||||
|
class="relative group rounded-lg cursor-pointer h-80 flex-grow overflow-hidden"
|
||||||
|
href="{{ $image->lightbox['location'] }}"
|
||||||
|
data-pswp-width="{{ $image->lightbox['width'] }}"
|
||||||
|
data-pswp-download="{{ $image->getDownload() }}"
|
||||||
|
data-pswp-height="{{ $image->lightbox['height'] }}"
|
||||||
|
data-cropped="true"
|
||||||
|
>
|
||||||
|
<img class="max-h-full min-w-full align-bottom object-cover"
|
||||||
|
src="{{ $image->getThumbnail() }}" alt="Album image">
|
||||||
|
<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>
|
||||||
|
</a>
|
||||||
|
@endforeach
|
||||||
|
</div>
|
||||||
43
resources/views/livewire/menu.blade.php
Normal file
43
resources/views/livewire/menu.blade.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<div class="w-full flex flex-row items-center justify-between mb-12">
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<a href="javascript:history.back();">
|
||||||
|
<svg class="w-8 h-8 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m15 19-7-7 7-7"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
<a href="/" wire:navigate>
|
||||||
|
<svg class="w-8 h-8 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="m4 12 8-8 8 8M6 10.5V19a1 1 0 0 0 1 1h3v-3a1 1 0 0 1 1-1h2a1 1 0 0 1 1 1v3h3a1 1 0 0 0 1-1v-8.5"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<h1 class="text-4xl font-extrabold leading-none tracking-tight text-gray-900 md:text-5xl lg:text-6xl dark:text-white inline-block relative after:absolute after:left-0 after:bottom-1 after:border-yellow-pfadi after:-z-10 after:border-4 dark:after:opacity-80 after:w-full text-black dark:text-white">
|
||||||
|
{{ $title }}
|
||||||
|
</h1>
|
||||||
|
<div class="flex flex-row gap-4">
|
||||||
|
<form class="relative text-right w-48">
|
||||||
|
<input type="search" name="" placeholder="Search for..." class="peer absolute -top-1 right-0 block p-2.5 w-8 focus:w-48 text-sm text-gray-900 bg-gray-50 rounded-lg focus:ring-0 focus:border-2 focus:border-yellow-pfadi dark:bg-gray-700 dark:border-s-gray-700 dark:border-gray-600 dark:placeholder-gray-400 dark:text-white opacity-0 focus:opacity-100 transition-all">
|
||||||
|
|
||||||
|
<button type="submit" class="absolute top-0 right-0 pointer-events-none transition-colors peer-focus:opacity-0">
|
||||||
|
<svg class="w-8 h-8 text-gray-800 dark:text-white" 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>
|
||||||
|
<span class="sr-only">Search</span>
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<div @click="darkMode = !darkMode"
|
||||||
|
x-data="{darkMode: $persist(matchMedia('(prefers-color-scheme: dark)').matches).as('darkMode')}"
|
||||||
|
x-init="$watch('darkMode', darkMode => document.documentElement.dataset.dark = darkMode)"
|
||||||
|
class="w-8 h-8">
|
||||||
|
<svg x-show="!darkMode" class="w-8 h-8 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" x-cloak>
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 21a9 9 0 0 1-.5-17.986V3c-.354.966-.5 1.911-.5 3a9 9 0 0 0 9 9c.239 0 .254.018.488 0A9.004 9.004 0 0 1 12 21Z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
<svg x-show="darkMode" class="w-8 h-8 text-gray-800 dark:text-white" aria-hidden="true" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" x-cloak>
|
||||||
|
<path stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 5V3m0 18v-2M7.05 7.05 5.636 5.636m12.728 12.728L16.95 16.95M5 12H3m18 0h-2M7.05 16.95l-1.414 1.414M18.364 5.636 16.95 7.05M16 12a4 4 0 1 1-8 0 4 4 0 0 1 8 0Z"/>
|
||||||
|
</svg>
|
||||||
|
<span x-show="!darkMode" class="sr-only">Dunkle Theme aktivieren</span>
|
||||||
|
<span x-show="darkMode" class="sr-only">Helle Theme aktivieren</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
<x-layout>
|
<x-layouts.app title="Pfadi Säuliamt Galerie">
|
||||||
<x-hero-search></x-hero-search>
|
|
||||||
|
|
||||||
<livewire:category-filter />
|
<livewire:category-filter />
|
||||||
</x-layout>
|
</x-layouts.app>
|
||||||
|
|||||||
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
|
<?php
|
||||||
|
|
||||||
use Illuminate\Foundation\Inspiring;
|
|
||||||
use Illuminate\Support\Facades\Artisan;
|
use Illuminate\Support\Facades\Artisan;
|
||||||
|
use Illuminate\Support\Facades\Schedule;
|
||||||
|
|
||||||
Artisan::command('inspire', function () {
|
Schedule::command('queue:prune-batches')->daily();
|
||||||
$this->comment(Inspiring::quote());
|
Schedule::command('horizon:snapshot')->everyFiveMinutes();
|
||||||
})->purpose('Display an inspiring quote')->hourly();
|
|
||||||
@@ -3,9 +3,11 @@
|
|||||||
use App\Http\Controllers\ImageController;
|
use App\Http\Controllers\ImageController;
|
||||||
use Illuminate\Support\Facades\Route;
|
use Illuminate\Support\Facades\Route;
|
||||||
use App\Http\Controllers\CategoryController;
|
use App\Http\Controllers\CategoryController;
|
||||||
use App\Http\Controllers\AlbumController;
|
use App\Livewire\Album\Show as AlbumShow;
|
||||||
|
|
||||||
Route::get('/', [CategoryController::class, 'index'])->name('index');
|
Route::get('/', [CategoryController::class, 'index'])->name('index');
|
||||||
Route::get('/category/{category}', [CategoryController::class, 'show'])->name('category.show');
|
Route::get('/category/{category}', [CategoryController::class, 'show'])->name('category.show');
|
||||||
Route::get('/album/{album}', [AlbumController::class, 'show'])->name('album.show');
|
Route::get('/album/{album}', AlbumShow::class)->name('album.show');
|
||||||
Route::get('/image/{image}', [ImageController::class, 'show'])->name('image.show');
|
Route::get('/image/{image}/thumbnail', [ImageController::class, 'thumbnail'])->name('image.thumbnail');
|
||||||
|
Route::get('/image/{image}/lightbox', [ImageController::class, 'lightbox'])->name('image.lightbox');
|
||||||
|
Route::get('/image/{image}/download', [ImageController::class, 'download'])->name('image.download');
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/** @type {import('tailwindcss').Config} */
|
/** @type {import('tailwindcss').Config} */
|
||||||
export default {
|
export default {
|
||||||
darkMode: 'selector',
|
darkMode: ['selector', '[data-dark="true"]'],
|
||||||
content: [
|
content: [
|
||||||
"./resources/**/*.blade.php",
|
"./resources/**/*.blade.php",
|
||||||
"./resources/**/*.js",
|
"./resources/**/*.js",
|
||||||
@@ -10,6 +10,7 @@ export default {
|
|||||||
extend: {
|
extend: {
|
||||||
colors: {
|
colors: {
|
||||||
'yellow-pfadi': '#ffdd00',
|
'yellow-pfadi': '#ffdd00',
|
||||||
|
'yellow-pfadi-dark': '#f2d40d',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user