Update playlist sidebar to use livewire
This commit is contained in:
@@ -56,14 +56,11 @@ class StreamController extends Controller
|
||||
// Playlist
|
||||
if ($request->has('playlist')) {
|
||||
// Get and check if playlist exists
|
||||
$playlist = Playlist::where('id', $request->input('playlist'))->firstOrFail();
|
||||
$playlist = Playlist::withCount('episodes')->where('id', $request->input('playlist'))->firstOrFail();
|
||||
|
||||
// Check if episode is in playlist
|
||||
$inPlaylist = PlaylistEpisode::where('playlist_id', $playlist->id)->where('episode_id', $episode->id)->firstOrFail();
|
||||
|
||||
// Get Playlist Episodes and order them
|
||||
$playlistEpisodes = $playlist->episodes()->orderBy('position')->get();
|
||||
|
||||
// Check if authorized
|
||||
if ($playlist->is_private && (Auth::guest() || (! Auth::guest() && Auth::user()->id != $playlist->user_id))) {
|
||||
abort(404);
|
||||
@@ -75,7 +72,6 @@ class StreamController extends Controller
|
||||
'studioEpisodes' => $studioEpisodes,
|
||||
'gallery' => $gallery,
|
||||
'playlist' => $playlist,
|
||||
'playlistEpisodes' => $playlistEpisodes,
|
||||
'popularWeekly' => CacheHelper::getPopularWeekly(),
|
||||
'isMobile' => $isMobile,
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,182 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire;
|
||||
|
||||
use App\Models\Episode;
|
||||
use App\Models\Playlist;
|
||||
use App\Models\PlaylistEpisode;
|
||||
use App\Services\PlaylistService;
|
||||
use Illuminate\Contracts\View\View;
|
||||
use Illuminate\Support\Collection;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Livewire\Component;
|
||||
|
||||
class PlaylistSidebar extends Component
|
||||
{
|
||||
public const PAGE_SIZE = 20;
|
||||
|
||||
public const MAX_WINDOW = 60;
|
||||
|
||||
protected PlaylistService $playlistService;
|
||||
|
||||
public Playlist $playlist;
|
||||
|
||||
public Episode $currentEpisode;
|
||||
|
||||
public int $currentEpisodeId;
|
||||
|
||||
public int $total = 0;
|
||||
|
||||
public int $windowStart = 1;
|
||||
|
||||
public int $windowEnd = 0;
|
||||
|
||||
public bool $isOwner = false;
|
||||
|
||||
public bool $collapsible = false;
|
||||
|
||||
public ?string $previousEpisodeSlug = null;
|
||||
|
||||
public ?string $nextEpisodeSlug = null;
|
||||
|
||||
public function boot(PlaylistService $playlistService): void
|
||||
{
|
||||
$this->playlistService = $playlistService;
|
||||
}
|
||||
|
||||
public function mount(int $playlistId, int $currentEpisodeId, bool $collapsible = false): void
|
||||
{
|
||||
$this->playlist = Playlist::with('user')->withCount('episodes')->findOrFail($playlistId);
|
||||
$this->currentEpisodeId = $currentEpisodeId;
|
||||
$this->currentEpisode = Episode::with(['gallery' => fn ($query) => $query->orderBy('id')->limit(1)])
|
||||
->findOrFail($currentEpisodeId);
|
||||
$this->total = $this->playlist->episodes_count;
|
||||
$this->collapsible = $collapsible;
|
||||
$this->isOwner = Auth::check() && Auth::user()->id === $this->playlist->user_id;
|
||||
|
||||
$this->repairNullPositions();
|
||||
|
||||
$position = $this->currentPosition() ?? 1;
|
||||
|
||||
$this->setWindowAround($position);
|
||||
$this->resolveAdjacentSlugs($position);
|
||||
}
|
||||
|
||||
public function getEpisodesProperty(): Collection
|
||||
{
|
||||
return PlaylistEpisode::query()
|
||||
->where('playlist_id', $this->playlist->id)
|
||||
->whereBetween('position', [$this->windowStart, $this->windowEnd])
|
||||
->orderBy('position')
|
||||
->with([
|
||||
'episode.gallery' => fn ($gallery) => $gallery->orderBy('id')->limit(1),
|
||||
'episode.studio',
|
||||
])
|
||||
->get();
|
||||
}
|
||||
|
||||
public function appendChunk(): void
|
||||
{
|
||||
if ($this->windowEnd >= $this->total) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->windowEnd = min($this->total, $this->windowEnd + self::PAGE_SIZE);
|
||||
$this->windowStart = max(1, $this->windowEnd - self::MAX_WINDOW + 1);
|
||||
}
|
||||
|
||||
public function prependChunk(): void
|
||||
{
|
||||
if ($this->windowStart <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->windowStart = max(1, $this->windowStart - self::PAGE_SIZE);
|
||||
$this->windowEnd = min($this->total, $this->windowStart + self::MAX_WINDOW - 1);
|
||||
}
|
||||
|
||||
public function remove(int $playlistEpisodeId): void
|
||||
{
|
||||
if (! $this->isOwner) {
|
||||
return;
|
||||
}
|
||||
|
||||
$playlistEpisode = PlaylistEpisode::find($playlistEpisodeId);
|
||||
|
||||
if (! $playlistEpisode) {
|
||||
return;
|
||||
}
|
||||
|
||||
$playlistEpisode->delete();
|
||||
$this->playlistService->reorderPositions($this->playlist);
|
||||
$this->playlist->loadCount('episodes');
|
||||
|
||||
$this->total = $this->playlist->episodes_count;
|
||||
|
||||
$this->windowEnd = min($this->windowEnd, max(1, $this->total));
|
||||
$this->windowStart = min($this->windowStart, max(1, $this->total));
|
||||
|
||||
$position = $this->currentPosition();
|
||||
|
||||
if ($position) {
|
||||
$this->resolveAdjacentSlugs($position);
|
||||
}
|
||||
}
|
||||
|
||||
public function render(): View
|
||||
{
|
||||
$activePlaylistEpisode = $this->activePlaylistEpisode();
|
||||
|
||||
return view('livewire.playlist-sidebar', [
|
||||
'episodes' => $this->episodes,
|
||||
'currentPosition' => $activePlaylistEpisode?->position ?? 1,
|
||||
'currentPlaylistEpisodeId' => $activePlaylistEpisode?->id,
|
||||
'isOwner' => $this->isOwner,
|
||||
]);
|
||||
}
|
||||
|
||||
private function activePlaylistEpisode(): ?PlaylistEpisode
|
||||
{
|
||||
return PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->where('episode_id', $this->currentEpisodeId)
|
||||
->first();
|
||||
}
|
||||
|
||||
private function currentPosition(): ?int
|
||||
{
|
||||
return $this->activePlaylistEpisode()?->position;
|
||||
}
|
||||
|
||||
private function setWindowAround(int $position): void
|
||||
{
|
||||
$this->windowStart = max(1, $position - self::PAGE_SIZE);
|
||||
$this->windowEnd = min($this->total, $position + self::PAGE_SIZE);
|
||||
}
|
||||
|
||||
private function resolveAdjacentSlugs(int $position): void
|
||||
{
|
||||
$adjacent = PlaylistEpisode::query()
|
||||
->where('playlist_id', $this->playlist->id)
|
||||
->whereBetween('position', [$position - 1, $position + 1])
|
||||
->orderBy('position')
|
||||
->with('episode')
|
||||
->get();
|
||||
|
||||
$slugsByPosition = $adjacent->keyBy('position');
|
||||
|
||||
$this->previousEpisodeSlug = $slugsByPosition->get($position - 1)?->episode->slug;
|
||||
$this->nextEpisodeSlug = $slugsByPosition->get($position + 1)?->episode->slug;
|
||||
}
|
||||
|
||||
private function repairNullPositions(): void
|
||||
{
|
||||
if (! PlaylistEpisode::where('playlist_id', $this->playlist->id)->whereNull('position')->exists()) {
|
||||
return;
|
||||
}
|
||||
|
||||
PlaylistEpisode::where('playlist_id', $this->playlist->id)
|
||||
->orderBy('position')->orderBy('id')
|
||||
->get()
|
||||
->each(fn ($playlistEpisode, $index) => $playlistEpisode->update(['position' => $index + 1]));
|
||||
}
|
||||
}
|
||||
@@ -13,4 +13,6 @@ return [
|
||||
'no-matches' => 'Keine Episoden entsprechen deiner Suche.',
|
||||
'watched' => 'Gesehen',
|
||||
'remove' => 'Entfernen',
|
||||
'now-playing' => 'Jetzt läuft',
|
||||
'remove-confirm' => 'Diese Episode aus der Playlist entfernen?',
|
||||
];
|
||||
|
||||
@@ -13,4 +13,6 @@ return [
|
||||
'no-matches' => 'No episodes match your search.',
|
||||
'watched' => 'Watched',
|
||||
'remove' => 'Remove',
|
||||
'now-playing' => 'Now Playing',
|
||||
'remove-confirm' => 'Remove this episode from the playlist?',
|
||||
];
|
||||
|
||||
@@ -13,4 +13,6 @@ return [
|
||||
'no-matches' => 'Aucun épisode ne correspond à votre recherche.',
|
||||
'watched' => 'Vu',
|
||||
'remove' => 'Supprimer',
|
||||
'now-playing' => 'En lecture',
|
||||
'remove-confirm' => 'Supprimer cet épisode de la playlist ?',
|
||||
];
|
||||
|
||||
@@ -14,107 +14,3 @@ export function playNextPlaylistVideo() {
|
||||
|
||||
window.location.href = '/hentai/' + nextEpisode + '?playlist=' + playlistId;
|
||||
}
|
||||
|
||||
function deleteEntry(playlistId, episodeId) {
|
||||
window.axios.post('/user/playlist-episode', {
|
||||
playlist: playlistId,
|
||||
episode: episodeId
|
||||
}).then(function (response) {
|
||||
if (response.status == 200) {
|
||||
console.log(response);
|
||||
if (response.data.message == 'success') {
|
||||
Swal.fire({
|
||||
title: "Deleted!",
|
||||
text: "Removed entry from playlist!",
|
||||
icon: "success",
|
||||
confirmButtonText: "OK",
|
||||
willClose: () => {
|
||||
location.reload();
|
||||
}
|
||||
}).then((result) => {
|
||||
if (result.isConfirmed) {
|
||||
location.reload();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}).catch(function (error) {
|
||||
Swal.fire({
|
||||
title: "Error!",
|
||||
text: error,
|
||||
icon: "error"
|
||||
});
|
||||
console.log(error);
|
||||
});
|
||||
}
|
||||
|
||||
function addDesktopDeleteListener() {
|
||||
const deleteButtons = document.querySelectorAll('[id^="delD"]');
|
||||
|
||||
deleteButtons.forEach(button => {
|
||||
const playlist = button.id.split('-')[1];
|
||||
const episode = button.id.split('-')[2];
|
||||
console.log("Playlist: " + playlist + " Episode: " + episode);
|
||||
|
||||
button.addEventListener('click', () => deleteEntry(playlist, episode));
|
||||
});
|
||||
}
|
||||
|
||||
// Playlist Swipe (Delete)
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const swipeContainers = document.querySelectorAll('.swipe-container');
|
||||
|
||||
var swipeOptions = {
|
||||
dragLockToAxis: true,
|
||||
dragBlockHorizontal: true
|
||||
};
|
||||
|
||||
swipeContainers.forEach(container => {
|
||||
const controls = new Hammer(container, swipeOptions);
|
||||
const originalColor = container.style.backgroundColor;
|
||||
const playlistId = container.id.split('-')[0];
|
||||
const episodeId = container.id.split('-')[1];
|
||||
const delIcon = document.getElementById('del-' + container.id);
|
||||
|
||||
// Set the initial position
|
||||
let posX = 0;
|
||||
|
||||
// Listen for the pan gesture
|
||||
controls.on('pan', (event) => {
|
||||
// Update the X position based on the drag delta
|
||||
posX = event.deltaX;
|
||||
if (posX > 0) {
|
||||
// Only allow left swipe
|
||||
posX = 0;
|
||||
}
|
||||
|
||||
// Apply the translation to the element
|
||||
container.style.transform = `translateX(${posX}px)`;
|
||||
container.style.backgroundColor = "rgba(159, 18, 18, 0.3)";
|
||||
|
||||
setTimeout(() => {
|
||||
delIcon.classList.remove('fa-grip-lines-vertical');
|
||||
delIcon.classList.add('fa-trash');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
controls.on('panend', () => {
|
||||
container.style.transition = 'transform 0.3s ease';
|
||||
container.style.transform = 'translateX(0)';
|
||||
setTimeout(() => {
|
||||
container.style.transition = ''; // Reset transition for next drag
|
||||
container.style.backgroundColor = originalColor;
|
||||
delIcon.classList.remove('fa-trash');
|
||||
delIcon.classList.add('fa-grip-lines-vertical');
|
||||
}, 300);
|
||||
});
|
||||
|
||||
controls.on('swipeleft', (event) => {
|
||||
container.style.display = 'none';
|
||||
console.log(playlistId, episodeId);
|
||||
deleteEntry(playlistId, episodeId);
|
||||
});
|
||||
});
|
||||
|
||||
addDesktopDeleteListener();
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
@props([
|
||||
'playlistEpisode',
|
||||
'isActive' => false,
|
||||
'isOwner' => false,
|
||||
])
|
||||
|
||||
@php
|
||||
$episode = $playlistEpisode->episode;
|
||||
@endphp
|
||||
|
||||
<div
|
||||
wire:key="playlist-sidebar-row-{{ $playlistEpisode->id }}"
|
||||
@if ($isActive) data-active-row aria-current="true" @endif
|
||||
class="group relative flex h-20 shrink-0 items-center gap-3 px-2.5 transition {{ $isActive ? 'bg-rose-600/10 dark:bg-rose-500/10' : 'hover:bg-neutral-100 dark:hover:bg-neutral-900' }}"
|
||||
>
|
||||
@if ($isActive)
|
||||
<span class="absolute inset-y-0 left-0 w-1 bg-rose-600"></span>
|
||||
@endif
|
||||
|
||||
<div class="flex h-7 w-7 shrink-0 items-center justify-center text-sm text-neutral-500 dark:text-neutral-400">
|
||||
@if ($isActive)
|
||||
<i class="fa-solid fa-play text-rose-600 dark:text-rose-400"></i>
|
||||
@else
|
||||
{{ $playlistEpisode->position }}
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<a href="{{ route('hentai.index', ['title' => $episode->slug, 'playlist' => $playlistEpisode->playlist_id]) }}" class="shrink-0">
|
||||
<img
|
||||
src="{{ $episode->gallery->first()?->thumbnail_url }}"
|
||||
alt="{{ $episode->title }} - {{ $episode->episode }}"
|
||||
loading="{{ $isActive ? 'eager' : 'lazy' }}"
|
||||
decoding="async"
|
||||
width="100"
|
||||
class="h-14 w-[100px] rounded-md object-cover {{ $isActive ? 'ring-2 ring-rose-600/60' : '' }}"
|
||||
>
|
||||
</a>
|
||||
|
||||
<div class="min-w-0 flex-1">
|
||||
<a href="{{ route('hentai.index', ['title' => $episode->slug, 'playlist' => $playlistEpisode->playlist_id]) }}"
|
||||
class="block truncate text-sm font-semibold text-neutral-900 transition hover:text-rose-500 dark:text-white dark:hover:text-rose-400">
|
||||
{{ $episode->title }}
|
||||
</a>
|
||||
<p class="truncate text-xs text-neutral-500 dark:text-neutral-400">{{ $episode->studio->name }}</p>
|
||||
</div>
|
||||
|
||||
@if ($isOwner)
|
||||
<button
|
||||
type="button"
|
||||
wire:click="remove({{ $playlistEpisode->id }})"
|
||||
wire:confirm="{{ __('playlist.remove-confirm') }}"
|
||||
aria-label="{{ __('playlist.remove') }}"
|
||||
class="shrink-0 rounded-md p-2 text-neutral-400 transition hover:bg-red-50 hover:text-red-600 sm:opacity-0 sm:group-hover:opacity-100 dark:text-neutral-500 dark:hover:bg-red-950/40 dark:hover:text-red-400"
|
||||
>
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
@endif
|
||||
</div>
|
||||
@@ -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>
|
||||
@@ -16,7 +16,9 @@
|
||||
|
||||
@if($isMobile)
|
||||
<div class="flex flex-col">
|
||||
@include('stream.partials.playlist')
|
||||
@isset($playlist)
|
||||
<livewire:playlist-sidebar :playlist-id="$playlist->id" :current-episode-id="$episode->id" :collapsible="true" />
|
||||
@endisset
|
||||
</div>
|
||||
@endif
|
||||
|
||||
@@ -27,7 +29,9 @@
|
||||
</div>
|
||||
<div class="flex flex-col">
|
||||
@if(! $isMobile)
|
||||
@include('stream.partials.playlist')
|
||||
@isset($playlist)
|
||||
<livewire:playlist-sidebar :playlist-id="$playlist->id" :current-episode-id="$episode->id" />
|
||||
@endisset
|
||||
@endif
|
||||
|
||||
@include('stream.partials.more-episodes')
|
||||
|
||||
@@ -1,94 +0,0 @@
|
||||
@isset($playlist)
|
||||
<div class="pt-2 sm:px-2 lg:px-4 2xl:w-[450px]">
|
||||
<div class="bg-transparent rounded-lg overflow-hidden bg-white dark:bg-neutral-800">
|
||||
<div class="p-4">
|
||||
<p class="leading-normal font-bold text-lg text-rose-600 pb-2">
|
||||
@if ($playlist->is_private)
|
||||
<a href="{{ route('profile.playlist.show', $playlist->id) }}">{{ $playlist->name }}</a>
|
||||
@else
|
||||
<a href="{{ route('playlist.show', $playlist->id) }}">{{ $playlist->name }}</a>
|
||||
@endif
|
||||
</p>
|
||||
@php
|
||||
$episodeCount = $playlistEpisodes->count();
|
||||
$currentIndex = 0;
|
||||
$nextEpisode = "";
|
||||
|
||||
if ($episodeCount > 1) {
|
||||
$currentIndex = $playlistEpisodes->search(fn($playlistEpisode) => $playlistEpisode->episode->id == $episode->id);
|
||||
$nextEpisode = $currentIndex !== false && $currentIndex + 1 < $episodeCount
|
||||
? $playlistEpisodes[$currentIndex + 1]->episode->slug
|
||||
: "";
|
||||
}
|
||||
@endphp
|
||||
<p class="text-neutral-800 dark:text-neutral-300">
|
||||
{{ $playlist->user->name }} • {{ $currentIndex + 1 }}/{{ $episodeCount }} Episodes
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div id="scrollable" class="flex-none min-w-full px-4 sm:px-6 md:px-0 overflow-auto scrollbar:!w-1.5 scrollbar:!h-1.5 scrollbar:bg-transparent scrollbar-track:!bg-slate-100 scrollbar-thumb:!rounded scrollbar-thumb:!bg-slate-300 scrollbar-track:!rounded dark:scrollbar-track:!bg-slate-500/[0.16] dark:scrollbar-thumb:!bg-slate-500/50 max-h-96 lg:supports-scrollbars:pr-2 lg:max-h-96">
|
||||
<div class="overflow-y-auto">
|
||||
<div class="space-y-2 p-0 pb-2 sm:p-2">
|
||||
@php
|
||||
$counter = 1;
|
||||
$isAuthedUsersPlaylist = false;
|
||||
if (auth()->check() && $playlist->user->id == auth()->user()->id) {
|
||||
$isAuthedUsersPlaylist = true;
|
||||
}
|
||||
@endphp
|
||||
@foreach($playlistEpisodes as $playlistEpisode)
|
||||
@if ($playlistEpisode->episode->id == $episode->id)
|
||||
<div class="flex items-center gap-4 p-2 bg-rose-800/30 rounded-lg shadow swipe-container transition-colors" id="active">
|
||||
@else
|
||||
<div
|
||||
class="flex items-center gap-4 p-2 dark:bg-neutral-900/50 bg-white rounded-lg shadow transition-colors @if($isMobile && $isAuthedUsersPlaylist) swipe-container @endif"
|
||||
id="{{ $playlist->id }}-{{ $playlistEpisode->episode->id }}">
|
||||
@endif
|
||||
<div class="text-black dark:text-white">
|
||||
@if ($playlistEpisode->episode->id == $episode->id)
|
||||
<i class="fa-solid fa-play w-[15px]"></i>
|
||||
@else
|
||||
<p class="w-[15px]">{{ $counter }}</p>
|
||||
@endif
|
||||
</div>
|
||||
<a href="{{ route('hentai.index', ['title' => $playlistEpisode->episode->slug, 'playlist' => $playlist->id ]) }}" class="contents">
|
||||
<img loading="lazy" src="{{ $playlistEpisode->episode->gallery->first()->thumbnail_url }}" alt="{{ $playlistEpisode->episode->title }} - {{ $playlistEpisode->episode->episode }}" class="w-20 h-14 object-cover rounded">
|
||||
</a>
|
||||
<div class="grow">
|
||||
<a href="{{ route('hentai.index', ['title' => $playlistEpisode->episode->slug, 'playlist' => $playlist->id ]) }}">
|
||||
<p class="text-black dark:text-white font-medium text-sm break-words">{{ $playlistEpisode->episode->title }} - {{ $playlistEpisode->episode->episode }}</p>
|
||||
</a>
|
||||
<p class="text-gray-700 dark:text-gray-300 text-xs truncate">{{ $playlistEpisode->episode->viewCount() }} Views - {{ $playlistEpisode->episode->studio->name }}</p>
|
||||
</div>
|
||||
@if ($playlistEpisode->episode->id != $episode->id && $isAuthedUsersPlaylist)
|
||||
@if($isMobile)
|
||||
<div class="justify-self-end flex items-center">
|
||||
<i class="transition-all fa-solid fa-grip-lines-vertical cursor-grab text-black dark:text-white" id="del-{{ $playlist->id }}-{{ $playlistEpisode->episode->id }}"></i>
|
||||
</div>
|
||||
@else
|
||||
<div class="justify-self-end flex items-center">
|
||||
<a class="transition-all fa-solid fa-trash cursor-pointer text-red-700/80" id="delD-{{ $playlist->id }}-{{ $playlistEpisode->episode->id }}"></a>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
</div>
|
||||
@php $counter++; @endphp
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<input id="playlist_id" type="hidden" value="{{ $playlist->id }}">
|
||||
<input id="playlist_next_episode_slug" type="hidden" value="{{ $nextEpisode }}">
|
||||
|
||||
<script>
|
||||
// Select the scrollable div and the target child element
|
||||
const scrollableDiv = document.getElementById('scrollable');
|
||||
const targetElement = document.getElementById('active');
|
||||
|
||||
// Scroll to the target element
|
||||
scrollableDiv.scrollTop = targetElement.offsetTop - scrollableDiv.offsetTop - 50;
|
||||
</script>
|
||||
@endisset
|
||||
Reference in New Issue
Block a user