authorize('viewAny', EnhanceJob::class); $jobs = EnhanceJob::query() ->where('user_id', (int) $request->user()->id) ->with('artwork:id,title,slug') ->latest('id') ->paginate(12) ->withQueryString() ->through(fn (EnhanceJob $job): array => $this->serializeJobListItem($job)); $latestCompleted = EnhanceJob::query() ->where('user_id', (int) $request->user()->id) ->where('status', EnhanceJob::STATUS_COMPLETED) ->latest('finished_at') ->limit(4) ->get() ->map(fn (EnhanceJob $job): array => $this->serializeJobListItem($job)) ->all(); return Inertia::render('Enhance/Index', [ 'title' => 'Skinbase Enhance', 'jobs' => $jobs, 'latestCompleted' => $latestCompleted, 'createUrl' => route('enhance.create'), 'indexUrl' => route('enhance.index'), 'dailyLimit' => (int) config('enhance.daily_limit', 10), 'enhanceConfig' => $this->enhanceService->frontendConfig(), ]); } public function create(Request $request): Response { $this->authorize('create', EnhanceJob::class); $selectedArtwork = null; if (($artworkId = (int) $request->integer('artwork')) > 0) { $artwork = Artwork::query() ->select(['id', 'user_id', 'title', 'slug']) ->findOrFail($artworkId); $actor = $request->user(); abort_unless($actor !== null, 403); $isOwner = (int) $artwork->user_id === (int) $actor->id; $isStaff = $actor->isAdmin() || $actor->isModerator(); abort_unless($isOwner || $isStaff, 403); $selectedArtwork = [ 'id' => $artwork->id, 'title' => $artwork->title, 'show_url' => route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]), 'store_url' => route('artworks.enhance.store', ['artwork' => $artwork->id]), ]; } return Inertia::render('Enhance/Create', [ 'title' => 'Skinbase Enhance', 'options' => $this->optionsPayload(), 'storeUrl' => route('enhance.store'), 'indexUrl' => route('enhance.index'), 'maxUploadMb' => (int) config('enhance.max_upload_mb', 20), 'selectedArtwork' => $selectedArtwork, 'enhanceConfig' => $this->enhanceService->frontendConfig(), ]); } public function store(Request $request): RedirectResponse { $this->authorize('create', EnhanceJob::class); $validated = $request->validate([ 'image' => ['required', 'file', 'mimetypes:image/jpeg,image/png,image/webp', 'max:' . ((int) config('enhance.max_upload_mb', 20) * 1024)], 'scale' => ['required', 'integer', Rule::in((array) config('enhance.allowed_scales', [2, 4]))], 'mode' => ['required', 'string', Rule::in((array) config('enhance.allowed_modes', ['standard', 'artwork', 'photo', 'illustration']))], ]); $job = $this->enhanceService->createFromUpload($request->user(), $request->file('image'), $validated); return redirect() ->route('enhance.show', ['enhanceJob' => $job]) ->with('success', 'Enhance job created.'); } public function show(EnhanceJob $enhanceJob): Response { $this->authorize('view', $enhanceJob); $enhanceJob->loadMissing('artwork:id,title,slug'); return Inertia::render('Enhance/Show', [ 'title' => 'Enhance Job', 'job' => $this->serializeJobDetail($enhanceJob), 'indexUrl' => route('enhance.index'), 'createUrl' => route('enhance.create'), 'enhanceConfig' => $this->enhanceService->frontendConfig(), ]); } public function retry(EnhanceJob $enhanceJob): RedirectResponse { $this->authorize('retry', $enhanceJob); $job = $this->enhanceService->retry($enhanceJob); return redirect() ->route('enhance.show', ['enhanceJob' => $job]) ->with('success', 'Enhance job queued again.'); } public function destroy(EnhanceJob $enhanceJob): RedirectResponse { $this->authorize('delete', $enhanceJob); $this->enhanceService->delete($enhanceJob); return redirect() ->route('enhance.index') ->with('success', 'Enhance job deleted.'); } private function optionsPayload(): array { return [ 'modes' => array_map(fn (string $mode): array => [ 'value' => $mode, 'label' => ucfirst($mode), ], (array) config('enhance.allowed_modes', [])), 'scales' => array_map(fn (int $scale): array => [ 'value' => $scale, 'label' => $scale . 'x', ], array_map('intval', (array) config('enhance.allowed_scales', []))), ]; } private function serializeJobListItem(EnhanceJob $job): array { return [ 'id' => $job->id, 'status' => (string) $job->status, 'engine' => (string) $job->engine, 'mode' => (string) $job->mode, 'scale' => (int) $job->scale, 'source_url' => $job->sourceUrl(), 'output_url' => $job->outputUrl(), 'preview_url' => $job->previewUrl(), 'input_width' => (int) ($job->input_width ?? 0), 'input_height' => (int) ($job->input_height ?? 0), 'output_width' => (int) ($job->output_width ?? 0), 'output_height' => (int) ($job->output_height ?? 0), 'error_message' => $job->error_message, 'processing_seconds' => $job->processing_seconds, 'created_at' => optional($job->created_at)?->toIso8601String(), 'finished_at' => optional($job->finished_at)?->toIso8601String(), 'show_url' => route('enhance.show', ['enhanceJob' => $job]), 'artwork' => $job->artwork ? [ 'id' => $job->artwork->id, 'title' => $job->artwork->title, 'slug' => $job->artwork->slug, 'url' => route('art.show', ['id' => $job->artwork->id, 'slug' => $job->artwork->slug]), ] : null, ]; } private function serializeJobDetail(EnhanceJob $job): array { return $this->serializeJobListItem($job) + [ 'input_filesize' => (int) ($job->input_filesize ?? 0), 'input_mime' => $job->input_mime, 'output_filesize' => (int) ($job->output_filesize ?? 0), 'output_mime' => $job->output_mime, 'metadata' => $job->metadata ?? [], 'queued_at' => optional($job->queued_at)?->toIso8601String(), 'started_at' => optional($job->started_at)?->toIso8601String(), 'deleted_at' => optional($job->deleted_at)?->toIso8601String(), 'expires_at' => optional($job->expires_at)?->toIso8601String(), 'retry_url' => route('enhance.retry', ['enhanceJob' => $job]), 'delete_url' => route('enhance.destroy', ['enhanceJob' => $job]), 'download_url' => $job->outputUrl(), 'can_retry' => auth()->user()?->can('retry', $job) ?? false, 'can_delete' => auth()->user()?->can('delete', $job) ?? false, ]; } }