resource->loadMissing(['group', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'worldSubmissions.world', 'worldRewardGrants.world']); $md = ThumbnailPresenter::present($this->resource, 'md'); $lg = ThumbnailPresenter::present($this->resource, 'lg'); $xl = ThumbnailPresenter::present($this->resource, 'xl'); $sq = ThumbnailPresenter::present($this->resource, 'sq'); $screenshots = $this->resolveScreenshotAssets(); $canonicalSlug = \Illuminate\Support\Str::slug((string) ($this->slug ?: $this->title)); if ($canonicalSlug === '') { $canonicalSlug = (string) $this->id; } $followerCount = 0; if (!empty($this->user?->id)) { if (Schema::hasTable('user_statistics')) { $followerCount = (int) DB::table('user_statistics') ->where('user_id', (int) $this->user->id) ->value('followers_count'); } // Legacy fallback for environments where new tables are unavailable. if (($followerCount <= 0) && Schema::hasTable('friends_list')) { $followerCount = (int) DB::table('friends_list') ->where('friend_id', (int) $this->user->id) ->count(); } } $viewerId = (int) optional($request->user())->id; $isLiked = false; $isFavorited = false; $isBookmarked = false; $isFollowing = false; $isFollowingGroup = false; $viewerAward = null; $isOwner = $viewerId > 0 && $viewerId === (int) ($this->user?->id ?? 0); $bookmarksCount = Schema::hasTable('artwork_bookmarks') ? (int) DB::table('artwork_bookmarks')->where('artwork_id', (int) $this->id)->count() : 0; if ($viewerId > 0) { if (Schema::hasTable('artwork_likes')) { $isLiked = DB::table('artwork_likes') ->where('user_id', $viewerId) ->where('artwork_id', (int) $this->id) ->exists(); } if (Schema::hasTable('artwork_bookmarks')) { $isBookmarked = DB::table('artwork_bookmarks') ->where('user_id', $viewerId) ->where('artwork_id', (int) $this->id) ->exists(); } $isFavorited = DB::table('artwork_favourites') ->where('user_id', $viewerId) ->where('artwork_id', (int) $this->id) ->exists(); if (!empty($this->user?->id)) { if (Schema::hasTable('user_followers')) { $isFollowing = DB::table('user_followers') ->where('user_id', (int) $this->user->id) ->where('follower_id', $viewerId) ->exists(); } elseif (Schema::hasTable('friends_list')) { // Legacy fallback only. $isFollowing = DB::table('friends_list') ->where('user_id', $viewerId) ->where('friend_id', (int) $this->user->id) ->exists(); } } if (!empty($this->group?->id) && Schema::hasTable('group_follows')) { $isFollowingGroup = DB::table('group_follows') ->where('group_id', (int) $this->group->id) ->where('user_id', $viewerId) ->exists(); } if (Schema::hasTable('artwork_medals')) { $viewerAward = DB::table('artwork_medals') ->where('user_id', $viewerId) ->where('artwork_id', (int) $this->id) ->value('medal_type'); } } $decode = static fn (?string $v): string => html_entity_decode((string) ($v ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'); $mapUser = static function ($user): ?array { if (! $user) { return null; } return [ 'id' => (int) ($user->id ?? 0), 'name' => html_entity_decode((string) ($user->name ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'username' => (string) ($user->username ?? ''), 'profile_url' => ! empty($user->username) ? '/@' . $user->username : null, 'avatar_url' => $user->profile?->avatar_url, ]; }; $publisher = $this->group ? [ 'type' => 'group', 'id' => (int) $this->group->id, 'name' => (string) $this->group->name, 'slug' => (string) $this->group->slug, 'headline' => (string) ($this->group->headline ?? ''), 'avatar_url' => $this->group->avatarUrl(), 'profile_url' => $this->group->publicUrl(), 'followers_count' => (int) ($this->group->followers_count ?? 0), 'follow_url' => route('groups.follow', ['group' => $this->group]), 'unfollow_url' => route('groups.unfollow', ['group' => $this->group]), ] : [ 'type' => 'user', 'id' => (int) ($this->user?->id ?? 0), 'name' => html_entity_decode((string) ($this->user?->name ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'slug' => (string) ($this->user?->username ?? ''), 'headline' => '', 'avatar_url' => $this->user?->profile?->avatar_url, 'profile_url' => $this->user?->username ? '/@' . $this->user->username : null, 'followers_count' => $followerCount, 'follow_url' => null, 'unfollow_url' => null, ]; $primaryAuthor = $mapUser($this->primaryAuthor ?: $this->user); $uploadedBy = $mapUser($this->uploadedBy ?: $this->user); $contributors = $this->contributors ->map(function ($contributor) use ($mapUser): ?array { $user = $mapUser($contributor->user); if (! $user) { return null; } return array_merge($user, [ 'credit_role' => $contributor->credit_role, 'is_primary' => (bool) $contributor->is_primary, ]); }) ->filter() ->values(); return [ 'id' => (int) $this->id, 'slug' => (string) $this->slug, 'title' => $decode($this->title), 'description' => $decode($this->description), 'description_html' => $this->renderDescriptionHtml(), 'dimensions' => [ 'width' => (int) ($this->width ?? 0), 'height' => (int) ($this->height ?? 0), ], 'published_at' => optional($this->published_at)->toIsoString(), 'canonical_url' => route('art.show', ['id' => (int) $this->id, 'slug' => $canonicalSlug]), 'thumbs' => [ 'md' => $md, 'lg' => $lg, 'xl' => $xl, 'sq' => $sq, ], 'file' => [ 'url' => $lg['url'] ?? null, 'srcset' => ThumbnailPresenter::srcsetForArtwork($this->resource), 'mime_type' => 'image/webp', ], 'screenshots' => $screenshots, 'user' => [ 'id' => (int) ($this->user?->id ?? 0), 'name' => html_entity_decode((string) ($this->user?->name ?? ''), ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'username' => (string) ($this->user?->username ?? ''), 'profile_url' => $this->user?->username ? '/@' . $this->user->username : null, 'avatar_url' => $this->user?->profile?->avatar_url, 'level' => (int) ($this->user?->level ?? 1), 'rank' => (string) ($this->user?->rank ?? 'Newbie'), 'followers_count' => $followerCount, ], 'publisher' => $publisher, 'credits' => [ 'uploaded_by' => $uploadedBy, 'primary_author' => $primaryAuthor, 'contributors' => $contributors, ], 'viewer' => [ 'is_bookmarked' => $isBookmarked, 'is_liked' => $isLiked, 'is_favorited' => $isFavorited, 'is_following_author' => $isFollowing, 'is_following_group' => $isFollowingGroup, 'is_following_publisher' => $publisher['type'] === 'group' ? $isFollowingGroup : $isFollowing, 'is_authenticated' => $viewerId > 0, 'is_owner' => $isOwner, 'id' => $viewerId > 0 ? $viewerId : null, ], 'management' => [ 'analytics_url' => $isOwner ? route('studio.artworks.analytics', ['id' => (int) $this->id]) : null, ], 'stats' => [ 'bookmarks' => $bookmarksCount, 'views' => (int) ($this->stats?->views ?? 0), 'downloads' => (int) ($this->stats?->downloads ?? 0), 'favorites' => (int) ($this->stats?->favorites ?? 0), 'likes' => (int) ($this->stats?->rating_count ?? 0), ], 'awards' => [ 'gold' => (int) ($this->awardStat?->gold_count ?? 0), 'silver' => (int) ($this->awardStat?->silver_count ?? 0), 'bronze' => (int) ($this->awardStat?->bronze_count ?? 0), 'score' => (int) ($this->awardStat?->score_total ?? 0), 'score_7d' => (int) ($this->awardStat?->score_7d ?? 0), 'score_30d' => (int) ($this->awardStat?->score_30d ?? 0), 'last_medaled_at' => optional($this->awardStat?->last_medaled_at)->toIsoString(), 'viewer_award' => $viewerAward, ], 'medals' => [ 'gold' => (int) ($this->awardStat?->gold_count ?? 0), 'silver' => (int) ($this->awardStat?->silver_count ?? 0), 'bronze' => (int) ($this->awardStat?->bronze_count ?? 0), 'score' => (int) ($this->awardStat?->score_total ?? 0), 'score_7d' => (int) ($this->awardStat?->score_7d ?? 0), 'score_30d' => (int) ($this->awardStat?->score_30d ?? 0), 'last_medaled_at' => optional($this->awardStat?->last_medaled_at)->toIsoString(), 'current_user_medal' => $viewerAward, 'viewer_award' => $viewerAward, ], 'maturity' => app(ArtworkMaturityService::class)->presentation($this->resource, $request->user()), 'evolution' => app(ArtworkEvolutionService::class)->publicPayload($this->resource, $request->user()), 'world_participation' => $this->resolveWorldParticipation(), 'categories' => $this->categories->map(fn ($category) => [ 'id' => (int) $category->id, 'slug' => (string) $category->slug, 'name' => html_entity_decode((string) $category->name, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'content_type_slug' => (string) ($category->contentType?->slug ?? ''), 'url' => $category->contentType ? $category->url : null, 'parent' => $category->parent ? [ 'id' => (int) $category->parent->id, 'slug' => (string) $category->parent->slug, 'name' => html_entity_decode((string) $category->parent->name, ENT_QUOTES | ENT_HTML5, 'UTF-8'), 'content_type_slug' => (string) ($category->parent->contentType?->slug ?? ''), 'url' => $category->parent->contentType ? $category->parent->url : null, ] : null, ])->values(), 'tags' => $this->tags->map(fn ($tag) => [ 'id' => (int) $tag->id, 'slug' => (string) $tag->slug, 'name' => html_entity_decode((string) $tag->name, ENT_QUOTES | ENT_HTML5, 'UTF-8'), ])->values(), ]; } private function resolveScreenshotAssets(): array { if (! Schema::hasTable('artwork_files')) { return []; } return DB::table('artwork_files') ->where('artwork_id', (int) $this->id) ->where('variant', 'like', 'shot%') ->orderBy('variant') ->get(['variant', 'path', 'mime', 'size']) ->map(function ($row, int $index): array { $path = (string) ($row->path ?? ''); $url = $this->objectUrl($path); return [ 'id' => (string) ($row->variant ?? ('shot' . ($index + 1))), 'variant' => (string) ($row->variant ?? ''), 'label' => 'Screenshot ' . ($index + 1), 'url' => $url, 'thumb_url' => $url, 'mime_type' => (string) ($row->mime ?? 'image/jpeg'), 'size' => (int) ($row->size ?? 0), ]; }) ->filter(fn (array $item): bool => $item['url'] !== null) ->values() ->all(); } private function objectUrl(string $path): ?string { $trimmedPath = trim($path, '/'); if ($trimmedPath === '') { return null; } $base = rtrim((string) config('cdn.files_url', 'https://files.skinbase.org'), '/'); return $base . '/' . $trimmedPath; } private function renderDescriptionHtml(): string { $rawDescription = (string) ($this->description ?? ''); if (trim($rawDescription) === '') { return ''; } if (! $this->authorCanPublishLinks()) { return nl2br(e(ContentSanitizer::stripToPlain($rawDescription))); } return ContentSanitizer::render($rawDescription); } private function resolveWorldParticipation(): array { $items = collect(); if (Schema::hasTable('world_relations') && Schema::hasTable('worlds')) { $items = $items->concat( WorldRelation::query() ->with('world') ->where('related_type', WorldRelation::TYPE_ARTWORK) ->where('related_id', (int) $this->id) ->get() ->filter(fn (WorldRelation $relation): bool => $relation->world !== null && $relation->world->isPubliclyVisible()) ->map(function (WorldRelation $relation): array { $world = $relation->world; return [ 'world_id' => (int) $relation->world_id, 'world_title' => (string) $world->title, 'world_slug' => (string) $world->slug, 'world_url' => $world->publicUrl(), 'badge_label' => 'Part of ' . $world->title, 'status' => 'curated', 'status_label' => 'Curated', 'tone' => 'curated', 'sort_priority' => 1, ]; }) ); } if (Schema::hasTable('world_submissions')) { $items = $items->concat( $this->worldSubmissions ->filter(function (WorldSubmission $submission): bool { return (string) $submission->status === WorldSubmission::STATUS_LIVE && $submission->world !== null && $submission->world->isPubliclyVisible(); }) ->map(function (WorldSubmission $submission): array { $world = $submission->world; $isFeatured = (bool) $submission->is_featured; return [ 'world_id' => (int) $submission->world_id, 'world_title' => (string) $world->title, 'world_slug' => (string) $world->slug, 'world_url' => $world->publicUrl(), 'badge_label' => ($isFeatured ? 'Featured in ' : 'Part of ') . $world->title, 'status' => (string) $submission->status, 'status_label' => $isFeatured ? 'Featured' : 'Community submission', 'tone' => $isFeatured ? 'featured' : 'community', 'sort_priority' => $isFeatured ? 0 : 2, ]; }) ); } if (Schema::hasTable('world_reward_grants')) { $items = $items->concat(app(WorldRewardService::class)->artworkRewardBadges($this->resource)); } return $items ->sortBy('sort_priority') ->groupBy('world_id') ->map(function ($group): array { $item = $group->first(); unset($item['sort_priority']); return $item; }) ->sortBy(fn (array $item): string => strtolower((string) $item['world_title'])) ->values() ->all(); } private function authorCanPublishLinks(): bool { $level = (int) ($this->user?->level ?? 1); $rank = strtolower((string) ($this->user?->rank ?? 'Newbie')); return $level > 1 && $rank !== 'newbie'; } }