335 lines
12 KiB
PHP
335 lines
12 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services;
|
|
|
|
use App\Models\Collection;
|
|
use App\Models\CollectionSave;
|
|
use App\Models\CollectionSavedNote;
|
|
use App\Models\CollectionSavedList;
|
|
use App\Models\CollectionSavedListItem;
|
|
use App\Models\User;
|
|
use Illuminate\Support\Collection as SupportCollection;
|
|
use Illuminate\Support\Facades\DB;
|
|
use Illuminate\Support\Str;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class CollectionSavedLibraryService
|
|
{
|
|
/**
|
|
* @param array<int, int> $collectionIds
|
|
* @return array<int, array{saved_because:?string,last_viewed_at:?string}>
|
|
*/
|
|
public function saveMetadataFor(User $user, array $collectionIds): array
|
|
{
|
|
if ($collectionIds === []) {
|
|
return [];
|
|
}
|
|
|
|
return CollectionSave::query()
|
|
->where('user_id', $user->id)
|
|
->whereIn('collection_id', $collectionIds)
|
|
->get(['collection_id', 'save_context', 'save_context_meta_json', 'last_viewed_at'])
|
|
->mapWithKeys(function (CollectionSave $save): array {
|
|
return [
|
|
(int) $save->collection_id => [
|
|
'saved_because' => $this->savedBecauseLabel($save),
|
|
'last_viewed_at' => $save->last_viewed_at?->toIso8601String(),
|
|
],
|
|
];
|
|
})
|
|
->all();
|
|
}
|
|
|
|
public function recentlyRevisited(User $user, int $limit = 6): SupportCollection
|
|
{
|
|
$savedIds = CollectionSave::query()
|
|
->where('user_id', $user->id)
|
|
->whereNotNull('last_viewed_at')
|
|
->orderByDesc('last_viewed_at')
|
|
->limit(max(1, min($limit, 12)))
|
|
->pluck('collection_id')
|
|
->map(static fn ($id): int => (int) $id)
|
|
->all();
|
|
|
|
if ($savedIds === []) {
|
|
return collect();
|
|
}
|
|
|
|
$collections = Collection::query()
|
|
->public()
|
|
->with([
|
|
'user:id,username,name',
|
|
'coverArtwork:id,user_id,title,slug,hash,thumb_ext,published_at,is_public,is_approved,deleted_at',
|
|
])
|
|
->whereIn('id', $savedIds)
|
|
->get()
|
|
->keyBy('id');
|
|
|
|
return collect($savedIds)
|
|
->map(fn (int $collectionId) => $collections->get($collectionId))
|
|
->filter()
|
|
->values();
|
|
}
|
|
|
|
public function listsFor(User $user): array
|
|
{
|
|
return CollectionSavedList::query()
|
|
->withCount('items')
|
|
->where('user_id', $user->id)
|
|
->orderBy('title')
|
|
->get()
|
|
->map(fn (CollectionSavedList $list) => [
|
|
'id' => (int) $list->id,
|
|
'title' => $list->title,
|
|
'slug' => $list->slug,
|
|
'items_count' => (int) $list->items_count,
|
|
])
|
|
->all();
|
|
}
|
|
|
|
public function findListBySlugForUser(User $user, string $slug): CollectionSavedList
|
|
{
|
|
return CollectionSavedList::query()
|
|
->withCount('items')
|
|
->where('user_id', $user->id)
|
|
->where('slug', $slug)
|
|
->firstOrFail();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $collectionIds
|
|
* @return array<int, array<int, int>>
|
|
*/
|
|
public function membershipsFor(User $user, array $collectionIds): array
|
|
{
|
|
if ($collectionIds === []) {
|
|
return [];
|
|
}
|
|
|
|
return DB::table('collection_saved_list_items as items')
|
|
->join('collection_saved_lists as lists', 'lists.id', '=', 'items.saved_list_id')
|
|
->where('lists.user_id', $user->id)
|
|
->whereIn('items.collection_id', $collectionIds)
|
|
->orderBy('items.saved_list_id')
|
|
->get(['items.collection_id', 'items.saved_list_id'])
|
|
->groupBy('collection_id')
|
|
->map(fn ($rows) => collect($rows)->pluck('saved_list_id')->map(static fn ($id) => (int) $id)->values()->all())
|
|
->mapWithKeys(fn ($listIds, $collectionId) => [(int) $collectionId => $listIds])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int> $collectionIds
|
|
* @return array<int, string>
|
|
*/
|
|
public function notesFor(User $user, array $collectionIds): array
|
|
{
|
|
if ($collectionIds === []) {
|
|
return [];
|
|
}
|
|
|
|
return CollectionSavedNote::query()
|
|
->where('user_id', $user->id)
|
|
->whereIn('collection_id', $collectionIds)
|
|
->pluck('note', 'collection_id')
|
|
->mapWithKeys(fn ($note, $collectionId) => [(int) $collectionId => (string) $note])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<int, int>
|
|
*/
|
|
public function collectionIdsForList(User $user, CollectionSavedList $list): array
|
|
{
|
|
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
|
|
|
return CollectionSavedListItem::query()
|
|
->where('saved_list_id', $list->id)
|
|
->orderBy('order_num')
|
|
->pluck('collection_id')
|
|
->map(static fn ($id) => (int) $id)
|
|
->all();
|
|
}
|
|
|
|
public function createList(User $user, string $title): CollectionSavedList
|
|
{
|
|
$slug = $this->uniqueSlug($user, $title);
|
|
|
|
return CollectionSavedList::query()->create([
|
|
'user_id' => $user->id,
|
|
'title' => $title,
|
|
'slug' => $slug,
|
|
]);
|
|
}
|
|
|
|
public function addToList(User $user, CollectionSavedList $list, Collection $collection): CollectionSavedListItem
|
|
{
|
|
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
|
|
|
$nextOrder = (int) (CollectionSavedListItem::query()->where('saved_list_id', $list->id)->max('order_num') ?? -1) + 1;
|
|
|
|
return CollectionSavedListItem::query()->firstOrCreate(
|
|
[
|
|
'saved_list_id' => $list->id,
|
|
'collection_id' => $collection->id,
|
|
],
|
|
[
|
|
'order_num' => $nextOrder,
|
|
'created_at' => now(),
|
|
]
|
|
);
|
|
}
|
|
|
|
public function removeFromList(User $user, CollectionSavedList $list, Collection $collection): bool
|
|
{
|
|
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
|
|
|
$deleted = CollectionSavedListItem::query()
|
|
->where('saved_list_id', $list->id)
|
|
->where('collection_id', $collection->id)
|
|
->delete();
|
|
|
|
if ($deleted > 0) {
|
|
$this->normalizeOrder($list);
|
|
}
|
|
|
|
return $deleted > 0;
|
|
}
|
|
|
|
/**
|
|
* @param array<int, int|string> $orderedCollectionIds
|
|
*/
|
|
public function reorderList(User $user, CollectionSavedList $list, array $orderedCollectionIds): void
|
|
{
|
|
abort_unless((int) $list->user_id === (int) $user->id, 403);
|
|
|
|
$normalizedIds = collect($orderedCollectionIds)
|
|
->map(static fn ($id) => (int) $id)
|
|
->filter(static fn (int $id) => $id > 0)
|
|
->values();
|
|
|
|
$currentIds = collect($this->collectionIdsForList($user, $list))->values();
|
|
|
|
if ($normalizedIds->count() !== $currentIds->count() || $normalizedIds->diff($currentIds)->isNotEmpty() || $currentIds->diff($normalizedIds)->isNotEmpty()) {
|
|
throw ValidationException::withMessages([
|
|
'collection_ids' => 'The submitted saved-list order is invalid.',
|
|
]);
|
|
}
|
|
|
|
DB::transaction(function () use ($list, $normalizedIds): void {
|
|
/** @var SupportCollection<int, int> $itemIds */
|
|
$itemIds = CollectionSavedListItem::query()
|
|
->where('saved_list_id', $list->id)
|
|
->whereIn('collection_id', $normalizedIds->all())
|
|
->pluck('id', 'collection_id')
|
|
->mapWithKeys(static fn ($id, $collectionId) => [(int) $collectionId => (int) $id]);
|
|
|
|
foreach ($normalizedIds as $index => $collectionId) {
|
|
$itemId = $itemIds->get($collectionId);
|
|
if (! $itemId) {
|
|
continue;
|
|
}
|
|
|
|
CollectionSavedListItem::query()
|
|
->whereKey($itemId)
|
|
->update(['order_num' => $index]);
|
|
}
|
|
});
|
|
}
|
|
|
|
public function itemsCount(CollectionSavedList $list): int
|
|
{
|
|
return (int) CollectionSavedListItem::query()
|
|
->where('saved_list_id', $list->id)
|
|
->count();
|
|
}
|
|
|
|
public function upsertNote(User $user, Collection $collection, ?string $note): ?CollectionSavedNote
|
|
{
|
|
$hasSavedCollection = DB::table('collection_saves')
|
|
->where('user_id', $user->id)
|
|
->where('collection_id', $collection->id)
|
|
->exists();
|
|
|
|
if (! $hasSavedCollection) {
|
|
throw ValidationException::withMessages([
|
|
'collection' => 'You can only add notes to collections saved in your library.',
|
|
]);
|
|
}
|
|
|
|
$normalizedNote = trim((string) ($note ?? ''));
|
|
|
|
if ($normalizedNote === '') {
|
|
CollectionSavedNote::query()
|
|
->where('user_id', $user->id)
|
|
->where('collection_id', $collection->id)
|
|
->delete();
|
|
|
|
return null;
|
|
}
|
|
|
|
return CollectionSavedNote::query()->updateOrCreate(
|
|
[
|
|
'user_id' => $user->id,
|
|
'collection_id' => $collection->id,
|
|
],
|
|
[
|
|
'note' => $normalizedNote,
|
|
]
|
|
);
|
|
}
|
|
|
|
private function uniqueSlug(User $user, string $title): string
|
|
{
|
|
$base = Str::slug(Str::limit($title, 80, '')) ?: 'saved-list';
|
|
$slug = $base;
|
|
$suffix = 2;
|
|
|
|
while (CollectionSavedList::query()->where('user_id', $user->id)->where('slug', $slug)->exists()) {
|
|
$slug = $base . '-' . $suffix;
|
|
$suffix++;
|
|
}
|
|
|
|
return $slug;
|
|
}
|
|
|
|
private function normalizeOrder(CollectionSavedList $list): void
|
|
{
|
|
$itemIds = CollectionSavedListItem::query()
|
|
->where('saved_list_id', $list->id)
|
|
->orderBy('order_num')
|
|
->orderBy('id')
|
|
->pluck('id');
|
|
|
|
foreach ($itemIds as $index => $itemId) {
|
|
CollectionSavedListItem::query()
|
|
->whereKey($itemId)
|
|
->update(['order_num' => $index]);
|
|
}
|
|
}
|
|
|
|
private function savedBecauseLabel(CollectionSave $save): ?string
|
|
{
|
|
$context = trim((string) ($save->save_context ?? ''));
|
|
$meta = is_array($save->save_context_meta_json) ? $save->save_context_meta_json : [];
|
|
|
|
return match ($context) {
|
|
'collection_detail' => 'Saved from the collection page',
|
|
'featured_collections' => 'Saved from featured collections',
|
|
'featured_landing' => 'Saved from featured collections',
|
|
'recommended_landing' => 'Saved from recommended collections',
|
|
'trending_landing' => 'Saved from trending collections',
|
|
'community_landing' => 'Saved from community collections',
|
|
'editorial_landing' => 'Saved from editorial collections',
|
|
'seasonal_landing' => 'Saved from seasonal collections',
|
|
'collection_search' => ! empty($meta['query']) ? sprintf('Saved from search for "%s"', (string) $meta['query']) : 'Saved from collection search',
|
|
'community_row', 'trending_row', 'editorial_row', 'seasonal_row', 'recent_row' => ! empty($meta['surface_label']) ? sprintf('Saved from %s', (string) $meta['surface_label']) : 'Saved from a collection rail',
|
|
'program_landing' => ! empty($meta['program_label']) ? sprintf('Saved from the %s program', (string) $meta['program_label']) : (! empty($meta['program_key']) ? sprintf('Saved from the %s program', (string) $meta['program_key']) : 'Saved from a program landing'),
|
|
'campaign_landing' => ! empty($meta['campaign_label']) ? sprintf('Saved during %s', (string) $meta['campaign_label']) : (! empty($meta['campaign_key']) ? sprintf('Saved during %s', (string) $meta['campaign_key']) : 'Saved from a campaign landing'),
|
|
default => $context !== '' ? str_replace('_', ' ', ucfirst($context)) : null,
|
|
};
|
|
}
|
|
}
|