mirror of
https://github.com/TeamPiped/Piped.git
synced 2025-05-04 13:41:19 +00:00
feat: improve playlists display aside
This commit is contained in:
parent
fa1ee969a7
commit
5266b2d1d9
@ -1,24 +1,90 @@
|
|||||||
<template>
|
<template>
|
||||||
<div ref="scrollable" class="h-screen-sm overflow-x-scroll">
|
<div>
|
||||||
<VideoItem
|
<router-link :to="{ path: '/playlist', query: { list: playlistId } }"
|
||||||
|
><h1 class="font-bold !text-lg hover:underline" v-text="playlist.name"
|
||||||
|
/></router-link>
|
||||||
|
<span class="text-sm">
|
||||||
|
<template v-if="playlist.uploader">
|
||||||
|
<router-link class="link-secondary" :to="playlist.uploaderUrl" :title="playlist.uploader">
|
||||||
|
{{ playlist.uploader }}
|
||||||
|
</router-link>
|
||||||
|
-
|
||||||
|
</template>
|
||||||
|
{{ selectedIndex }} / {{ playlist.videos }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div ref="scrollable" class="mt-4 max-h-screen-sm overflow-y-auto">
|
||||||
|
<div
|
||||||
v-for="(related, index) in playlist.relatedStreams"
|
v-for="(related, index) in playlist.relatedStreams"
|
||||||
:key="related.url"
|
:key="related.url"
|
||||||
:item="related"
|
|
||||||
:index="index"
|
:index="index"
|
||||||
:playlist-id="playlistId"
|
:playlist-id="playlistId"
|
||||||
:prefer-listen="preferListen"
|
:prefer-listen="preferListen"
|
||||||
height="94"
|
>
|
||||||
width="168"
|
<router-link
|
||||||
/>
|
class="flex hover:bg-gray-50 .dark:hover:bg-neutral-800"
|
||||||
|
:class="{ 'bg-gray-200 .dark:bg-neutral-700': index === selectedIndex - 1 }"
|
||||||
|
:to="{
|
||||||
|
path: '/watch',
|
||||||
|
query: {
|
||||||
|
v: related.url.substr(-11),
|
||||||
|
...(playlistId && { list: playlistId }),
|
||||||
|
...(index >= 0 && { index: index + 1 }),
|
||||||
|
...(preferListen && { listen: 1 }),
|
||||||
|
},
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<span class="min-w-5 flex-none text-xs" v-text="index + 1" />
|
||||||
|
<div class="w-24 flex-none pb-1">
|
||||||
|
<VideoThumbnail :item="related" />
|
||||||
|
|
||||||
|
<div class="relative text-xs">
|
||||||
|
<!-- shorts thumbnail -->
|
||||||
|
<span v-if="related.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
|
||||||
|
<span
|
||||||
|
v-if="related.duration > 0 || (related.duration == 0 && related.isShort)"
|
||||||
|
class="thumbnail-overlay thumbnail-right px-0.5"
|
||||||
|
v-text="timeFormat(related.duration)"
|
||||||
|
/>
|
||||||
|
<i18n-t
|
||||||
|
v-else
|
||||||
|
keypath="video.live"
|
||||||
|
class="thumbnail-overlay thumbnail-right !bg-red-600"
|
||||||
|
tag="div"
|
||||||
|
>
|
||||||
|
<i class="i-fa6-solid:tower-broadcast w-3" />
|
||||||
|
</i18n-t>
|
||||||
|
<span
|
||||||
|
v-if="related.watched"
|
||||||
|
v-t="'video.watched'"
|
||||||
|
class="thumbnail-overlay bottom-5px left-5px px-0.5"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="ml-2 flex flex-col">
|
||||||
|
<span class="text-sm font-semibold leading-4" :title="related.title" v-text="related.title" />
|
||||||
|
<div class="flex-1">
|
||||||
|
<router-link
|
||||||
|
v-if="related.uploaderUrl && related.uploaderName && !hideChannel"
|
||||||
|
class="link-secondary block overflow-hidden text-xs"
|
||||||
|
:to="related.uploaderUrl"
|
||||||
|
:title="related.uploaderName"
|
||||||
|
>
|
||||||
|
<span v-text="related.uploaderName" />
|
||||||
|
<i v-if="related.uploaderVerified" class="i-fa6-solid:check ml-1.5" />
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { nextTick } from "vue";
|
import { nextTick } from "vue";
|
||||||
import VideoItem from "./VideoItem.vue";
|
import VideoThumbnail from "./VideoThumbnail.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { VideoItem },
|
components: { VideoThumbnail },
|
||||||
props: {
|
props: {
|
||||||
playlist: {
|
playlist: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -12,35 +12,14 @@
|
|||||||
},
|
},
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="w-full">
|
<VideoThumbnail :item="item" />
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
class="aspect-video w-full rounded-md object-contain"
|
|
||||||
:src="thumbnail"
|
|
||||||
:alt="title"
|
|
||||||
:class="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
|
|
||||||
/>
|
|
||||||
<!-- progress bar -->
|
|
||||||
<div class="relative h-1 w-full">
|
|
||||||
<div
|
|
||||||
v-if="item.watched && item.duration > 0"
|
|
||||||
class="absolute bottom-0 left-0 h-1 bg-red-600"
|
|
||||||
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="relative text-sm">
|
<div class="relative text-sm">
|
||||||
<span
|
|
||||||
v-if="item.duration > 0"
|
|
||||||
class="thumbnail-overlay thumbnail-right"
|
|
||||||
v-text="timeFormat(item.duration)"
|
|
||||||
/>
|
|
||||||
<!-- shorts thumbnail -->
|
<!-- shorts thumbnail -->
|
||||||
<span v-if="item.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
|
<span v-if="item.isShort" v-t="'video.shorts'" class="thumbnail-overlay thumbnail-left" />
|
||||||
<span
|
<span
|
||||||
v-else-if="item.duration >= 0"
|
v-if="item.duration > 0 || (item.duration == 0 && item.isShort)"
|
||||||
class="thumbnail-overlay thumbnail-right"
|
class="thumbnail-overlay thumbnail-right px-0.5"
|
||||||
v-text="timeFormat(item.duration)"
|
v-text="timeFormat(item.duration)"
|
||||||
/>
|
/>
|
||||||
<i18n-t v-else keypath="video.live" class="thumbnail-overlay thumbnail-right !bg-red-600" tag="div">
|
<i18n-t v-else keypath="video.live" class="thumbnail-overlay thumbnail-right !bg-red-600" tag="div">
|
||||||
@ -166,9 +145,10 @@
|
|||||||
import PlaylistAddModal from "./PlaylistAddModal.vue";
|
import PlaylistAddModal from "./PlaylistAddModal.vue";
|
||||||
import ShareModal from "./ShareModal.vue";
|
import ShareModal from "./ShareModal.vue";
|
||||||
import ConfirmModal from "./ConfirmModal.vue";
|
import ConfirmModal from "./ConfirmModal.vue";
|
||||||
|
import VideoThumbnail from "./VideoThumbnail.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { PlaylistAddModal, ConfirmModal, ShareModal },
|
components: { PlaylistAddModal, ConfirmModal, ShareModal, VideoThumbnail },
|
||||||
props: {
|
props: {
|
||||||
item: {
|
item: {
|
||||||
type: Object,
|
type: Object,
|
||||||
@ -268,10 +248,6 @@ export default {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.shorts-img {
|
|
||||||
@apply w-full object-contain;
|
|
||||||
}
|
|
||||||
|
|
||||||
.video-info {
|
.video-info {
|
||||||
@apply mt-1 text-xs text-gray-600 font-normal;
|
@apply mt-1 text-xs text-gray-600 font-normal;
|
||||||
}
|
}
|
||||||
|
45
src/components/VideoThumbnail.vue
Normal file
45
src/components/VideoThumbnail.vue
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
<template>
|
||||||
|
<div class="w-full">
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
class="aspect-video w-full rounded-md object-contain"
|
||||||
|
:src="item.thumbnail"
|
||||||
|
:alt="item.title"
|
||||||
|
:class="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
|
||||||
|
/>
|
||||||
|
<!-- progress bar -->
|
||||||
|
<div class="relative h-1 w-full">
|
||||||
|
<div
|
||||||
|
v-if="item.watched && item.duration > 0"
|
||||||
|
class="absolute bottom-0 left-0 h-1 bg-red-600"
|
||||||
|
:style="{ width: `clamp(0%, ${(item.currentTime / item.duration) * 100}%, 100%` }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
item: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {
|
||||||
|
return {};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
title() {
|
||||||
|
return this.item.dearrow?.titles[0]?.title ?? this.item.title;
|
||||||
|
},
|
||||||
|
thumbnail() {
|
||||||
|
return this.item.dearrow?.thumbnails[0]?.thumbnail ?? this.item.thumbnail;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.shorts-img {
|
||||||
|
@apply w-full object-contain;
|
||||||
|
}
|
||||||
|
</style>
|
@ -17,21 +17,282 @@
|
|||||||
<i18n-t keypath="info.next_video_countdown">{{ counter }}</i18n-t>
|
<i18n-t keypath="info.next_video_countdown">{{ counter }}</i18n-t>
|
||||||
</ToastComponent>
|
</ToastComponent>
|
||||||
</Transition>
|
</Transition>
|
||||||
|
<div class="flex gap-5 max-lg:flex-col">
|
||||||
|
<div class="flex-auto">
|
||||||
|
<div v-show="!video.error">
|
||||||
|
<keep-alive>
|
||||||
|
<VideoPlayer
|
||||||
|
ref="videoPlayer"
|
||||||
|
:video="video"
|
||||||
|
:sponsors="sponsors"
|
||||||
|
:selected-auto-play="selectedAutoPlay"
|
||||||
|
:selected-auto-loop="selectedAutoLoop"
|
||||||
|
@timeupdate="onTimeUpdate"
|
||||||
|
@ended="onVideoEnded"
|
||||||
|
@navigate-next="navigateNext"
|
||||||
|
/>
|
||||||
|
</keep-alive>
|
||||||
|
<div v-if="video && isMobile">
|
||||||
|
<ChaptersBar
|
||||||
|
v-if="video?.chapters?.length > 0 && showChapters"
|
||||||
|
:mobile-layout="isMobile"
|
||||||
|
:chapters="video.chapters"
|
||||||
|
:player-position="currentTime"
|
||||||
|
@seek="navigate"
|
||||||
|
/>
|
||||||
|
<PlaylistVideos
|
||||||
|
v-if="playlist"
|
||||||
|
:playlist-id="playlistId"
|
||||||
|
:playlist="playlist"
|
||||||
|
:selected-index="index"
|
||||||
|
:prefer-listen="isListening"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<!-- video title -->
|
||||||
|
<div class="mt-2 break-words text-2xl font-bold" v-text="video.title" />
|
||||||
|
<div class="mb-3 mt-3 flex flex-wrap">
|
||||||
|
<!-- views / date -->
|
||||||
|
<div class="flex flex-auto gap-2">
|
||||||
|
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
|
||||||
|
<span> | </span>
|
||||||
|
<span :title="new Date(video.uploadDate).toLocaleString()" v-text="uploadDate" />
|
||||||
|
</div>
|
||||||
|
<!-- Likes/dilikes -->
|
||||||
|
<div class="flex gap-2">
|
||||||
|
<template v-if="video.likes >= 0">
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="i-fa6-solid:thumbs-up" />
|
||||||
|
<strong class="ml-1" v-text="addCommas(video.likes)" />
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center">
|
||||||
|
<div class="i-fa6-solid:thumbs-down" />
|
||||||
|
<strong
|
||||||
|
class="ml-1"
|
||||||
|
v-text="video.dislikes >= 0 ? addCommas(video.dislikes) : '?'"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="video.likes < 0">
|
||||||
|
<div>
|
||||||
|
<strong v-t="'video.ratings_disabled'" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!-- Channel info & options flex container -->
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<!-- Channel Image & Info -->
|
||||||
|
<div class="flex items-center">
|
||||||
|
<img
|
||||||
|
loading="lazy"
|
||||||
|
height="48"
|
||||||
|
width="48"
|
||||||
|
:src="video.uploaderAvatar"
|
||||||
|
alt=""
|
||||||
|
class="rounded-full"
|
||||||
|
/>
|
||||||
|
<router-link v-if="video.uploaderUrl" class="link ml-1.5" :to="video.uploaderUrl">{{
|
||||||
|
video.uploader
|
||||||
|
}}</router-link>
|
||||||
|
<!-- Verified Badge -->
|
||||||
|
<i v-if="video.uploaderVerified" class="i-fa6-solid:check ml-1" />
|
||||||
|
</div>
|
||||||
|
<PlaylistAddModal
|
||||||
|
v-if="showModal"
|
||||||
|
:video-id="getVideoId()"
|
||||||
|
:video-info="video"
|
||||||
|
@close="showModal = !showModal"
|
||||||
|
/>
|
||||||
|
<ShareModal
|
||||||
|
v-if="showShareModal"
|
||||||
|
:video-id="getVideoId()"
|
||||||
|
:current-time="currentTime"
|
||||||
|
:playlist-id="playlistId"
|
||||||
|
:playlist-index="index"
|
||||||
|
@close="showShareModal = !showShareModal"
|
||||||
|
/>
|
||||||
|
<div class="ml-auto flex flex-wrap gap-1">
|
||||||
|
<!-- Subscribe Button -->
|
||||||
|
<button class="btn flex items-center gap-1 <md:hidden" @click="downloadCurrentFrame">
|
||||||
|
{{ $t("actions.download_frame") }}<i class="i-fa6-solid:download" />
|
||||||
|
</button>
|
||||||
|
<button class="btn flex items-center" @click="showModal = !showModal">
|
||||||
|
{{ $t("actions.add_to_playlist") }}<i class="i-fa6-solid:circle-plus ml-1" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="btn"
|
||||||
|
@click="subscribeHandler"
|
||||||
|
v-text="
|
||||||
|
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
||||||
|
' - ' +
|
||||||
|
numberFormat(video.uploaderSubscriberCount)
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
<div class="flex flex-wrap gap-1">
|
||||||
|
<!-- RSS Feed button -->
|
||||||
|
<a
|
||||||
|
v-if="video.uploaderUrl"
|
||||||
|
aria-label="RSS feed"
|
||||||
|
title="RSS feed"
|
||||||
|
role="button"
|
||||||
|
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
|
||||||
|
target="_blank"
|
||||||
|
class="btn flex items-center"
|
||||||
|
>
|
||||||
|
<i class="i-fa6-solid:rss mx-1.5" />
|
||||||
|
</a>
|
||||||
|
<!-- Share Dialog -->
|
||||||
|
<button class="btn flex items-center" @click="showShareModal = !showShareModal">
|
||||||
|
<i18n-t class="lt-lg:hidden" keypath="actions.share" tag="strong"></i18n-t>
|
||||||
|
<i class="i-fa6-solid:share mx-1.5" />
|
||||||
|
</button>
|
||||||
|
<!-- YouTube -->
|
||||||
|
<WatchOnButton :link="youtubeVideoHref" />
|
||||||
|
<!-- Odysee -->
|
||||||
|
<WatchOnButton
|
||||||
|
v-if="video.lbryId"
|
||||||
|
:link="`https://odysee.com/${video.lbryId}`"
|
||||||
|
platform="Odysee"
|
||||||
|
/>
|
||||||
|
<!-- listen / watch toggle -->
|
||||||
|
<router-link
|
||||||
|
:to="toggleListenUrl"
|
||||||
|
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
||||||
|
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
||||||
|
class="btn flex items-center"
|
||||||
|
>
|
||||||
|
<i
|
||||||
|
:class="isListening ? 'i-fa6-solid:tv' : 'i-fa6-solid:headphones'"
|
||||||
|
class="mx-1.5"
|
||||||
|
/>
|
||||||
|
</router-link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div v-show="!video.error">
|
<hr class="mb-2" />
|
||||||
<div :class="isMobile ? 'flex-col' : 'flex'">
|
|
||||||
<keep-alive>
|
<div
|
||||||
<VideoPlayer
|
v-for="metaInfo in video?.metaInfo ?? []"
|
||||||
ref="videoPlayer"
|
:key="metaInfo.title"
|
||||||
:video="video"
|
class="btn my-3 flex flex-wrap cursor-default gap-2 px-4 py-2"
|
||||||
:sponsors="sponsors"
|
>
|
||||||
:selected-auto-play="selectedAutoPlay"
|
<span>{{ metaInfo.description ?? metaInfo.title }}</span>
|
||||||
:selected-auto-loop="selectedAutoLoop"
|
<a v-for="(link, linkIndex) in metaInfo.urls" :key="linkIndex" :href="link" class="underline">{{
|
||||||
@timeupdate="onTimeUpdate"
|
metaInfo.urlTexts[linkIndex]
|
||||||
@ended="onVideoEnded"
|
}}</a>
|
||||||
@navigate-next="navigateNext"
|
<br />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
||||||
|
class="btn mb-2"
|
||||||
|
@click="showDesc = !showDesc"
|
||||||
/>
|
/>
|
||||||
</keep-alive>
|
|
||||||
|
<span v-show="video?.chapters?.length > 0" class="btn ml-2">
|
||||||
|
<input id="showChapters" v-model="showChapters" type="checkbox" />
|
||||||
|
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<template v-if="showDesc">
|
||||||
|
<!-- eslint-disable-next-line vue/no-v-html -->
|
||||||
|
<div class="description break-words" v-html="purifiedDescription" />
|
||||||
|
<br />
|
||||||
|
|
||||||
|
<div
|
||||||
|
v-if="sponsors && sponsors.segments"
|
||||||
|
v-text="`${$t('video.sponsor_segments')}: ${sponsors.segments.length}`"
|
||||||
|
/>
|
||||||
|
<div v-if="video.category" v-text="`${$t('video.category')}: ${video.category}`" />
|
||||||
|
<div v-text="`${$t('video.license')}: ${video.license}`" />
|
||||||
|
<div class="capitalize" v-text="`${$t('video.visibility')}: ${video.visibility}`" />
|
||||||
|
|
||||||
|
<div v-if="video.tags" class="mt-2 flex flex-wrap gap-2">
|
||||||
|
<router-link
|
||||||
|
v-for="tag in video.tags"
|
||||||
|
:key="tag"
|
||||||
|
class="btn line-clamp-1 rounded-s px-2 py-1"
|
||||||
|
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
||||||
|
>{{ tag }}</router-link
|
||||||
|
>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
|
||||||
|
<label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
|
||||||
|
<input
|
||||||
|
id="chkAutoLoop"
|
||||||
|
v-model="selectedAutoLoop"
|
||||||
|
class="ml-1.5"
|
||||||
|
type="checkbox"
|
||||||
|
@change="onChange($event)"
|
||||||
|
/>
|
||||||
|
<br />
|
||||||
|
<label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
|
||||||
|
<select id="chkAutoPlay" v-model="selectedAutoPlay" class="select ml-1.5" @change="onChange($event)">
|
||||||
|
<option v-t="'actions.never'" value="0" />
|
||||||
|
<option v-t="'actions.playlists_only'" value="1" />
|
||||||
|
<option v-t="'actions.always'" value="2" />
|
||||||
|
</select>
|
||||||
|
|
||||||
|
<hr />
|
||||||
|
<div v-if="isMobile">
|
||||||
|
<a
|
||||||
|
v-t="`actions.${showRecs ? 'minimize_recommendations' : 'show_recommendations'}`"
|
||||||
|
class="btn mb-2"
|
||||||
|
@click="showRecs = !showRecs"
|
||||||
|
/>
|
||||||
|
<hr v-show="showRecs" />
|
||||||
|
<div v-show="showRecs">
|
||||||
|
<ContentItem
|
||||||
|
v-for="related in video.relatedStreams"
|
||||||
|
:key="related.url"
|
||||||
|
:item="related"
|
||||||
|
:prefer-listen="isListening"
|
||||||
|
class="mb-4"
|
||||||
|
height="94"
|
||||||
|
width="168"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<hr class="" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex flex-col">
|
||||||
|
<div class="">
|
||||||
|
<button
|
||||||
|
v-if="!comments?.disabled"
|
||||||
|
class="btn mb-2"
|
||||||
|
@click="toggleComments"
|
||||||
|
v-text="
|
||||||
|
`${$t(showComments ? 'actions.minimize_comments' : 'actions.show_comments')} (${numberFormat(
|
||||||
|
comments?.commentCount,
|
||||||
|
)})`
|
||||||
|
"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div v-if="!showComments" class=""></div>
|
||||||
|
<div v-else-if="!comments" class="">
|
||||||
|
<p v-t="'comment.loading'" class="mt-8 text-center"></p>
|
||||||
|
</div>
|
||||||
|
<div v-else-if="comments.disabled" class="">
|
||||||
|
<p v-t="'comment.disabled'" class="mt-8 text-center"></p>
|
||||||
|
</div>
|
||||||
|
<div v-else ref="comments" class="">
|
||||||
|
<CommentItem
|
||||||
|
v-for="comment in comments.comments"
|
||||||
|
:key="comment.commentId"
|
||||||
|
:comment="comment"
|
||||||
|
:uploader="video.uploader"
|
||||||
|
:uploader-avatar-url="video.uploaderAvatar"
|
||||||
|
:video-id="getVideoId()"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="video && !isMobile" class="w-96 flex-none">
|
||||||
<ChaptersBar
|
<ChaptersBar
|
||||||
v-if="video?.chapters?.length > 0 && showChapters"
|
v-if="video?.chapters?.length > 0 && showChapters"
|
||||||
:mobile-layout="isMobile"
|
:mobile-layout="isMobile"
|
||||||
@ -39,219 +300,6 @@
|
|||||||
:player-position="currentTime"
|
:player-position="currentTime"
|
||||||
@seek="navigate"
|
@seek="navigate"
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
<!-- video title -->
|
|
||||||
<div class="mt-2 break-words text-2xl font-bold" v-text="video.title" />
|
|
||||||
<div class="mb-3 mt-3 flex flex-wrap">
|
|
||||||
<!-- views / date -->
|
|
||||||
<div class="flex flex-auto gap-2">
|
|
||||||
<span v-t="{ path: 'video.views', args: { views: addCommas(video.views) } }" />
|
|
||||||
<span> | </span>
|
|
||||||
<span :title="new Date(video.uploadDate).toLocaleString()" v-text="uploadDate" />
|
|
||||||
</div>
|
|
||||||
<!-- Likes/dilikes -->
|
|
||||||
<div class="flex gap-2">
|
|
||||||
<template v-if="video.likes >= 0">
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="i-fa6-solid:thumbs-up" />
|
|
||||||
<strong class="ml-1" v-text="addCommas(video.likes)" />
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center">
|
|
||||||
<div class="i-fa6-solid:thumbs-down" />
|
|
||||||
<strong class="ml-1" v-text="video.dislikes >= 0 ? addCommas(video.dislikes) : '?'" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<template v-if="video.likes < 0">
|
|
||||||
<div>
|
|
||||||
<strong v-t="'video.ratings_disabled'" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<!-- Channel info & options flex container -->
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<!-- Channel Image & Info -->
|
|
||||||
<div class="flex items-center">
|
|
||||||
<img
|
|
||||||
loading="lazy"
|
|
||||||
height="48"
|
|
||||||
width="48"
|
|
||||||
:src="video.uploaderAvatar"
|
|
||||||
alt=""
|
|
||||||
class="rounded-full"
|
|
||||||
/>
|
|
||||||
<router-link v-if="video.uploaderUrl" class="link ml-1.5" :to="video.uploaderUrl">{{
|
|
||||||
video.uploader
|
|
||||||
}}</router-link>
|
|
||||||
<!-- Verified Badge -->
|
|
||||||
<i v-if="video.uploaderVerified" class="i-fa6-solid:check ml-1" />
|
|
||||||
</div>
|
|
||||||
<PlaylistAddModal
|
|
||||||
v-if="showModal"
|
|
||||||
:video-id="getVideoId()"
|
|
||||||
:video-info="video"
|
|
||||||
@close="showModal = !showModal"
|
|
||||||
/>
|
|
||||||
<ShareModal
|
|
||||||
v-if="showShareModal"
|
|
||||||
:video-id="getVideoId()"
|
|
||||||
:current-time="currentTime"
|
|
||||||
:playlist-id="playlistId"
|
|
||||||
:playlist-index="index"
|
|
||||||
@close="showShareModal = !showShareModal"
|
|
||||||
/>
|
|
||||||
<div class="ml-auto flex flex-wrap gap-1">
|
|
||||||
<!-- Subscribe Button -->
|
|
||||||
<button class="btn flex items-center gap-1 <md:hidden" @click="downloadCurrentFrame">
|
|
||||||
{{ $t("actions.download_frame") }}<i class="i-fa6-solid:download" />
|
|
||||||
</button>
|
|
||||||
<button class="btn flex items-center" @click="showModal = !showModal">
|
|
||||||
{{ $t("actions.add_to_playlist") }}<i class="i-fa6-solid:circle-plus ml-1" />
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn"
|
|
||||||
@click="subscribeHandler"
|
|
||||||
v-text="
|
|
||||||
$t('actions.' + (subscribed ? 'unsubscribe' : 'subscribe')) +
|
|
||||||
' - ' +
|
|
||||||
numberFormat(video.uploaderSubscriberCount)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div class="flex flex-wrap gap-1">
|
|
||||||
<!-- RSS Feed button -->
|
|
||||||
<a
|
|
||||||
v-if="video.uploaderUrl"
|
|
||||||
aria-label="RSS feed"
|
|
||||||
title="RSS feed"
|
|
||||||
role="button"
|
|
||||||
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${video.uploaderUrl.split('/')[2]}`"
|
|
||||||
target="_blank"
|
|
||||||
class="btn flex items-center"
|
|
||||||
>
|
|
||||||
<i class="i-fa6-solid:rss mx-1.5" />
|
|
||||||
</a>
|
|
||||||
<!-- Share Dialog -->
|
|
||||||
<button class="btn flex items-center" @click="showShareModal = !showShareModal">
|
|
||||||
<i18n-t class="lt-lg:hidden" keypath="actions.share" tag="strong"></i18n-t>
|
|
||||||
<i class="i-fa6-solid:share mx-1.5" />
|
|
||||||
</button>
|
|
||||||
<!-- YouTube -->
|
|
||||||
<WatchOnButton :link="youtubeVideoHref" />
|
|
||||||
<!-- Odysee -->
|
|
||||||
<WatchOnButton
|
|
||||||
v-if="video.lbryId"
|
|
||||||
:link="`https://odysee.com/${video.lbryId}`"
|
|
||||||
platform="Odysee"
|
|
||||||
/>
|
|
||||||
<!-- listen / watch toggle -->
|
|
||||||
<router-link
|
|
||||||
:to="toggleListenUrl"
|
|
||||||
:aria-label="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
|
||||||
:title="(isListening ? 'Watch ' : 'Listen to ') + video.title"
|
|
||||||
class="btn flex items-center"
|
|
||||||
>
|
|
||||||
<i :class="isListening ? 'i-fa6-solid:tv' : 'i-fa6-solid:headphones'" class="mx-1.5" />
|
|
||||||
</router-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr class="mb-2" />
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-for="metaInfo in video?.metaInfo ?? []"
|
|
||||||
:key="metaInfo.title"
|
|
||||||
class="btn my-3 flex flex-wrap cursor-default gap-2 px-4 py-2"
|
|
||||||
>
|
|
||||||
<span>{{ metaInfo.description ?? metaInfo.title }}</span>
|
|
||||||
<a v-for="(link, linkIndex) in metaInfo.urls" :key="linkIndex" :href="link" class="underline">{{
|
|
||||||
metaInfo.urlTexts[linkIndex]
|
|
||||||
}}</a>
|
|
||||||
<br />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<button
|
|
||||||
v-t="`actions.${showDesc ? 'minimize_description' : 'show_description'}`"
|
|
||||||
class="btn mb-2"
|
|
||||||
@click="showDesc = !showDesc"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<span v-show="video?.chapters?.length > 0" class="btn ml-2">
|
|
||||||
<input id="showChapters" v-model="showChapters" type="checkbox" />
|
|
||||||
<label v-t="'actions.show_chapters'" class="ml-2" for="showChapters" />
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<template v-if="showDesc">
|
|
||||||
<!-- eslint-disable-next-line vue/no-v-html -->
|
|
||||||
<div class="description break-words" v-html="purifiedDescription" />
|
|
||||||
<br />
|
|
||||||
|
|
||||||
<div
|
|
||||||
v-if="sponsors && sponsors.segments"
|
|
||||||
v-text="`${$t('video.sponsor_segments')}: ${sponsors.segments.length}`"
|
|
||||||
/>
|
|
||||||
<div v-if="video.category" v-text="`${$t('video.category')}: ${video.category}`" />
|
|
||||||
<div v-text="`${$t('video.license')}: ${video.license}`" />
|
|
||||||
<div class="capitalize" v-text="`${$t('video.visibility')}: ${video.visibility}`" />
|
|
||||||
|
|
||||||
<div v-if="video.tags" class="mt-2 flex flex-wrap gap-2">
|
|
||||||
<router-link
|
|
||||||
v-for="tag in video.tags"
|
|
||||||
:key="tag"
|
|
||||||
class="btn line-clamp-1 rounded-s px-2 py-1"
|
|
||||||
:to="`/results?search_query=${encodeURIComponent(tag)}`"
|
|
||||||
>{{ tag }}</router-link
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<label for="chkAutoLoop"><strong v-text="`${$t('actions.loop_this_video')}:`" /></label>
|
|
||||||
<input id="chkAutoLoop" v-model="selectedAutoLoop" class="ml-1.5" type="checkbox" @change="onChange($event)" />
|
|
||||||
<br />
|
|
||||||
<label for="chkAutoPlay"><strong v-text="`${$t('actions.auto_play_next_video')}:`" /></label>
|
|
||||||
<select id="chkAutoPlay" v-model="selectedAutoPlay" class="select ml-1.5" @change="onChange($event)">
|
|
||||||
<option v-t="'actions.never'" value="0" />
|
|
||||||
<option v-t="'actions.playlists_only'" value="1" />
|
|
||||||
<option v-t="'actions.always'" value="2" />
|
|
||||||
</select>
|
|
||||||
|
|
||||||
<hr />
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 sm:grid-cols-4 xl:grid-cols-5">
|
|
||||||
<div class="sm:col-span-3 xl:col-span-4">
|
|
||||||
<button
|
|
||||||
v-if="!comments?.disabled"
|
|
||||||
class="btn mb-2"
|
|
||||||
@click="toggleComments"
|
|
||||||
v-text="
|
|
||||||
`${$t(showComments ? 'actions.minimize_comments' : 'actions.show_comments')} (${numberFormat(
|
|
||||||
comments?.commentCount,
|
|
||||||
)})`
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div v-if="!showComments" class="sm:col-span-3 xl:col-span-4"></div>
|
|
||||||
<div v-else-if="!comments" class="sm:col-span-3 xl:col-span-4">
|
|
||||||
<p v-t="'comment.loading'" class="mt-8 text-center"></p>
|
|
||||||
</div>
|
|
||||||
<div v-else-if="comments.disabled" class="sm:col-span-3 xl:col-span-4">
|
|
||||||
<p v-t="'comment.disabled'" class="mt-8 text-center"></p>
|
|
||||||
</div>
|
|
||||||
<div v-else ref="comments" class="sm:col-span-3 xl:col-span-4">
|
|
||||||
<CommentItem
|
|
||||||
v-for="comment in comments.comments"
|
|
||||||
:key="comment.commentId"
|
|
||||||
:comment="comment"
|
|
||||||
:uploader="video.uploader"
|
|
||||||
:uploader-avatar-url="video.uploaderAvatar"
|
|
||||||
:video-id="getVideoId()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-if="video" class="order-first sm:order-last">
|
|
||||||
<PlaylistVideos
|
<PlaylistVideos
|
||||||
v-if="playlist"
|
v-if="playlist"
|
||||||
:playlist-id="playlistId"
|
:playlist-id="playlistId"
|
||||||
@ -276,7 +324,6 @@
|
|||||||
width="168"
|
width="168"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<hr class="sm:hidden" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</LoadingIndicatorPage>
|
</LoadingIndicatorPage>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user