300 lines
13 KiB
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(),
|
|
};
|
|
}
|
|
} |