thumbnail support
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Jobs;
|
|||||||
|
|
||||||
use App\Models\Media;
|
use App\Models\Media;
|
||||||
use Illuminate\Bus\Queueable;
|
use Illuminate\Bus\Queueable;
|
||||||
|
use Illuminate\Contracts\Container\BindingResolutionException;
|
||||||
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
use Illuminate\Contracts\Queue\ShouldBeUnique;
|
||||||
use Illuminate\Contracts\Queue\ShouldQueue;
|
use Illuminate\Contracts\Queue\ShouldQueue;
|
||||||
use Illuminate\Foundation\Bus\Dispatchable;
|
use Illuminate\Foundation\Bus\Dispatchable;
|
||||||
@@ -12,6 +13,8 @@ use Illuminate\Queue\SerializesModels;
|
|||||||
use Illuminate\Support\Facades\File;
|
use Illuminate\Support\Facades\File;
|
||||||
use Illuminate\Support\Facades\Log;
|
use Illuminate\Support\Facades\Log;
|
||||||
use Illuminate\Support\Facades\Storage;
|
use Illuminate\Support\Facades\Storage;
|
||||||
|
use Intervention\Image\Exception\NotWritableException;
|
||||||
|
use Intervention\Image\Exception\NotSupportedException;
|
||||||
use SplFileInfo;
|
use SplFileInfo;
|
||||||
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
use Symfony\Component\HttpFoundation\File\UploadedFile;
|
||||||
use Intervention\Image\Facades\Image;
|
use Intervention\Image\Facades\Image;
|
||||||
@@ -63,6 +66,8 @@ class StoreUploadedFileJob implements ShouldQueue
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Execute the job.
|
* Execute the job.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function handle(): void
|
public function handle(): void
|
||||||
{
|
{
|
||||||
@@ -195,6 +200,8 @@ class StoreUploadedFileJob implements ShouldQueue
|
|||||||
$this->media->variants = $variants;
|
$this->media->variants = $variants;
|
||||||
}//end if
|
}//end if
|
||||||
|
|
||||||
|
$this->generateThumbnail();
|
||||||
|
|
||||||
if (strlen($this->uploadedFilePath) > 0) {
|
if (strlen($this->uploadedFilePath) > 0) {
|
||||||
unlink($this->uploadedFilePath);
|
unlink($this->uploadedFilePath);
|
||||||
}
|
}
|
||||||
@@ -208,4 +215,92 @@ class StoreUploadedFileJob implements ShouldQueue
|
|||||||
$this->fail($e);
|
$this->fail($e);
|
||||||
}//end try
|
}//end try
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a Thumbnail for this media.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function generateThumbnail(): void
|
||||||
|
{
|
||||||
|
$thumbnailWidth = 200;
|
||||||
|
$thumbnailHeight = 200;
|
||||||
|
|
||||||
|
$storageDisk = $this->media->storage;
|
||||||
|
$fileName = $this->media->name;
|
||||||
|
$filePath = $this->uploadedFilePath;
|
||||||
|
$fileExtension = File::extension($fileName);
|
||||||
|
$mimeType = $this->media->mime_type;
|
||||||
|
$tempImagePath = tempnam(sys_get_temp_dir(), 'thumb');
|
||||||
|
$newFilename = pathinfo($fileName, PATHINFO_FILENAME) . "-thumb.webp";
|
||||||
|
$success = false;
|
||||||
|
|
||||||
|
// $ffmpegPath = '/usr/bin/ffmpeg';
|
||||||
|
$ffmpegPath = '/opt/homebrew/bin/ffmpeg';
|
||||||
|
|
||||||
|
if (strpos($mimeType, 'image/') === 0) {
|
||||||
|
$image = Image::make($filePath);
|
||||||
|
$image->fit($thumbnailWidth, $thumbnailHeight);
|
||||||
|
$image->encode('webp', 75)->save($tempImagePath);
|
||||||
|
$success = true;
|
||||||
|
} elseif ($mimeType === 'application/pdf' && extension_loaded('imagick') === true) {
|
||||||
|
$pdfPreview = new \Imagick();
|
||||||
|
$pdfPreview->setResolution(300, 300);
|
||||||
|
$pdfPreview->readImage($filePath . '[0]');
|
||||||
|
$pdfPreview->setImageFormat('webp');
|
||||||
|
$pdfPreview->thumbnailImage($thumbnailWidth, $thumbnailHeight, true);
|
||||||
|
file_put_contents($tempImagePath, $pdfPreview);
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
} elseif ($mimeType === 'text/plain') {
|
||||||
|
$image = Image::canvas($thumbnailWidth, $thumbnailHeight, '#FFFFFF');
|
||||||
|
|
||||||
|
// Read the first few lines of the text file
|
||||||
|
$numLines = 5;
|
||||||
|
$text = file_get_contents($filePath);
|
||||||
|
$lines = explode("\n", $text);
|
||||||
|
$previewText = implode("\n", array_slice($lines, 0, $numLines));
|
||||||
|
|
||||||
|
// Center the text on the image
|
||||||
|
$fontSize = 8;
|
||||||
|
$textColor = '#000000'; // Black text color
|
||||||
|
|
||||||
|
// Calculate the position to start drawing the text
|
||||||
|
$x = 10; // Left padding
|
||||||
|
$y = 10; // Top padding
|
||||||
|
|
||||||
|
// Draw the text on the canvas with text wrapping
|
||||||
|
$lines = explode("\n", wordwrap($previewText, 30, "\n", true));
|
||||||
|
foreach ($lines as $line) {
|
||||||
|
$image->text($line, $x, $y, function ($font) use ($fontSize, $textColor) {
|
||||||
|
$font->file(1);
|
||||||
|
$font->size($fontSize);
|
||||||
|
$font->color($textColor);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Move to the next line
|
||||||
|
$y += ($fontSize + 4); // Add some vertical spacing between lines (adjust as needed)
|
||||||
|
}
|
||||||
|
|
||||||
|
$image->encode('webp', 75)->save($tempImagePath);
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
} elseif (file_exists($ffmpegPath) === true && strpos($mimeType, 'video/') === 0) {
|
||||||
|
$tempImagePath .= '.webp';
|
||||||
|
exec("$ffmpegPath -i $filePath -ss 00:00:05 -vframes 1 -s {$thumbnailWidth}x{$thumbnailHeight} -c:v webp {$tempImagePath}");
|
||||||
|
|
||||||
|
$success = true;
|
||||||
|
}//end if
|
||||||
|
|
||||||
|
if ($success === true && file_exists($tempImagePath) === true) {
|
||||||
|
Storage::disk($storageDisk)->putFileAs('/', new SplFileInfo($tempImagePath), $newFilename);
|
||||||
|
unlink($tempImagePath);
|
||||||
|
|
||||||
|
$this->media->thumbnail = $this->media->getUrlPath() . $newFilename;
|
||||||
|
} else {
|
||||||
|
$fileIconPath = '/assets/fileicons/' . ($fileExtension !== '' && file_exists(public_path('assets/fileicons/' . $fileExtension . '.webp')) ? $fileExtension : 'unknown') . '.webp';
|
||||||
|
$this->media->thumbnail = asset($fileIconPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ class Media extends Model
|
|||||||
'description' => '',
|
'description' => '',
|
||||||
'dimensions' => '',
|
'dimensions' => '',
|
||||||
'permission' => '',
|
'permission' => '',
|
||||||
|
'thumbnail' => '',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
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::table('media', function (Blueprint $table) {
|
||||||
|
$table->string('thumbnail')->default('');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reverse the migrations.
|
||||||
|
*/
|
||||||
|
public function down(): void
|
||||||
|
{
|
||||||
|
Schema::table('media', function (Blueprint $table) {
|
||||||
|
$table->dropColumn('thumbnail');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -131,28 +131,28 @@
|
|||||||
@dblclick="handleDblClickItem(item.id)">
|
@dblclick="handleDblClickItem(item.id)">
|
||||||
<div
|
<div
|
||||||
:class="[
|
:class="[
|
||||||
'flex',
|
|
||||||
'flex-items-center',
|
|
||||||
'flex-justify-center',
|
|
||||||
'h-30',
|
'h-30',
|
||||||
'w-40',
|
'w-40',
|
||||||
'bg-contain',
|
'bg-contain',
|
||||||
'bg-center',
|
'bg-center',
|
||||||
'bg-no-repeat',
|
'bg-no-repeat',
|
||||||
|
'relative',
|
||||||
|
{ 'mb-6': showMediaName(item) },
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundImage:
|
backgroundImage: `url('${mediaGetThumbnail(
|
||||||
item.url.length > 0
|
item,
|
||||||
? `url('${mediaGetVariantUrl(
|
)}')`,
|
||||||
item,
|
|
||||||
'small',
|
|
||||||
)}')`
|
|
||||||
: 'none',
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.status === 'OK'
|
item.status === 'OK'
|
||||||
? 'initial'
|
? 'initial'
|
||||||
: 'rgba(220,220,220,1)',
|
: 'rgba(220,220,220,1)',
|
||||||
}">
|
}">
|
||||||
|
<div
|
||||||
|
v-if="showMediaName(item)"
|
||||||
|
class="absolute -bottom-6 small w-full text-ellipsis overflow-hidden whitespace-nowrap">
|
||||||
|
{{ item.title }}
|
||||||
|
</div>
|
||||||
<SMLoading
|
<SMLoading
|
||||||
v-if="
|
v-if="
|
||||||
item.status !== 'OK' &&
|
item.status !== 'OK' &&
|
||||||
@@ -230,12 +230,7 @@
|
|||||||
<div
|
<div
|
||||||
class="flex text-xs border-b border-gray-3 pb-4">
|
class="flex text-xs border-b border-gray-3 pb-4">
|
||||||
<img
|
<img
|
||||||
:src="
|
:src="mediaGetThumbnail(lastSelected)"
|
||||||
mediaGetVariantUrl(
|
|
||||||
lastSelected,
|
|
||||||
'thumb',
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="max-h-20 max-w-20 mr-2" />
|
class="max-h-20 max-w-20 mr-2" />
|
||||||
<div class="flex flex-col">
|
<div class="flex flex-col">
|
||||||
<p class="m-0 text-bold">
|
<p class="m-0 text-bold">
|
||||||
@@ -343,13 +338,9 @@
|
|||||||
'media-selected-list-item',
|
'media-selected-list-item',
|
||||||
]"
|
]"
|
||||||
:style="{
|
:style="{
|
||||||
backgroundImage:
|
backgroundImage: `url('${mediaGetThumbnail(
|
||||||
item.url.length > 0
|
item,
|
||||||
? `url('${mediaGetVariantUrl(
|
)}')`,
|
||||||
item,
|
|
||||||
'thumb',
|
|
||||||
)}')`
|
|
||||||
: 'none',
|
|
||||||
backgroundColor:
|
backgroundColor:
|
||||||
item.status === 'OK'
|
item.status === 'OK'
|
||||||
? 'initial'
|
? 'initial'
|
||||||
@@ -443,7 +434,7 @@ import {
|
|||||||
MediaResponse,
|
MediaResponse,
|
||||||
} from "../../helpers/api.types";
|
} from "../../helpers/api.types";
|
||||||
import { useApplicationStore } from "../../store/ApplicationStore";
|
import { useApplicationStore } from "../../store/ApplicationStore";
|
||||||
import { mediaGetVariantUrl, mimeMatches } from "../../helpers/media";
|
import { mediaGetThumbnail, mimeMatches } from "../../helpers/media";
|
||||||
import SMInput from "../SMInput.vue";
|
import SMInput from "../SMInput.vue";
|
||||||
import SMLoading from "../SMLoading.vue";
|
import SMLoading from "../SMLoading.vue";
|
||||||
import SMTabGroup from "../SMTabGroup.vue";
|
import SMTabGroup from "../SMTabGroup.vue";
|
||||||
@@ -593,6 +584,13 @@ const getMediaItem = (item_id: string): Media | null => {
|
|||||||
return found;
|
return found;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const showMediaName = (media: Media): boolean => {
|
||||||
|
return !(
|
||||||
|
media.mime_type.startsWith("image/") ||
|
||||||
|
media.mime_type.startsWith("video/")
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handle user clicking the cancel/close button.
|
* Handle user clicking the cancel/close button.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -71,6 +71,7 @@ export interface Media {
|
|||||||
status: string;
|
status: string;
|
||||||
storage: string;
|
storage: string;
|
||||||
url: string;
|
url: string;
|
||||||
|
thumbnail: string;
|
||||||
description: string;
|
description: string;
|
||||||
dimensions: string;
|
dimensions: string;
|
||||||
variants: { [key: string]: string };
|
variants: { [key: string]: string };
|
||||||
|
|||||||
@@ -37,3 +37,15 @@ export const mimeMatches = (
|
|||||||
|
|
||||||
return regex.test(mimeToCheck);
|
return regex.test(mimeToCheck);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const mediaGetThumbnail = (media: Media): string => {
|
||||||
|
if (!media) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (media.thumbnail && media.thumbnail.length > 0) {
|
||||||
|
return media.thumbnail;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mediaGetVariantUrl(media, "thumb");
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user