mirror of
https://github.com/VueTubeApp/VueTube
synced 2024-11-22 11:15:14 +00:00
658 lines
18 KiB
Vue
658 lines
18 KiB
Vue
<template>
|
|
<div id="watch-body" class="background">
|
|
<div id="player-container">
|
|
<!-- // TODO: move component to default.vue -->
|
|
<!-- // TODO: pass sources through vuex instead of props -->
|
|
<player
|
|
v-if="sources.length > 0 && video.title && video.channelName"
|
|
ref="player"
|
|
:video="video"
|
|
:sources="sources"
|
|
:recommends="recommends"
|
|
:disabled="saveDialog"
|
|
/>
|
|
</div>
|
|
|
|
<div
|
|
id="content-container"
|
|
:class="{
|
|
'overflow-y-auto': !showComments,
|
|
'overflow-y-hidden': showComments,
|
|
}"
|
|
>
|
|
<v-card v-if="loaded" class="background rounded-0" flat>
|
|
<div
|
|
v-ripple
|
|
class="d-flex justify-space-between align-start px-4 pt-4"
|
|
@click="showMore = !showMore"
|
|
>
|
|
<div class="d-flex flex-column">
|
|
<v-card-title
|
|
v-emoji
|
|
class="pa-0 text-wrap"
|
|
style="font-size: 0.95rem; line-height: 1.15rem"
|
|
v-text="video.title"
|
|
/>
|
|
<v-card-text
|
|
style="font-size: 0.75rem"
|
|
class="background--text pa-0"
|
|
:class="
|
|
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
|
|
"
|
|
>
|
|
<div>
|
|
<template
|
|
v-for="text in video.metadata.contents.find(
|
|
(content) => content.slimVideoInformationRenderer
|
|
).slimVideoInformationRenderer.collapsedSubtitle.runs"
|
|
>{{ text.text }}
|
|
</template>
|
|
</div>
|
|
</v-card-text>
|
|
</div>
|
|
<v-icon v-if="showMore" class="ml-4">mdi-chevron-up</v-icon>
|
|
<v-icon v-else class="ml-4">mdi-chevron-down</v-icon>
|
|
</div>
|
|
<div
|
|
class="d-flex justify-space-around"
|
|
:class="
|
|
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
|
|
? $vuetify.theme.dark
|
|
? 'background lighten-1'
|
|
: 'background darken-1'
|
|
: ''
|
|
"
|
|
:style="{
|
|
borderRadius: $store.state.tweaks.roundWatch
|
|
? `${$store.state.tweaks.roundTweak / 2}rem`
|
|
: '0',
|
|
margin:
|
|
$store.state.tweaks.roundWatch &&
|
|
$store.state.tweaks.roundTweak > 0
|
|
? '1rem'
|
|
: '0',
|
|
}"
|
|
>
|
|
<v-btn
|
|
v-for="(item, index) in interactions"
|
|
:key="index"
|
|
text
|
|
fab
|
|
class="vertical-button"
|
|
elevation="0"
|
|
style="
|
|
width: 4.2rem !important;
|
|
height: 4.2rem !important;
|
|
text-transform: none !important;
|
|
"
|
|
:disabled="item.disabled"
|
|
@click="callMethodByName(item.actionName)"
|
|
>
|
|
<v-icon v-text="item.icon" />
|
|
<div
|
|
class="mt-1"
|
|
style="font-size: 0.6rem"
|
|
v-text="item.value || item.name"
|
|
/>
|
|
</v-btn>
|
|
<!-- End Scrolling Div For Interactions --->
|
|
<!-- <hr /> -->
|
|
</div>
|
|
<!-- <v-bottom-sheet
|
|
v-model="showMore"
|
|
color="background"
|
|
style="z-index: 9999999"
|
|
>
|
|
<v-sheet style="padding: 12px">
|
|
<v-btn block @click="showMore = !showMore"
|
|
><v-icon>mdi-chevron-down</v-icon></v-btn
|
|
><br />
|
|
|
|
<slim-video-description-renderer
|
|
class="scroll-y"
|
|
:render="video.renderedData.description"
|
|
/>
|
|
</v-sheet>
|
|
</v-bottom-sheet> -->
|
|
|
|
<!-- <v-bottom-sheet v-model="share" color="background" style="z-index: 9999999">
|
|
<v-sheet style="padding: 1em">
|
|
<div class="scroll-y">
|
|
{{ response.renderedData.description }}
|
|
</div>
|
|
</v-sheet>
|
|
</v-bottom-sheet> -->
|
|
</v-card>
|
|
|
|
<v-divider
|
|
v-if="
|
|
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
|
"
|
|
/>
|
|
|
|
<!-- Channel Bar -->
|
|
<div v-if="loaded">
|
|
<v-card
|
|
flat
|
|
class="channel-section py-2 px-3 background"
|
|
:class="
|
|
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
|
|
? $vuetify.theme.dark
|
|
? 'background lighten-1'
|
|
: 'background darken-1'
|
|
: ''
|
|
"
|
|
to="/channel"
|
|
:style="{
|
|
borderRadius: $store.state.tweaks.roundWatch
|
|
? `${$store.state.tweaks.roundTweak / 2}rem`
|
|
: '0',
|
|
margin:
|
|
$store.state.tweaks.roundWatch &&
|
|
$store.state.tweaks.roundTweak > 0
|
|
? '1rem'
|
|
: '0',
|
|
}"
|
|
@click="$store.dispatch('channel/fetchChannel', video.channelUrl)"
|
|
>
|
|
<div id="details">
|
|
<div class="avatar-link mr-3">
|
|
<v-img class="avatar-thumbnail" :src="video.channelImg" />
|
|
</div>
|
|
<div v-emoji class="channel-byline">
|
|
<div class="channel-name" v-text="video.channelName" />
|
|
<div
|
|
class="caption background--text"
|
|
:class="
|
|
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
|
|
"
|
|
v-text="video.channelSubs + ' subscribers'"
|
|
/>
|
|
</div>
|
|
</div>
|
|
<div class="primary--text" style="text-transform: uppercase">
|
|
subscribe
|
|
</div>
|
|
</v-card>
|
|
</div>
|
|
|
|
<v-divider
|
|
v-if="
|
|
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
|
"
|
|
/>
|
|
|
|
<!-- Description -->
|
|
<div v-if="showMore">
|
|
<div class="scroll-y ma-4 pt-1">
|
|
<slim-video-description-renderer
|
|
:render="video.renderedData.description"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<v-divider
|
|
v-if="
|
|
showMore &&
|
|
(!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch)
|
|
"
|
|
/>
|
|
|
|
<!-- Comments -->
|
|
<div v-if="loaded && video.commentData" @click="toggleComment">
|
|
<v-card
|
|
v-ripple
|
|
flat
|
|
tile
|
|
class="comment-renderer px-3 background"
|
|
:class="
|
|
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
|
|
? $vuetify.theme.dark
|
|
? 'background lighten-1'
|
|
: 'background darken-1'
|
|
: ''
|
|
"
|
|
:style="{
|
|
borderRadius: $store.state.tweaks.roundWatch
|
|
? `${$store.state.tweaks.roundTweak / 2}rem !important`
|
|
: '0',
|
|
margin:
|
|
$store.state.tweaks.roundWatch &&
|
|
$store.state.tweaks.roundTweak > 0
|
|
? '1rem'
|
|
: '0',
|
|
}"
|
|
>
|
|
<v-card-text class="comment-count keep-spaces px-0">
|
|
<template v-for="text in video.commentData.headerText.runs">
|
|
<template v-if="text.bold">
|
|
<strong :key="text.text">{{ text.text }}</strong>
|
|
</template>
|
|
<template v-else>{{ text.text }}</template>
|
|
</template>
|
|
</v-card-text>
|
|
<v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon>
|
|
<v-icon v-else dense>mdi-unfold-more-horizontal</v-icon>
|
|
</v-card>
|
|
</div>
|
|
|
|
<v-divider
|
|
v-if="
|
|
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
|
"
|
|
/>
|
|
|
|
<div
|
|
v-if="loaded && video.commentData"
|
|
:class="showComments ? 'comments-open' : ''"
|
|
class="scroll-y comments"
|
|
>
|
|
<mainCommentRenderer
|
|
v-model="showComments"
|
|
:comment-data="video.commentData"
|
|
:default-continuation="video.commentContinuation"
|
|
></mainCommentRenderer>
|
|
</div>
|
|
|
|
<!-- <swipeable-bottom-sheet
|
|
:v-model="showComments"
|
|
style="z-index: 9999999"
|
|
></swipeable-bottom-sheet> -->
|
|
|
|
<!-- Related Videos -->
|
|
<div v-if="!loaded">
|
|
<v-skeleton-loader
|
|
type="list-item-two-line, actions, divider, list-item-avatar, divider, list-item-three-line"
|
|
/>
|
|
<vid-load-renderer :count="5" />
|
|
</div>
|
|
<item-section-renderer
|
|
v-else
|
|
:render="recommends"
|
|
:style="{
|
|
marginTop: $store.state.tweaks.roundTweak > 0 ? '1rem' : '0',
|
|
}"
|
|
/>
|
|
</div>
|
|
<v-dialog v-model="saveDialog" width="500">
|
|
<v-card
|
|
class="rounded-lg"
|
|
:class="
|
|
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
|
"
|
|
>
|
|
<v-card-title class="text-h5">Save To Playlist</v-card-title>
|
|
<v-spacer></v-spacer>
|
|
<v-checkbox
|
|
v-for="(playlist, index) in playlists"
|
|
:key="index"
|
|
v-model="playlistsCheckbox[index]"
|
|
class="mx-5"
|
|
:label="playlist.name"
|
|
@change="updatePlaylist($event, index)"
|
|
/>
|
|
<v-divider />
|
|
<v-card-actions>
|
|
<v-spacer></v-spacer>
|
|
<v-btn color="primary" text @click="saveDialog = false"> Done </v-btn>
|
|
</v-card-actions>
|
|
</v-card></v-dialog
|
|
>
|
|
</div>
|
|
</template>
|
|
|
|
<script>
|
|
import { Share } from "@capacitor/share";
|
|
import { getCpn } from "~/plugins/utils";
|
|
import player from "~/components/Player/index.vue";
|
|
import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
|
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
|
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
|
|
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
|
|
|
|
import backType from "~/plugins/classes/backType";
|
|
|
|
export default {
|
|
components: {
|
|
player,
|
|
VidLoadRenderer,
|
|
ItemSectionRenderer,
|
|
mainCommentRenderer,
|
|
SlimVideoDescriptionRenderer,
|
|
},
|
|
layout: "empty",
|
|
// transition(to) { // TODO: fix layout switching
|
|
// return to.name == "watch"
|
|
// ? { name: "slide-up", mode: "" }
|
|
// : { name: "slide-down", mode: "" };
|
|
// },
|
|
data: function () {
|
|
return this.initializeState();
|
|
},
|
|
|
|
computed: {
|
|
playlists() {
|
|
return this.$store.state.playlist.playlists;
|
|
},
|
|
},
|
|
watch: {
|
|
// Watch for change in the route query string (in this case, ?v=xxxxxxxx to ?v=yyyyyyyy)
|
|
$route: {
|
|
deep: true,
|
|
handler(newRt, oldRt) {
|
|
if (newRt.query.v && newRt.query.v != oldRt.query.v) {
|
|
// Exit fullscreen if currently in fullscreen
|
|
// if (this.$refs.player) this.$refs.player.webkitExitFullscreen();
|
|
// Reset player and run getVideo function again
|
|
// this.startTime = Math.floor(Date.now() / 1000);
|
|
// this.getVideo();
|
|
clearInterval(this.interval);
|
|
Object.assign(this.$data, this.initializeState());
|
|
this.mountedInit();
|
|
}
|
|
},
|
|
},
|
|
},
|
|
|
|
mounted() {
|
|
this.mountedInit();
|
|
this.$vuetube.resetBackActions();
|
|
},
|
|
|
|
beforeDestroy() {
|
|
clearInterval(this.interval);
|
|
},
|
|
|
|
methods: {
|
|
getVideo() {
|
|
this.loaded = false;
|
|
|
|
this.$youtube.getVid(this.$route.query.v).then((result) => {
|
|
// TODO: sourt "tiny" (no qualityLabel) as audio and rest as video
|
|
this.sources = result.availableResolutionsAdaptive;
|
|
console.log("Video info data", result);
|
|
this.video = result;
|
|
|
|
//--- Content Stuff ---//
|
|
// NOTE: extractor likes are broken, using RYD likes instead
|
|
// this.likes = result.metadata.likes.toLocaleString();
|
|
// this.interactions[0].value = result.metadata.likes.toLocaleString();
|
|
this.loaded = true;
|
|
this.recommends = result.renderedData.recommendations;
|
|
console.log("recommendations:", this.recommends);
|
|
|
|
// Store To History
|
|
this.$store.commit("history/addHistory", {
|
|
id: this.video.id,
|
|
title: this.video.title,
|
|
channel: this.video.channelName,
|
|
});
|
|
|
|
this.playlistsCheckbox = this.playlists.map(
|
|
(playlist) =>
|
|
playlist.videos.findIndex(
|
|
(playlistVideo) => playlistVideo.id === this.video.id
|
|
) !== -1
|
|
);
|
|
|
|
//--- API WatchTime call ---//
|
|
if (this.$store.state.watchTelemetry) {
|
|
this.playbackTracking = result.playbackTracking;
|
|
this.st = 0;
|
|
this.cpn = getCpn();
|
|
this.initWatchTime().then(() => {
|
|
this.sendWatchTime();
|
|
this.interval = setInterval(this.sendWatchTime, 60000);
|
|
});
|
|
}
|
|
});
|
|
|
|
this.$youtube.getReturnYoutubeDislike(this.$route.query.v, (data) => {
|
|
this.likes = data.likes.toLocaleString();
|
|
this.dislikes = data.dislikes.toLocaleString();
|
|
this.interactions[0].value = data.likes.toLocaleString();
|
|
this.interactions[1].value = data.dislikes.toLocaleString();
|
|
});
|
|
},
|
|
callMethodByName(name) {
|
|
// Helper function needed because of issues when directly calling method
|
|
// using item.action in the v-for loop
|
|
this[name]();
|
|
},
|
|
async share() {
|
|
// this.share = !this.share;
|
|
await Share.share({
|
|
title: this.video.title,
|
|
text: this.video.title,
|
|
url:
|
|
"https://youtu.be/" +
|
|
this.$route.query.v +
|
|
"?t=" +
|
|
Math.round(this.$refs.player.getPlayer().currentTime) +
|
|
"s",
|
|
dialogTitle: "Share video",
|
|
});
|
|
},
|
|
sendWatchTime() {
|
|
const player = this.$refs.player.getPlayer();
|
|
const rt = Math.floor(Date.now() / 1000) - this.startTime;
|
|
const params = {
|
|
cpn: this.cpn,
|
|
rt: rt,
|
|
rti: rt,
|
|
rtn: rt,
|
|
cmt: player.currentTime,
|
|
et: player.currentTime,
|
|
st: this.st,
|
|
state: player.paused ? "paused" : "playing",
|
|
volume: 100,
|
|
muted: 0,
|
|
fmt: 396,
|
|
};
|
|
this.st = player.currentTime;
|
|
this.$youtube.saveApiStats(
|
|
params,
|
|
this.playbackTracking.videostatsWatchtimeUrl.baseUrl
|
|
);
|
|
},
|
|
|
|
async initWatchTime() {
|
|
await this.$youtube.saveApiStats(
|
|
{
|
|
cpn: this.cpn,
|
|
fmt: 243,
|
|
rtn: Math.floor(Date.now() / 1000) - this.startTime,
|
|
rt: Math.floor(Date.now() / 1000) - this.startTime,
|
|
muted: 0,
|
|
},
|
|
this.playbackTracking.videostatsPlaybackUrl.baseUrl
|
|
);
|
|
},
|
|
|
|
initializeState() {
|
|
return {
|
|
interactions: [
|
|
{
|
|
name: "Likes",
|
|
icon: "mdi-thumb-up-outline",
|
|
// action: this.like(),
|
|
actionName: "like",
|
|
value: this.likes,
|
|
disabled: true,
|
|
},
|
|
{
|
|
name: "Dislikes",
|
|
icon: "mdi-thumb-down-outline",
|
|
// action: this.dislike(),
|
|
actionName: "dislike",
|
|
value: this.dislikes,
|
|
disabled: true,
|
|
},
|
|
{
|
|
name: "Share",
|
|
icon: "mdi-share-outline",
|
|
// action: this.share(),
|
|
actionName: "share",
|
|
disabled: false,
|
|
},
|
|
{
|
|
name: "Save",
|
|
icon: "mdi-plus-box-multiple-outline",
|
|
// action: this.save()
|
|
actionName: "save",
|
|
disabled: false,
|
|
},
|
|
// {
|
|
// name: "Quality",
|
|
// icon: "mdi-high-definition",
|
|
// actionName: "quality",
|
|
// disabled: false,
|
|
// },
|
|
// {
|
|
// name: "Speed",
|
|
// icon: "mdi-speedometer",
|
|
// actionName: "speed",
|
|
// disabled: false,
|
|
// },
|
|
],
|
|
showMore: false,
|
|
showComments: false,
|
|
// share: false,
|
|
sources: [],
|
|
recommends: null,
|
|
loaded: false,
|
|
interval: null,
|
|
video: null,
|
|
backHierarchy: [],
|
|
saveDialog: false,
|
|
playlistsCheckbox: [],
|
|
};
|
|
},
|
|
|
|
mountedInit() {
|
|
this.startTime = Math.floor(Date.now() / 1000);
|
|
this.getVideo();
|
|
// Reset vertical scrolling
|
|
const scrollableList = document.querySelectorAll(".overflow-y-auto");
|
|
scrollableList.forEach((scrollable) => {
|
|
scrollable.scrollTo(0, 0);
|
|
});
|
|
},
|
|
|
|
// Toggle this.showComments to true or false. If it is true, then add the dismiss function to backStack.
|
|
toggleComment() {
|
|
document.getElementById("content-container").scrollTo(0, 0);
|
|
this.showComments = !this.showComments;
|
|
if (this.showComments) {
|
|
const dismissComment = new backType(
|
|
() => {
|
|
this.showComments = false;
|
|
},
|
|
() => {
|
|
return this.showComments;
|
|
}
|
|
);
|
|
this.$vuetube.addBackAction(dismissComment);
|
|
}
|
|
},
|
|
|
|
save() {
|
|
this.saveDialog = true;
|
|
},
|
|
|
|
updatePlaylist(event, index) {
|
|
if (event) {
|
|
this.$store.commit("playlist/addToPlaylist", {
|
|
video: {
|
|
id: this.video.id,
|
|
title: this.video.title,
|
|
channel: this.video.channelName,
|
|
},
|
|
index,
|
|
});
|
|
} else {
|
|
this.$store.commit("playlist/removeFromPlaylist", {
|
|
video: this.video,
|
|
playlistIndex: index,
|
|
});
|
|
}
|
|
},
|
|
},
|
|
};
|
|
</script>
|
|
|
|
<style>
|
|
.comments {
|
|
left: 0;
|
|
right: 0;
|
|
bottom: 0;
|
|
opacity: 0;
|
|
z-index: 2;
|
|
height: 100%;
|
|
max-height: 100%;
|
|
position: absolute;
|
|
pointer-events: none;
|
|
transform: translateY(100%);
|
|
transition: transform 0.3s ease-in-out, opacity 0.3s ease-in-out;
|
|
}
|
|
.comments-open {
|
|
transform: translatey(0);
|
|
pointer-events: auto;
|
|
opacity: 1;
|
|
}
|
|
#watch-body {
|
|
height: 100%;
|
|
max-height: 100vh;
|
|
overflow: hidden;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
#content-container {
|
|
height: 100%;
|
|
position: relative;
|
|
}
|
|
|
|
.vertical-button span.v-btn__content {
|
|
flex-direction: column;
|
|
justify-content: space-around;
|
|
}
|
|
|
|
.channel-section,
|
|
.comment-renderer {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.channel-section #details,
|
|
.comment-renderer .comment-count {
|
|
flex-grow: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
min-width: 0;
|
|
}
|
|
|
|
.channel-section .channel-byline {
|
|
min-width: 0;
|
|
}
|
|
|
|
.channel-section .avatar-thumbnail {
|
|
border-radius: 50%;
|
|
width: 35px;
|
|
height: 35px;
|
|
}
|
|
|
|
.channel-section .channel-name {
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
|
|
.keep-spaces {
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
.v-card__title {
|
|
word-break: break-word;
|
|
}
|
|
</style>
|