414 lines
18 KiB
PHP
414 lines
18 KiB
PHP
<?php
|
|
namespace App\Http\Resources;
|
|
|
|
use App\Models\WorldRelation;
|
|
use App\Models\WorldSubmission;
|
|
use App\Services\ArtworkEvolutionService;
|
|
use App\Services\ContentSanitizer;
|
|
use App\Services\Maturity\ArtworkMaturityService;
|
|
use App\Services\ThumbnailPresenter;
|
|
use Illuminate\Http\Resources\Json\JsonResource;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Facades\Schema;
|
|
|
|
class ArtworkResource extends JsonResource
|
|
{
|
|
/**
|
|
* Transform the resource into an array.
|
|
*/
|
|
public function toArray($request): array
|
|
{
|
|
$this->resource->loadMissing(['group', 'uploadedBy.profile', 'primaryAuthor.profile', 'contributors.user.profile', 'worldSubmissions.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,
|
|
];
|
|
})
|
|
);
|
|
}
|
|
|
|
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';
|
|
}
|
|
}
|