Update playlist sidebar to use livewire
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
<div
|
||||
x-data="{
|
||||
total: {{ $total }},
|
||||
windowStart: {{ $windowStart }},
|
||||
windowEnd: {{ $windowEnd }},
|
||||
rowHeight: 80,
|
||||
collapsible: @json($collapsible),
|
||||
open: ! @json($collapsible),
|
||||
loadingUp: false,
|
||||
loadingDown: false,
|
||||
rafPending: false,
|
||||
init() {
|
||||
this.$watch('open', (value) => {
|
||||
if (value) {
|
||||
setTimeout(() => this.scrollActiveIntoView(), 250);
|
||||
}
|
||||
});
|
||||
|
||||
if (! this.collapsible) {
|
||||
this.$nextTick(() => this.scrollActiveIntoView());
|
||||
}
|
||||
},
|
||||
get scroller() {
|
||||
return this.$refs.scroller;
|
||||
},
|
||||
onScroll() {
|
||||
if (this.rafPending) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.rafPending = true;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
this.rafPending = false;
|
||||
this.evaluateScroll();
|
||||
});
|
||||
},
|
||||
evaluateScroll() {
|
||||
const scroller = this.scroller;
|
||||
|
||||
if (! scroller || scroller.clientHeight === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const threshold = this.rowHeight * 2;
|
||||
|
||||
if (scroller.scrollTop <= threshold) {
|
||||
this.loadUp();
|
||||
}
|
||||
|
||||
if (scroller.scrollTop + scroller.clientHeight >= scroller.scrollHeight - threshold) {
|
||||
this.loadDown();
|
||||
}
|
||||
},
|
||||
async loadUp() {
|
||||
if (this.loadingUp || this.windowStart <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingUp = true;
|
||||
const before = this.windowStart;
|
||||
|
||||
try {
|
||||
await $wire.prependChunk();
|
||||
this.syncWindow();
|
||||
|
||||
const added = before - this.windowStart;
|
||||
|
||||
if (added > 0) {
|
||||
this.scroller.scrollTop += added * this.rowHeight;
|
||||
}
|
||||
} finally {
|
||||
this.loadingUp = false;
|
||||
}
|
||||
},
|
||||
async loadDown() {
|
||||
if (this.loadingDown || this.windowEnd >= this.total) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.loadingDown = true;
|
||||
const before = this.windowStart;
|
||||
|
||||
try {
|
||||
await $wire.appendChunk();
|
||||
this.syncWindow();
|
||||
|
||||
const trimmedFromTop = this.windowStart - before;
|
||||
|
||||
if (trimmedFromTop > 0) {
|
||||
this.scroller.scrollTop -= trimmedFromTop * this.rowHeight;
|
||||
}
|
||||
} finally {
|
||||
this.loadingDown = false;
|
||||
}
|
||||
},
|
||||
syncWindow() {
|
||||
const data = this.$el.dataset;
|
||||
|
||||
this.total = parseInt(data.total, 10);
|
||||
this.windowStart = parseInt(data.windowStart, 10);
|
||||
this.windowEnd = parseInt(data.windowEnd, 10);
|
||||
},
|
||||
scrollActiveIntoView() {
|
||||
const scroller = this.scroller;
|
||||
const active = scroller ? scroller.querySelector('[data-active-row]') : null;
|
||||
|
||||
if (! scroller || ! active) {
|
||||
return;
|
||||
}
|
||||
|
||||
scroller.scrollTop = active.offsetTop - scroller.clientHeight / 2 + this.rowHeight / 2;
|
||||
},
|
||||
}"
|
||||
data-total="{{ $total }}"
|
||||
data-window-start="{{ $windowStart }}"
|
||||
data-window-end="{{ $windowEnd }}"
|
||||
data-active-playlist-episode-id="{{ $currentPlaylistEpisodeId ?? 'null' }}"
|
||||
>
|
||||
<input id="playlist_id" type="hidden" value="{{ $playlist->id }}">
|
||||
<input id="playlist_next_episode_slug" type="hidden" value="{{ $nextEpisodeSlug }}">
|
||||
|
||||
<div class="xl:sticky xl:top-[80px] xl:w-[420px]">
|
||||
<div class="overflow-hidden rounded-2xl border border-neutral-200/70 bg-white shadow-sm dark:border-neutral-800 dark:bg-neutral-950">
|
||||
|
||||
@if ($collapsible)
|
||||
<button type="button" @click="open = ! open" :aria-expanded="open ? 'true' : 'false'"
|
||||
class="flex w-full items-center justify-between gap-3 px-4 py-3 text-neutral-900 transition hover:bg-neutral-50 dark:text-white dark:hover:bg-neutral-900">
|
||||
<span class="flex items-center gap-2 font-semibold">
|
||||
<i class="fa-solid fa-list text-rose-600"></i>
|
||||
{{ __('playlist.playlist') }} · {{ $total }} {{ __('playlist.episodes') }}
|
||||
</span>
|
||||
<i class="fa-solid fa-chevron-down text-neutral-400 transition-transform" :class="{ 'rotate-180': open }"></i>
|
||||
</button>
|
||||
|
||||
<div x-show="open" x-collapse.duration.200ms>
|
||||
@endif
|
||||
|
||||
<div class="p-4">
|
||||
<a href="{{ $playlist->is_private ? route('profile.playlist.show', $playlist->id) : route('playlist.show', $playlist->id) }}"
|
||||
class="flex min-w-0 items-center gap-2 text-neutral-900 transition hover:text-rose-500 dark:text-white dark:hover:text-rose-400">
|
||||
<i class="fa-solid fa-list text-rose-600"></i>
|
||||
<h3 class="truncate font-bold">{{ $playlist->name }}</h3>
|
||||
<span class="ml-auto shrink-0 rounded-full bg-neutral-100 px-2 py-0.5 text-xs font-semibold text-neutral-600 dark:bg-neutral-800 dark:text-neutral-300">
|
||||
{{ $total }} {{ __('playlist.episodes') }}
|
||||
</span>
|
||||
</a>
|
||||
<div class="mt-2 flex items-center gap-2">
|
||||
<img src="{{ $playlist->user->getAvatar() }}" alt="" class="h-6 w-6 rounded-full">
|
||||
<span class="truncate text-xs text-neutral-500 dark:text-neutral-400">{{ $playlist->user->name }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="border-y border-neutral-200/70 px-4 py-3 dark:border-neutral-800">
|
||||
<p class="mb-2 text-[11px] font-semibold uppercase tracking-wider text-rose-600 dark:text-rose-400">
|
||||
<i class="fa-solid fa-play mr-1"></i>{{ __('playlist.now-playing') }}
|
||||
</p>
|
||||
<div class="flex items-center gap-3">
|
||||
<img src="{{ $currentEpisode->gallery->first()?->thumbnail_url }}" alt=""
|
||||
class="h-12 w-20 shrink-0 rounded-md object-cover" loading="eager" decoding="async">
|
||||
<div class="min-w-0 flex-1">
|
||||
<p class="truncate text-sm font-semibold text-neutral-900 dark:text-white">
|
||||
{{ $currentEpisode->title }}
|
||||
</p>
|
||||
<p class="text-xs text-neutral-500 dark:text-neutral-400">
|
||||
{{ $currentPosition }}/{{ $total }} {{ __('playlist.episodes') }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="flex shrink-0 items-center gap-1">
|
||||
@if ($previousEpisodeSlug)
|
||||
<a href="{{ route('hentai.index', ['title' => $previousEpisodeSlug, 'playlist' => $playlist->id]) }}"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-500 transition hover:bg-neutral-100 hover:text-rose-500 dark:text-neutral-400 dark:hover:bg-neutral-800">
|
||||
<i class="fa-solid fa-chevron-up"></i>
|
||||
</a>
|
||||
@endif
|
||||
@if ($nextEpisodeSlug)
|
||||
<a href="{{ route('hentai.index', ['title' => $nextEpisodeSlug, 'playlist' => $playlist->id]) }}"
|
||||
class="flex h-8 w-8 items-center justify-center rounded-lg text-neutral-500 transition hover:bg-neutral-100 hover:text-rose-500 dark:text-neutral-400 dark:hover:bg-neutral-800">
|
||||
<i class="fa-solid fa-chevron-down"></i>
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div x-ref="scroller" x-on:scroll.passive="onScroll"
|
||||
class="max-h-[50vh] overflow-y-auto overscroll-contain [overflow-anchor:none] [&::-webkit-scrollbar]:w-1.5 [&::-webkit-scrollbar-thumb]:rounded-full [&::-webkit-scrollbar-thumb]:bg-neutral-300 [&::-webkit-scrollbar-track]:bg-transparent dark:[&::-webkit-scrollbar-thumb]:bg-neutral-700 xl:max-h-[calc(100vh-340px)]">
|
||||
<div wire:loading.delay wire:target="prependChunk"
|
||||
class="flex items-center justify-center py-3 text-neutral-400">
|
||||
<i class="fa-solid fa-spinner animate-spin"></i>
|
||||
</div>
|
||||
|
||||
@forelse ($episodes as $playlistEpisode)
|
||||
@include('livewire.partials.playlist-sidebar-row', [
|
||||
'playlistEpisode' => $playlistEpisode,
|
||||
'isActive' => $playlistEpisode->episode_id === $currentEpisodeId,
|
||||
'isOwner' => $isOwner,
|
||||
])
|
||||
@empty
|
||||
<div class="px-4 py-10 text-center">
|
||||
<div class="inline-flex h-14 w-14 items-center justify-center rounded-full bg-neutral-100 dark:bg-neutral-800">
|
||||
<i class="fa-solid fa-clapperboard text-xl text-neutral-400 dark:text-neutral-500"></i>
|
||||
</div>
|
||||
<p class="mt-3 text-sm text-neutral-500 dark:text-neutral-400">{{ __('playlist.empty-playlist') }}</p>
|
||||
</div>
|
||||
@endforelse
|
||||
|
||||
<div wire:loading.delay wire:target="appendChunk"
|
||||
class="flex items-center justify-center py-3 text-neutral-400">
|
||||
<i class="fa-solid fa-spinner animate-spin"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if ($collapsible)
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
Reference in New Issue
Block a user