diff --git a/public/css/app.css b/public/css/app.css index cde978e..e3430e4 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -473,6 +473,9 @@ Ensure the default browser behavior of the `hidden` attribute. -webkit-animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; } +.cursor-pointer { + cursor: pointer; +} .cursor-not-allowed { cursor: not-allowed; } @@ -497,6 +500,14 @@ Ensure the default browser behavior of the `hidden` attribute. .rounded { border-radius: 0.25rem; } +.rounded-t-md { + border-top-left-radius: 0.375rem; + border-top-right-radius: 0.375rem; +} +.rounded-b-md { + border-bottom-right-radius: 0.375rem; + border-bottom-left-radius: 0.375rem; +} .border { border-width: 1px; } @@ -504,6 +515,10 @@ Ensure the default browser behavior of the `hidden` attribute. --tw-border-opacity: 1; border-color: rgb(0 0 0 / var(--tw-border-opacity)); } +.bg-black { + --tw-bg-opacity: 1; + background-color: rgb(0 0 0 / var(--tw-bg-opacity)); +} .bg-stone-800 { --tw-bg-opacity: 1; background-color: rgb(41 37 36 / var(--tw-bg-opacity)); @@ -512,6 +527,9 @@ Ensure the default browser behavior of the `hidden` attribute. -o-object-fit: scale-down; object-fit: scale-down; } +.p-2 { + padding: 0.5rem; +} .p-1 { padding: 0.25rem; } @@ -532,6 +550,14 @@ Ensure the default browser behavior of the `hidden` attribute. .pr-2 { padding-right: 0.5rem; } +.text-base { + font-size: 1rem; + line-height: 1.5rem; +} +.text-gray-500 { + --tw-text-opacity: 1; + color: rgb(107 114 128 / var(--tw-text-opacity)); +} .text-neutral-500 { --tw-text-opacity: 1; color: rgb(115 115 115 / var(--tw-text-opacity)); @@ -540,6 +566,9 @@ Ensure the default browser behavior of the `hidden` attribute. -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } +.opacity-75 { + opacity: 0.75; +} .shadow { --tw-shadow: 0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1); --tw-shadow-colored: 0 1px 3px 0 var(--tw-shadow-color), 0 1px 2px -1px var(--tw-shadow-color); @@ -555,6 +584,20 @@ Ensure the default browser behavior of the `hidden` attribute. transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } +.delay-150 { + transition-delay: 150ms; +} +.duration-300 { + transition-duration: 300ms; +} +.ease-in-out { + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); +} +.hover\:scale-90:hover { + --tw-scale-x: .9; + --tw-scale-y: .9; + transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y)); +} .hover\:text-orange-400:hover { --tw-text-opacity: 1; color: rgb(251 146 60 / var(--tw-text-opacity)); diff --git a/public/js/app.js b/public/js/app.js index 03cb31f..d74da18 100644 --- a/public/js/app.js +++ b/public/js/app.js @@ -19576,8 +19576,8 @@ __webpack_require__.r(__webpack_exports__); window.history.pushState({}, '', url); }, galleryUrl: function galleryUrl(page) { - var url = _config__WEBPACK_IMPORTED_MODULE_0__.isNotProduction ? '/gallery.json' : '/gallery'; - url += "?page=" + page; + var url = _config__WEBPACK_IMPORTED_MODULE_0__.isNotProduction ? '/gallery.json' : "/gallery?timestamp=".concat(new Date().getTime()); + url += "&page=" + page; if (this.filters) { url += '&filter[tags]=' + this.filters; @@ -19588,7 +19588,12 @@ __webpack_require__.r(__webpack_exports__); getImages: function getImages(page) { var _this = this; - axios.get(this.galleryUrl(page)).then(function (response) { + axios.get(this.galleryUrl(page), { + headers: { + 'Content-Type': 'application/json', + 'X-Robots-Tag': 'noindex, nofollow' + } + }).then(function (response) { var _response$data = response.data, data = _response$data.data, current_page = _response$data.current_page, @@ -19629,7 +19634,8 @@ __webpack_require__.r(__webpack_exports__); props: { image: { required: false, - type: String + type: Object, + "default": null } } }); @@ -19686,7 +19692,12 @@ __webpack_require__.r(__webpack_exports__); _this = this; var url = _config__WEBPACK_IMPORTED_MODULE_0__.isNotProduction ? '/tags.json' : '/tags/'; - axios.get(url + ((_this$filters = this.filters) !== null && _this$filters !== void 0 ? _this$filters : '')).then(function (response) { + axios.get(url + ((_this$filters = this.filters) !== null && _this$filters !== void 0 ? _this$filters : ''), { + headers: { + 'Content-Type': 'application/json', + 'X-Robots-Tag': 'noindex, nofollow' + } + }).then(function (response) { _this.navigation = response.data; _this.removeTheAllTag(); @@ -19987,8 +19998,9 @@ function render(_ctx, _cache, $props, $setup, $data, $options) { alt: image.alt, image: image.thumbnail, key: "image-".concat(index), + onContextmenu: _cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(function () {}, ["prevent"])), onClick: function onClick($event) { - return $data.currentImage = image.image; + return $data.currentImage = image; } }, null, 8 /* PROPS */ @@ -20007,7 +20019,7 @@ function render(_ctx, _cache, $props, $setup, $data, $options) { }, [$data.currentImage ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createBlock)(_component_modal, { key: 0, image: $data.currentImage, - onClose: _cache[0] || (_cache[0] = function ($event) { + onClose: _cache[1] || (_cache[1] = function ($event) { return $data.currentImage = null; }) }, null, 8 @@ -20038,21 +20050,39 @@ var _withScopeId = function _withScopeId(n) { }; var _hoisted_1 = { + key: 0, "class": "modal" }; -var _hoisted_2 = ["src"]; +var _hoisted_2 = { + "class": "rounded-t-md flex justify-between bg-black opacity-75 text-base text-gray-500 p-2" +}; + +var _hoisted_3 = /*#__PURE__*/_withScopeId(function () { + return /*#__PURE__*/(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", null, "Click to Close", -1 + /* HOISTED */ + ); +}); + +var _hoisted_4 = ["src"]; function render(_ctx, _cache, $props, $setup, $data, $options) { return (0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", { "class": "shadow", - onClick: _cache[0] || (_cache[0] = function ($event) { + onContextmenu: _cache[0] || (_cache[0] = (0,vue__WEBPACK_IMPORTED_MODULE_0__.withModifiers)(function ($event) { + return _ctx.$emit("close"); + }, ["prevent"])), + onClick: _cache[1] || (_cache[1] = function ($event) { return _ctx.$emit("close"); }) - }, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("img", { - src: $props.image, - "class": "object-scale-down max-w-screen max-h-screen" + }, [$props.image ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("div", _hoisted_1, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", _hoisted_2, [(0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("div", null, (0,vue__WEBPACK_IMPORTED_MODULE_0__.toDisplayString)($props.image.alt), 1 + /* TEXT */ + ), _hoisted_3]), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementVNode)("img", { + src: $props.image.image, + "class": "rounded-b-md cursor-pointer object-scale-down max-w-screen max-h-screen" }, null, 8 /* PROPS */ - , _hoisted_2)])]); + , _hoisted_4)])) : (0,vue__WEBPACK_IMPORTED_MODULE_0__.createCommentVNode)("v-if", true)], 32 + /* HYDRATE_EVENTS */ + ); } /***/ }), @@ -20290,7 +20320,7 @@ var _hoisted_1 = ["src", "alt"]; function render(_ctx, _cache, $props, $setup, $data, $options) { return $props.image ? ((0,vue__WEBPACK_IMPORTED_MODULE_0__.openBlock)(), (0,vue__WEBPACK_IMPORTED_MODULE_0__.createElementBlock)("img", { key: 0, - "class": "w-full", + "class": "transition delay-150 duration-300 ease-in-out hover:scale-90 cursor-pointer w-full", src: $props.image, alt: $props.alt }, null, 8 diff --git a/src/Console/Commands/GenerateImages.php b/src/Console/Commands/GenerateImages.php index 789739d..2eaac7a 100644 --- a/src/Console/Commands/GenerateImages.php +++ b/src/Console/Commands/GenerateImages.php @@ -10,6 +10,8 @@ class GenerateImages extends Command { + const MISSING_IMAGE_COLOR = '#ffb52e'; + const THUMBNAIL_WIDTH = 125; const THUMBNAIL_HEIGHT = 175; const THUMBNAIL_QUALITY = 75; @@ -39,7 +41,7 @@ class GenerateImages extends Command * * @return int */ - public function handle() + public function handle() : int { $originalImages = Storage::disk("gallery-originals")->allFiles(); @@ -58,6 +60,7 @@ public function handle() } $this->createMissingThumbnail(); + $this->createMissingImage(); return 0; } @@ -82,7 +85,7 @@ private function resizeImage($originalImage) ->save($thumbPath, self::THUMBNAIL_QUALITY, pathinfo($gallery->thumb, PATHINFO_EXTENSION)); $image = Image::make($originalPath); - list($width, $height) = $this->calculateWidthHeight($image); + [$width, $height] = $this->calculateWidthHeight($image); $image->resize($width, $height, function ($constraint) { $constraint->aspectRatio(); })->save($imagePath); @@ -97,7 +100,7 @@ private function resizeImage($originalImage) * @param \Intervention\Image\Image $image * @return array */ - private function calculateWidthHeight(\Intervention\Image\Image $image) + private function calculateWidthHeight(\Intervention\Image\Image $image) : array { $width = self::MAX_IMAGE_WIDTH; $height = self::MAX_IMAGE_HEIGHT; @@ -116,15 +119,32 @@ private function createMissingThumbnail() $img = Image::canvas(self::THUMBNAIL_WIDTH, self::THUMBNAIL_HEIGHT, '#ffa500'); $img ->line(0, 0, self::THUMBNAIL_WIDTH, self::THUMBNAIL_HEIGHT, function ($draw) { - $draw->color('#ffb52e'); + $draw->color(self::MISSING_IMAGE_COLOR); }) ->line(self::THUMBNAIL_WIDTH, 0, 0, self::THUMBNAIL_HEIGHT, function ($draw) { - $draw->color('#ffb52e'); + $draw->color(self::MISSING_IMAGE_COLOR); }) - ->text('Missing Image', 28, 20, function ($font) { + ->text('Missing Image', round(self::THUMBNAIL_WIDTH / 2 - 34), 20, function ($font) { $font->color('#000'); }); - $img->save(Storage::disk('gallery')->path('missing.gif'), 90, 'gif'); - $this->info('Created missing image: ' . Storage::disk('gallery')->path('missing.gif')); + $img->save(Storage::disk('gallery')->path('missing-thumbnail.gif'), 90, 'gif'); + $this->info('Created missing thumbnail: ' . Storage::disk('gallery')->path('missing-thumbnail.gif')); + } + + private function createMissingImage() + { + $img = Image::canvas(self::MAX_IMAGE_WIDTH, self::MAX_IMAGE_HEIGHT, '#ffa500'); + $img + ->line(0, 0, self::MAX_IMAGE_WIDTH, self::MAX_IMAGE_HEIGHT, function ($draw) { + $draw->color(self::MISSING_IMAGE_COLOR); + }) + ->line(self::MAX_IMAGE_WIDTH, 0, 0, self::MAX_IMAGE_HEIGHT, function ($draw) { + $draw->color(self::MISSING_IMAGE_COLOR); + }) + ->text('Missing Image', round(self::MAX_IMAGE_WIDTH / 2 - 34), 20, function ($font) { + $font->color('#000'); + }); + $img->save(Storage::disk('gallery')->path('missing-image.gif'), 90, 'gif'); + $this->info('Created missing image: ' . Storage::disk('gallery')->path('missing-image.gif')); } } diff --git a/src/Http/Controllers/Controller.php b/src/Http/Controllers/Controller.php deleted file mode 100644 index 0614c5a..0000000 --- a/src/Http/Controllers/Controller.php +++ /dev/null @@ -1,15 +0,0 @@ -json([ 'children' => - UberTags::where('parent', 0) + UberTags::query()->where('parent', 0) ->get() ->filter(function ($tag) { return Route::has($tag->name); @@ -25,11 +25,11 @@ public function __invoke($filters = '') $childFilters = array_slice($names, 1); return response()->json([ - 'current' => UberTags::where('name', $currentFilter) + 'current' => UberTags::query()->where('name', $currentFilter) ->with('parent') ->first(), 'children' => - UberTags::whereIn('name', $childFilters) + UberTags::query()->whereIn('name', $childFilters) //->children() ->with('parent') ->orderBy('name') diff --git a/src/Models/UberGallery.php b/src/Models/UberGallery.php index 7335af0..ddbe5cd 100644 --- a/src/Models/UberGallery.php +++ b/src/Models/UberGallery.php @@ -2,6 +2,7 @@ namespace LisaFehr\Gallery\Models; +use Carbon\Carbon; use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; @@ -23,7 +24,7 @@ class UberGallery extends Model 'text', ]; - public function tag() + public function tag() : \Illuminate\Database\Eloquent\Relations\HasOneThrough { return $this->hasOneThrough( UberTags::class, @@ -35,25 +36,25 @@ public function tag() ); } - public function getImageAttribute() + public function getImageAttribute() : string { $url = $this->tag->directory . '/' . $this->img . '.' . $this->type; if (Storage::disk('gallery')->exists($url)) { - return Storage::disk('gallery')->url($url); + return Storage::disk('gallery')->temporaryUrl($url, Carbon::now()->addDay()); } - return Storage::disk('gallery')->url('/missing.gif'); + return Storage::disk('gallery')->url('/missing-image.gif'); } - public function getThumbnailAttribute() + public function getThumbnailAttribute() : ?string { if (! $this->tag) { return null; } $url = $this->tag->directory . '/t/' . $this->thumb; if (Storage::disk('gallery')->exists($url)) { - return Storage::disk('gallery')->url($url); + return Storage::disk('gallery')->temporaryUrl($url, Carbon::now()->addDay()); } - return Storage::disk('gallery')->url('/missing.gif'); + return Storage::disk('gallery')->url('/missing-thumbnail.gif'); } public function scopeTags(Builder $builder, ...$tags) @@ -65,7 +66,7 @@ public function scopeTags(Builder $builder, ...$tags) ->whereIn('uber_tags.name', $tags); } - public function toArray() + public function toArray() : array { return [ 'image' => $this->image, diff --git a/src/Models/UberTags.php b/src/Models/UberTags.php index 89caac5..3c1021e 100644 --- a/src/Models/UberTags.php +++ b/src/Models/UberTags.php @@ -30,7 +30,7 @@ class UberTags extends Model 'parent', ]; - public function scopeAllChildren(Builder $builder) + public function scopeAllChildren(Builder $builder) : Builder { $children = $builder->select('children', 'id')->having('children', '=', 1); $parents = $builder->pluck('id'); @@ -42,10 +42,10 @@ public function scopeAllChildren(Builder $builder) $collect = $collect->merge($parents); $children = $current->contains('children', '1'); } - return self::whereIn('id', $collect); + return self::query()->whereIn('id', $collect); } - public function scopeChildren(Builder $builder) + public function scopeChildren(Builder $builder) : Builder { $parents = $builder->pluck('id'); $collect = collect([]); @@ -54,10 +54,10 @@ public function scopeChildren(Builder $builder) $parents = $current->pluck('id'); $collect = $collect->merge($parents); - return self::whereIn('id', $collect); + return self::query()->whereIn('id', $collect); } - public function parent() + public function parent() : \Illuminate\Database\Eloquent\Relations\HasOne { return $this->hasOne(self::class, 'id', 'parent'); } diff --git a/src/Providers/AppServiceProvider.php b/src/Providers/AppServiceProvider.php index ce18c5d..3deea49 100644 --- a/src/Providers/AppServiceProvider.php +++ b/src/Providers/AppServiceProvider.php @@ -2,6 +2,8 @@ namespace LisaFehr\Gallery\Providers; +use Illuminate\Support\Facades\Storage; +use Illuminate\Support\Facades\URL; use Illuminate\Support\ServiceProvider; use LisaFehr\Gallery\Console\Commands\GenerateImages; use LisaFehr\Gallery\Console\Commands\InstallCommand; @@ -36,5 +38,13 @@ public function boot() InstallCommand::class, ]); } + + Storage::disk('gallery')->buildTemporaryUrlsUsing(function ($path, $expiration, $options) { + return URL::temporarySignedRoute( + 'gallery.temp', + $expiration, + array_merge($options, ['path' => $path]) + ); + }); } } diff --git a/src/resources/js/components/gallery.vue b/src/resources/js/components/gallery.vue index 814b388..7834688 100644 --- a/src/resources/js/components/gallery.vue +++ b/src/resources/js/components/gallery.vue @@ -3,7 +3,7 @@
- +
@@ -78,9 +78,9 @@ window.history.pushState({}, '', url); }, galleryUrl(page) { - let url = isNotProduction ? '/gallery.json' : '/gallery'; + let url = isNotProduction ? '/gallery.json' : `/gallery?timestamp=${new Date().getTime()}`; - url += "?page=" + page; + url += "&page=" + page; if (this.filters) { url += '&filter[tags]=' + this.filters; } @@ -88,7 +88,9 @@ return url; }, getImages(page) { - axios.get(this.galleryUrl(page)).then(response => { + axios.get(this.galleryUrl(page), { + headers: {'Content-Type': 'application/json', 'X-Robots-Tag': 'noindex, nofollow'} + }).then(response => { const { data, current_page, from, last_page, per_page, to, total } = response.data; this.images = data; this.pagination = { current_page, from, last_page, per_page,to, total }; diff --git a/src/resources/js/components/modal.vue b/src/resources/js/components/modal.vue index 3222870..3cc6eec 100644 --- a/src/resources/js/components/modal.vue +++ b/src/resources/js/components/modal.vue @@ -1,7 +1,11 @@ @@ -12,7 +16,8 @@ props: { image: { required: false, - type: String, + type: Object, + default: null }, } }; diff --git a/src/resources/js/components/navigation.vue b/src/resources/js/components/navigation.vue index e334112..a6a6c99 100644 --- a/src/resources/js/components/navigation.vue +++ b/src/resources/js/components/navigation.vue @@ -53,7 +53,9 @@ getFilters() { let url = isNotProduction ? '/tags.json' : '/tags/'; - axios.get(url + (this.filters ?? '')).then(response => { + axios.get(url + (this.filters ?? ''), { + headers: {'Content-Type': 'application/json', 'X-Robots-Tag': 'noindex, nofollow'} + }).then(response => { this.navigation = response.data; this.removeTheAllTag(); this.loading = false; diff --git a/src/resources/js/components/thumbnail.vue b/src/resources/js/components/thumbnail.vue index 50939f7..08f9d06 100644 --- a/src/resources/js/components/thumbnail.vue +++ b/src/resources/js/components/thumbnail.vue @@ -1,5 +1,5 @@