$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(), }; } }