Restore toolbar background to bg-nebula; add toolbar backdrop blur

This commit is contained in:
2026-02-15 09:24:43 +01:00
parent 79192345e3
commit 9dbe848412
28 changed files with 736 additions and 110 deletions

View File

@@ -11,8 +11,10 @@ use App\Models\Artwork;
use App\Models\Tag;
use App\Services\TagService;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Symfony\Component\HttpFoundation\Response;
use App\Jobs\AutoTagArtworkJob;
final class ArtworkTagController extends Controller
{
@@ -21,6 +23,78 @@ final class ArtworkTagController extends Controller
) {
}
public function index(int $id): JsonResponse
{
$artwork = Artwork::query()->findOrFail($id);
$this->authorizeOrNotFound(request()->user(), $artwork);
$queueConnection = (string) config('queue.default', 'sync');
$visionEnabled = (bool) config('vision.enabled', true);
$queuedCount = 0;
$failedCount = 0;
if (in_array($queueConnection, ['database', 'redis'], true)) {
try {
$queuedCount = (int) DB::table('jobs')
->where('payload', 'like', '%AutoTagArtworkJob%')
->where('payload', 'like', '%' . $artwork->id . '%')
->count();
} catch (\Throwable) {
$queuedCount = 0;
}
try {
$failedCount = (int) DB::table('failed_jobs')
->where('payload', 'like', '%AutoTagArtworkJob%')
->where('payload', 'like', '%' . $artwork->id . '%')
->count();
} catch (\Throwable) {
$failedCount = 0;
}
}
$triggered = false;
$shouldTrigger = request()->boolean('trigger', false);
if ($shouldTrigger && $visionEnabled && ! empty($artwork->hash) && $queuedCount === 0) {
AutoTagArtworkJob::dispatch((int) $artwork->id, (string) $artwork->hash);
$triggered = true;
$queuedCount = max(1, $queuedCount);
}
$tags = $artwork->tags()
->select('tags.id', 'tags.name', 'tags.slug')
->withPivot(['source', 'confidence'])
->orderByDesc('artwork_tag.confidence')
->get()
->map(static function ($tag): array {
$source = (string) ($tag->pivot->source ?? 'manual');
return [
'id' => (int) $tag->id,
'name' => (string) $tag->name,
'slug' => (string) $tag->slug,
'source' => $source,
'confidence' => (float) ($tag->pivot->confidence ?? 0),
'is_ai' => $source === 'ai',
];
})
->values();
return response()->json([
'vision_enabled' => $visionEnabled,
'tags' => $tags,
'ai_tags' => $tags->where('is_ai', true)->values(),
'debug' => [
'queue_connection' => $queueConnection,
'queued_jobs' => $queuedCount,
'failed_jobs' => $failedCount,
'triggered' => $triggered,
'ai_tag_count' => (int) $tags->where('is_ai', true)->count(),
'total_tag_count' => (int) $tags->count(),
],
]);
}
public function store(int $id, ArtworkTagsStoreRequest $request): JsonResponse
{
$artwork = Artwork::query()->findOrFail($id);

View File

@@ -37,6 +37,8 @@ use App\Uploads\Exceptions\UploadPublishValidationException;
use App\Uploads\Services\ArchiveInspectorService;
use App\Uploads\Services\DraftQuotaService;
use App\Uploads\Exceptions\DraftQuotaException;
use App\Models\Artwork;
use Illuminate\Support\Str;
final class UploadController extends Controller
{
@@ -101,15 +103,13 @@ final class UploadController extends Controller
}
try {
$previewPath = null;
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId, &$previewPath) {
$status = DB::transaction(function () use ($pipeline, $sessionId, $validated, $artworkId) {
if ((bool) config('uploads.queue_derivatives', false)) {
GenerateDerivativesJob::dispatch($sessionId, $validated->hash, $artworkId)->afterCommit();
return 'queued';
}
$result = $pipeline->processAndPublish($sessionId, $validated->hash, $artworkId);
$previewPath = $result['public']['md'] ?? $result['public']['lg'] ?? null;
$pipeline->processAndPublish($sessionId, $validated->hash, $artworkId);
// Derivatives are available now; dispatch AI auto-tagging.
AutoTagArtworkJob::dispatch($artworkId, $validated->hash)->afterCommit();
@@ -127,7 +127,6 @@ final class UploadController extends Controller
return response()->json([
'artwork_id' => $artworkId,
'status' => $status,
'preview_path' => $previewPath,
], Response::HTTP_OK);
} catch (Throwable $e) {
Log::error('Upload finish failed', [
@@ -476,6 +475,58 @@ final class UploadController extends Controller
{
$user = $request->user();
$validated = $request->validate([
'title' => ['nullable', 'string', 'max:150'],
'description' => ['nullable', 'string'],
]);
if (ctype_digit($id)) {
$artworkId = (int) $id;
$artwork = Artwork::query()->find($artworkId);
if (! $artwork) {
return response()->json(['message' => 'Artwork not found.'], Response::HTTP_NOT_FOUND);
}
if ((int) $artwork->user_id !== (int) $user->id) {
return response()->json(['message' => 'Forbidden.'], Response::HTTP_FORBIDDEN);
}
$title = trim((string) ($validated['title'] ?? $artwork->title ?? ''));
if ($title === '') {
$title = 'Untitled artwork';
}
$slugBase = Str::slug($title);
if ($slugBase === '') {
$slugBase = 'artwork';
}
$slug = $slugBase;
$suffix = 2;
while (Artwork::query()->where('slug', $slug)->where('id', '!=', $artwork->id)->exists()) {
$slug = $slugBase . '-' . $suffix;
$suffix++;
}
$artwork->title = $title;
if (array_key_exists('description', $validated)) {
$artwork->description = $validated['description'];
}
$artwork->slug = $slug;
$artwork->is_public = true;
$artwork->is_approved = true;
$artwork->published_at = now();
$artwork->save();
return response()->json([
'success' => true,
'artwork_id' => (int) $artwork->id,
'status' => 'published',
'slug' => (string) $artwork->slug,
'published_at' => optional($artwork->published_at)->toISOString(),
], Response::HTTP_OK);
}
try {
$upload = $publishService->publish($id, $user);
@@ -484,7 +535,6 @@ final class UploadController extends Controller
'upload_id' => (string) $upload->id,
'status' => (string) $upload->status,
'published_at' => optional($upload->published_at)->toISOString(),
'final_path' => (string) $upload->final_path,
], Response::HTTP_OK);
} catch (UploadOwnershipException $e) {
return response()->json(['message' => $e->getMessage()], Response::HTTP_FORBIDDEN);

View File

@@ -2,8 +2,8 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Resources\MissingValue;
use App\Services\ThumbnailService;
class ArtworkListResource extends JsonResource
{
@@ -48,6 +48,8 @@ class ArtworkListResource extends JsonResource
$categoryPath = $primaryCategory->full_slug_path ?? null;
}
$slugVal = $get('slug');
$hash = (string) ($get('hash') ?? '');
$thumbExt = (string) ($get('thumb_ext') ?? '');
$webUrl = $contentTypeSlug && $categoryPath && $slugVal
? '/' . strtolower($contentTypeSlug) . '/' . strtolower($categoryPath) . '/' . $slugVal
: null;
@@ -60,7 +62,7 @@ class ArtworkListResource extends JsonResource
'width' => $get('width'),
'height' => $get('height'),
],
'thumbnail_url' => $this->when(! empty($get('file_path')), fn() => Storage::url($get('file_path'))),
'thumbnail_url' => $this->when(! empty($hash) && ! empty($thumbExt), fn() => ThumbnailService::fromHash($hash, $thumbExt, 'md')),
'author' => $this->whenLoaded('user', function () {
return [
'name' => $this->user->name ?? null,

View File

@@ -2,7 +2,6 @@
namespace App\Http\Resources;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Support\Facades\Storage;
use Illuminate\Http\Resources\MissingValue;
class ArtworkResource extends JsonResource
@@ -32,6 +31,21 @@ class ArtworkResource extends JsonResource
}
return null;
};
$hash = (string) ($get('hash') ?? '');
$fileExt = (string) ($get('file_ext') ?? '');
$filesBase = rtrim((string) config('cdn.files_url', ''), '/');
$buildOriginalUrl = static function (string $hashValue, string $extValue) use ($filesBase): ?string {
$normalizedHash = strtolower((string) preg_replace('/[^a-f0-9]/', '', $hashValue));
$normalizedExt = strtolower((string) preg_replace('/[^a-z0-9]/', '', $extValue));
if ($normalizedHash === '' || $normalizedExt === '') return null;
$h1 = substr($normalizedHash, 0, 2);
$h2 = substr($normalizedHash, 2, 2);
if ($h1 === '' || $h2 === '' || $filesBase === '') return null;
return sprintf('%s/originals/%s/%s/%s.%s', $filesBase, $h1, $h2, $normalizedHash, $normalizedExt);
};
return [
'slug' => $get('slug'),
'title' => $get('title'),
@@ -39,10 +53,10 @@ class ArtworkResource extends JsonResource
'width' => $get('width'),
'height' => $get('height'),
// File URLs: produce public URLs without exposing internal file_path
// File URLs are derived from hash/ext (no DB path dependency)
'file' => [
'name' => $get('file_name') ?? null,
'url' => $this->when(! empty($get('file_path')), fn() => Storage::url($get('file_path'))),
'url' => $this->when(! empty($hash) && ! empty($fileExt), fn() => $buildOriginalUrl($hash, $fileExt)),
'size' => $get('file_size') ?? null,
'mime_type' => $get('mime_type') ?? null,
],