Files
SkinbaseNova/app/Services/Studio/CreatorStudioAssetService.php

300 lines
13 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Services\Studio;
use App\Models\Artwork;
use App\Models\Collection;
use App\Models\NovaCardBackground;
use App\Models\Story;
use App\Models\User;
use App\Support\CoverUrl;
use Illuminate\Support\Collection as SupportCollection;
final class CreatorStudioAssetService
{
/**
* @param array<string, mixed> $filters
*/
public function library(User $user, array $filters = []): array
{
$type = $this->normalizeType((string) ($filters['type'] ?? 'all'));
$source = $this->normalizeSource((string) ($filters['source'] ?? 'all'));
$sort = $this->normalizeSort((string) ($filters['sort'] ?? 'recent'));
$query = trim((string) ($filters['q'] ?? ''));
$page = max(1, (int) ($filters['page'] ?? 1));
$perPage = min(max((int) ($filters['per_page'] ?? 18), 12), 36);
$items = $this->allAssets($user);
if ($type !== 'all') {
$items = $items->where('type', $type)->values();
}
if ($source !== 'all') {
$items = $items->where('source_key', $source)->values();
}
if ($query !== '') {
$needle = mb_strtolower($query);
$items = $items->filter(function (array $item) use ($needle): bool {
return collect([
$item['title'] ?? '',
$item['type_label'] ?? '',
$item['source_label'] ?? '',
$item['description'] ?? '',
])->contains(fn ($value): bool => is_string($value) && str_contains(mb_strtolower($value), $needle));
})->values();
}
$items = $this->sortAssets($items, $sort);
$total = $items->count();
$lastPage = max(1, (int) ceil($total / $perPage));
$page = min($page, $lastPage);
return [
'items' => $items->forPage($page, $perPage)->values()->all(),
'meta' => [
'current_page' => $page,
'last_page' => $lastPage,
'per_page' => $perPage,
'total' => $total,
],
'filters' => [
'type' => $type,
'source' => $source,
'sort' => $sort,
'q' => $query,
],
'type_options' => [
['value' => 'all', 'label' => 'All assets'],
['value' => 'card_backgrounds', 'label' => 'Card backgrounds'],
['value' => 'story_covers', 'label' => 'Story covers'],
['value' => 'collection_covers', 'label' => 'Collection covers'],
['value' => 'artwork_previews', 'label' => 'Artwork previews'],
['value' => 'profile_branding', 'label' => 'Profile branding'],
],
'source_options' => [
['value' => 'all', 'label' => 'All sources'],
['value' => 'cards', 'label' => 'Nova Cards'],
['value' => 'stories', 'label' => 'Stories'],
['value' => 'collections', 'label' => 'Collections'],
['value' => 'artworks', 'label' => 'Artworks'],
['value' => 'profile', 'label' => 'Profile'],
],
'sort_options' => [
['value' => 'recent', 'label' => 'Recently updated'],
['value' => 'usage', 'label' => 'Most reused'],
['value' => 'title', 'label' => 'Title A-Z'],
],
'summary' => $this->summary($items),
'highlights' => [
'recent_uploads' => $items->take(5)->values()->all(),
'most_used' => $items->sortByDesc(fn (array $item): int => (int) ($item['usage_count'] ?? 0))->take(5)->values()->all(),
],
];
}
private function allAssets(User $user): SupportCollection
{
return $this->cardBackgrounds($user)
->concat($this->storyCovers($user))
->concat($this->collectionCovers($user))
->concat($this->artworkPreviews($user))
->concat($this->profileBranding($user))
->sortByDesc(fn (array $item): int => strtotime((string) ($item['created_at'] ?? '')) ?: 0)
->values();
}
private function cardBackgrounds(User $user): SupportCollection
{
return NovaCardBackground::query()
->withCount('cards')
->where('user_id', $user->id)
->latest('created_at')
->limit(120)
->get()
->map(fn (NovaCardBackground $background): array => [
'id' => 'card-background:' . $background->id,
'numeric_id' => (int) $background->id,
'type' => 'card_backgrounds',
'type_label' => 'Card background',
'source_key' => 'cards',
'title' => 'Background #' . $background->id,
'description' => 'Reusable upload for Nova Card drafts and remixes.',
'image_url' => $background->processedUrl(),
'source_label' => 'Nova Cards',
'usage_count' => (int) ($background->cards_count ?? 0),
'usage_references' => [
['label' => 'Nova Cards editor', 'href' => route('studio.cards.create')],
['label' => 'Cards library', 'href' => route('studio.cards.index')],
],
'created_at' => $background->created_at?->toIso8601String(),
'manage_url' => route('studio.cards.index'),
'view_url' => route('studio.cards.create'),
]);
}
private function storyCovers(User $user): SupportCollection
{
return Story::query()
->where('creator_id', $user->id)
->whereNotNull('cover_image')
->latest('updated_at')
->limit(120)
->get()
->map(fn (Story $story): array => [
'id' => 'story-cover:' . $story->id,
'numeric_id' => (int) $story->id,
'type' => 'story_covers',
'type_label' => 'Story cover',
'source_key' => 'stories',
'title' => $story->title,
'description' => $story->excerpt ?: 'Cover art attached to a story draft or publication.',
'image_url' => $story->cover_url,
'source_label' => 'Stories',
'usage_count' => 1,
'usage_references' => [
['label' => 'Story editor', 'href' => route('creator.stories.edit', ['story' => $story->id])],
],
'created_at' => $story->updated_at?->toIso8601String(),
'manage_url' => route('creator.stories.edit', ['story' => $story->id]),
'view_url' => in_array($story->status, ['published', 'scheduled'], true)
? route('stories.show', ['slug' => $story->slug])
: route('creator.stories.preview', ['story' => $story->id]),
]);
}
private function collectionCovers(User $user): SupportCollection
{
return Collection::query()
->with('coverArtwork')
->where('user_id', $user->id)
->latest('updated_at')
->limit(120)
->get()
->filter(fn (Collection $collection): bool => $collection->coverArtwork !== null)
->map(fn (Collection $collection): array => [
'id' => 'collection-cover:' . $collection->id,
'numeric_id' => (int) $collection->id,
'type' => 'collection_covers',
'type_label' => 'Collection cover',
'source_key' => 'collections',
'title' => $collection->title,
'description' => $collection->summary ?: $collection->description ?: 'Cover artwork used for a collection shelf.',
'image_url' => $collection->coverArtwork?->thumbUrl('md'),
'source_label' => 'Collections',
'usage_count' => 1,
'usage_references' => [
['label' => 'Collection dashboard', 'href' => route('settings.collections.show', ['collection' => $collection->id])],
],
'created_at' => $collection->updated_at?->toIso8601String(),
'manage_url' => route('settings.collections.show', ['collection' => $collection->id]),
'view_url' => route('profile.collections.show', [
'username' => strtolower((string) $user->username),
'slug' => $collection->slug,
]),
]);
}
private function artworkPreviews(User $user): SupportCollection
{
return Artwork::query()
->where('user_id', $user->id)
->whereNull('deleted_at')
->latest('updated_at')
->limit(120)
->get()
->map(fn (Artwork $artwork): array => [
'id' => 'artwork-preview:' . $artwork->id,
'numeric_id' => (int) $artwork->id,
'type' => 'artwork_previews',
'type_label' => 'Artwork preview',
'source_key' => 'artworks',
'title' => $artwork->title ?: 'Untitled artwork',
'description' => $artwork->description ?: 'Reusable preview image from your artwork library.',
'image_url' => $artwork->thumbUrl('md'),
'source_label' => 'Artworks',
'usage_count' => 1,
'usage_references' => [
['label' => 'Artwork editor', 'href' => route('studio.artworks.edit', ['id' => $artwork->id])],
['label' => 'Artwork page', 'href' => $artwork->published_at ? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug]) : route('studio.artworks.edit', ['id' => $artwork->id])],
],
'created_at' => $artwork->updated_at?->toIso8601String(),
'manage_url' => route('studio.artworks.edit', ['id' => $artwork->id]),
'view_url' => $artwork->published_at
? route('art.show', ['id' => $artwork->id, 'slug' => $artwork->slug])
: route('studio.artworks.edit', ['id' => $artwork->id]),
]);
}
private function profileBranding(User $user): SupportCollection
{
$items = [];
if ($user->cover_hash && $user->cover_ext) {
$items[] = [
'id' => 'profile-cover:' . $user->id,
'numeric_id' => (int) $user->id,
'type' => 'profile_branding',
'type_label' => 'Profile banner',
'source_key' => 'profile',
'title' => 'Profile banner',
'description' => 'Banner image used on your public creator profile.',
'image_url' => CoverUrl::forUser($user->cover_hash, $user->cover_ext, time()),
'source_label' => 'Profile',
'usage_count' => 1,
'usage_references' => [
['label' => 'Studio profile', 'href' => route('studio.profile')],
['label' => 'Public profile', 'href' => '/@' . strtolower((string) $user->username)],
],
'created_at' => now()->toIso8601String(),
'manage_url' => route('studio.profile'),
'view_url' => '/@' . strtolower((string) $user->username),
];
}
return collect($items);
}
private function summary(SupportCollection $items): array
{
return [
['key' => 'card_backgrounds', 'label' => 'Card backgrounds', 'count' => $items->where('type', 'card_backgrounds')->count(), 'icon' => 'fa-solid fa-panorama'],
['key' => 'story_covers', 'label' => 'Story covers', 'count' => $items->where('type', 'story_covers')->count(), 'icon' => 'fa-solid fa-book-open-cover'],
['key' => 'collection_covers', 'label' => 'Collection covers', 'count' => $items->where('type', 'collection_covers')->count(), 'icon' => 'fa-solid fa-layer-group'],
['key' => 'artwork_previews', 'label' => 'Artwork previews', 'count' => $items->where('type', 'artwork_previews')->count(), 'icon' => 'fa-solid fa-image'],
['key' => 'profile_branding', 'label' => 'Profile branding', 'count' => $items->where('type', 'profile_branding')->count(), 'icon' => 'fa-solid fa-id-badge'],
];
}
private function normalizeType(string $type): string
{
$allowed = ['all', 'card_backgrounds', 'story_covers', 'collection_covers', 'artwork_previews', 'profile_branding'];
return in_array($type, $allowed, true) ? $type : 'all';
}
private function normalizeSource(string $source): string
{
$allowed = ['all', 'cards', 'stories', 'collections', 'artworks', 'profile'];
return in_array($source, $allowed, true) ? $source : 'all';
}
private function normalizeSort(string $sort): string
{
return in_array($sort, ['recent', 'usage', 'title'], true) ? $sort : 'recent';
}
private function sortAssets(SupportCollection $items, string $sort): SupportCollection
{
return match ($sort) {
'usage' => $items->sortByDesc(fn (array $item): int => (int) ($item['usage_count'] ?? 0))->values(),
'title' => $items->sortBy(fn (array $item): string => mb_strtolower((string) ($item['title'] ?? '')))->values(),
default => $items->sortByDesc(fn (array $item): int => strtotime((string) ($item['created_at'] ?? '')) ?: 0)->values(),
};
}
}