Restore toolbar background to bg-nebula; add toolbar backdrop blur
This commit is contained in:
@@ -6,11 +6,11 @@ class Banner
|
||||
{
|
||||
public static function ShowResponsiveAd()
|
||||
{
|
||||
echo '<div class="responsive_ad">';
|
||||
echo '<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>';
|
||||
echo '<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6457864535683080" data-ad-slot="9918154676" data-ad-format="auto"></ins>';
|
||||
echo '<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>';
|
||||
echo '</div>';
|
||||
#echo '<div class="responsive_ad">';
|
||||
#echo '<script async src="//pagead2.googlesyndication.com/pagead/js/adsbygoogle.js"></script>';
|
||||
#echo '<ins class="adsbygoogle" style="display:block" data-ad-client="ca-pub-6457864535683080" data-ad-slot="9918154676" data-ad-format="auto"></ins>';
|
||||
#echo '<script>(adsbygoogle = window.adsbygoogle || []).push({});</script>';
|
||||
#echo '</div>';
|
||||
}
|
||||
|
||||
public static function ShowBanner300x250()
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
],
|
||||
|
||||
@@ -14,6 +14,7 @@ use Illuminate\Queue\InteractsWithQueue;
|
||||
use Illuminate\Queue\SerializesModels;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Redis;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
@@ -166,7 +167,9 @@ final class AutoTagArtworkJob implements ShouldQueue
|
||||
->timeout(max(1, $timeout))
|
||||
->retry(max(0, $retries), max(0, $delay), throw: false)
|
||||
->post($url, [
|
||||
'url' => $imageUrl,
|
||||
'image_url' => $imageUrl,
|
||||
'limit' => 8,
|
||||
'artwork_id' => $this->artworkId,
|
||||
'hash' => $this->hash,
|
||||
]);
|
||||
@@ -182,6 +185,41 @@ final class AutoTagArtworkJob implements ShouldQueue
|
||||
|
||||
if (! $response->ok()) {
|
||||
Log::warning('CLIP analyze non-ok response', ['ref' => $ref, 'status' => $response->status(), 'body' => $this->safeBody($response->body())]);
|
||||
|
||||
// Fallback: try uploading the local derivative file to the gateway's file upload
|
||||
// endpoint (`/analyze/all/file`) if the gateway cannot fetch the public URL.
|
||||
try {
|
||||
$variant = (string) config('vision.image_variant', 'md');
|
||||
$row = DB::table('artwork_files')
|
||||
->where('artwork_id', $this->artworkId)
|
||||
->where('variant', $variant)
|
||||
->first();
|
||||
|
||||
if ($row && ! empty($row->path)) {
|
||||
$storageRoot = rtrim((string) config('uploads.storage_root', ''), DIRECTORY_SEPARATOR);
|
||||
$absolute = $storageRoot . DIRECTORY_SEPARATOR . str_replace(['/', '\\'], DIRECTORY_SEPARATOR, $row->path);
|
||||
if (is_file($absolute) && is_readable($absolute)) {
|
||||
$uploadUrl = rtrim($base, '/') . '/analyze/all/file';
|
||||
try {
|
||||
$attach = file_get_contents($absolute);
|
||||
if ($attach !== false) {
|
||||
$uploadResp = Http::attach('file', $attach, basename($absolute))
|
||||
->post($uploadUrl, ['limit' => 5]);
|
||||
|
||||
if ($uploadResp->ok()) {
|
||||
return $this->extractTagList($uploadResp->json());
|
||||
}
|
||||
Log::warning('CLIP upload fallback non-ok', ['ref' => $ref, 'status' => $uploadResp->status(), 'body' => $this->safeBody($uploadResp->body())]);
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('CLIP upload fallback failed', ['ref' => $ref, 'error' => $e->getMessage()]);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (\Throwable $e) {
|
||||
Log::warning('CLIP fallback check failed', ['ref' => $ref, 'error' => $e->getMessage()]);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
@@ -216,7 +254,9 @@ final class AutoTagArtworkJob implements ShouldQueue
|
||||
->timeout(max(1, $timeout))
|
||||
->retry(max(0, $retries), max(0, $delay), throw: false)
|
||||
->post($url, [
|
||||
'url' => $imageUrl,
|
||||
'image_url' => $imageUrl,
|
||||
'conf' => 0.25,
|
||||
'artwork_id' => $this->artworkId,
|
||||
'hash' => $this->hash,
|
||||
]);
|
||||
|
||||
@@ -22,7 +22,7 @@ final class ArtworkDraftService
|
||||
'slug' => $slug,
|
||||
'description' => $description,
|
||||
'file_name' => 'pending',
|
||||
'file_path' => 'pending',
|
||||
'file_path' => '',
|
||||
'file_size' => 0,
|
||||
'mime_type' => 'application/octet-stream',
|
||||
'width' => 1,
|
||||
|
||||
@@ -8,6 +8,7 @@ use App\DTOs\Uploads\UploadSessionData;
|
||||
use App\DTOs\Uploads\UploadInitResult;
|
||||
use App\DTOs\Uploads\UploadValidatedFile;
|
||||
use App\DTOs\Uploads\UploadScanResult;
|
||||
use App\Models\Artwork;
|
||||
use App\Repositories\Uploads\ArtworkFileRepository;
|
||||
use App\Repositories\Uploads\UploadSessionRepository;
|
||||
use Illuminate\Http\UploadedFile;
|
||||
@@ -121,6 +122,22 @@ final class UploadPipelineService
|
||||
$publicRelative[$variant] = $relativePath;
|
||||
}
|
||||
|
||||
$dimensions = @getimagesize($session->tempPath);
|
||||
$width = is_array($dimensions) && isset($dimensions[0]) ? (int) $dimensions[0] : 1;
|
||||
$height = is_array($dimensions) && isset($dimensions[1]) ? (int) $dimensions[1] : 1;
|
||||
|
||||
Artwork::query()->whereKey($artworkId)->update([
|
||||
'file_name' => basename($originalRelative),
|
||||
'file_path' => '',
|
||||
'file_size' => (int) filesize($originalPath),
|
||||
'mime_type' => 'image/webp',
|
||||
'hash' => $hash,
|
||||
'file_ext' => 'webp',
|
||||
'thumb_ext' => 'webp',
|
||||
'width' => max(1, $width),
|
||||
'height' => max(1, $height),
|
||||
]);
|
||||
|
||||
$this->sessions->updateStatus($sessionId, UploadSessionStatus::PROCESSED);
|
||||
$this->sessions->updateProgress($sessionId, 100);
|
||||
$this->audit->log($session->userId, 'upload_processed', $session->ip, [
|
||||
|
||||
Reference in New Issue
Block a user