mirror of
https://github.com/VueTubeApp/VueTube
synced 2025-01-06 23:51:13 +00:00
feat: comments fully functional
This commit is contained in:
parent
291f961186
commit
3337a08095
10 changed files with 317 additions and 125 deletions
|
@ -1,87 +0,0 @@
|
|||
<template>
|
||||
<div id="comment">
|
||||
<a
|
||||
:href="this.$rendererUtils.getNavigationEndpoints(comment.authorEndpoint)"
|
||||
class="avatar-link pt-2"
|
||||
>
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
:src="
|
||||
comment.authorThumbnail.thumbnails[
|
||||
comment.authorThumbnail.thumbnails.length - 1
|
||||
].url
|
||||
"
|
||||
/>
|
||||
</a>
|
||||
<v-card-text class="comment-info pt-2">
|
||||
<div
|
||||
v-for="title in comment.title.runs"
|
||||
:key="title.text"
|
||||
style="margin-top: 0.5em"
|
||||
class="vid-title"
|
||||
>
|
||||
{{ title.text }}
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="caption background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
v-text="parseBottom(comment)"
|
||||
/>
|
||||
</v-card-text>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
}
|
||||
|
||||
.vid-title {
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.avatar-thumbnail {
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
#details {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media screen and (orientation: landscape) {
|
||||
.entry {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
#details {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["comment"],
|
||||
|
||||
methods: {
|
||||
parseBottom(comment) {
|
||||
const bottomText = [
|
||||
comment.subscriberCountText?.runs[0].text,
|
||||
comment.videoCountText?.runs.map((run) => run.text).join(" "),
|
||||
];
|
||||
return bottomText.join(" · ");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -0,0 +1,90 @@
|
|||
<template>
|
||||
<div class="comment-thread" v-if="commentRenderer">
|
||||
<a
|
||||
:href="
|
||||
this.$rendererUtils.getNavigationEndpoints(
|
||||
commentRenderer.authorEndpoint
|
||||
)
|
||||
"
|
||||
class="avatar-link"
|
||||
>
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
:src="
|
||||
commentRenderer.authorThumbnail.thumbnails[
|
||||
commentRenderer.authorThumbnail.thumbnails.length - 1
|
||||
].url
|
||||
"
|
||||
/>
|
||||
</a>
|
||||
<div class="comment-content">
|
||||
<div class="comment-content--header">
|
||||
<span class="font-weight-bold subtitle-2 pr-1">
|
||||
{{ commentRenderer.authorText.runs[0].text }}
|
||||
</span>
|
||||
<span
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
class="background--text subtitle-2"
|
||||
>
|
||||
{{ commentRenderer.publishedTimeText.runs[0].text }}
|
||||
</span>
|
||||
</div>
|
||||
<p class="comment-text">
|
||||
<template v-for="text in commentRenderer.contentText.runs">{{
|
||||
text.text
|
||||
}}</template>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
}
|
||||
|
||||
.avatar-thumbnail {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.comment-thread {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: auto;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.comment-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-basis: auto;
|
||||
}
|
||||
|
||||
.comment-content--header {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.comment-text {
|
||||
white-space: pre-line;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["comment"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
commentRenderer: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.commentRenderer = this.comment?.comment?.commentRenderer;
|
||||
},
|
||||
};
|
||||
</script>
|
59
NUXT/components/Comments/commentsHeaderRenderer.vue
Normal file
59
NUXT/components/Comments/commentsHeaderRenderer.vue
Normal file
|
@ -0,0 +1,59 @@
|
|||
<template>
|
||||
<div class="comment-header" v-if="boxRenderer">
|
||||
<div class="avatar-container">
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
:src="
|
||||
boxRenderer.authorThumbnail.thumbnails[
|
||||
boxRenderer.authorThumbnail.thumbnails.length - 1
|
||||
].url
|
||||
"
|
||||
/>
|
||||
</div>
|
||||
<v-text-field disabled>
|
||||
{{
|
||||
boxRenderer.placeholderText.runs[
|
||||
boxRenderer.placeholderText.runs.length - 1
|
||||
].text
|
||||
}}
|
||||
</v-text-field>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
}
|
||||
|
||||
.avatar-thumbnail {
|
||||
margin-top: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
}
|
||||
|
||||
.comment-header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: auto;
|
||||
padding: 10px;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["comment"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
boxRenderer: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
|
||||
},
|
||||
};
|
||||
</script>
|
88
NUXT/components/Comments/mainCommentRenderer.vue
Normal file
88
NUXT/components/Comments/mainCommentRenderer.vue
Normal file
|
@ -0,0 +1,88 @@
|
|||
<template>
|
||||
<dialog-base>
|
||||
<template v-slot:header>
|
||||
<v-toolbar-title>
|
||||
<template v-for="text in commentData.headerText.runs">
|
||||
<template v-if="text.bold">
|
||||
<strong :key="text.text">{{ text.text }}</strong>
|
||||
</template>
|
||||
<template v-else>{{ text.text }}</template>
|
||||
</template>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon dark @click="$emit('changeState', false)">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<div class="commentList">
|
||||
<template v-for="commentItems in comments">
|
||||
<v-list-item
|
||||
v-for="(comment, index) in commentItems.reloadContinuationItemsCommand
|
||||
.continuationItems"
|
||||
:key="index"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||
:is="Object.keys(comment)[0]"
|
||||
:comment="comment[Object.keys(comment)[0]]"
|
||||
></component>
|
||||
</v-list-item>
|
||||
</template>
|
||||
</div>
|
||||
<div class="loading" v-if="loading">
|
||||
<v-sheet color="background" v-for="i in !comments ? 10 : 3" :key="i">
|
||||
<v-skeleton-loader type="list-item-avatar-three-line, actions" />
|
||||
</v-sheet>
|
||||
</div>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import dialogBase from "~/components/dialogBase.vue";
|
||||
import commentsHeaderRenderer from "~/components/Comments/commentsHeaderRenderer.vue";
|
||||
import commentThreadRenderer from "~/components/Comments/commentThreadRenderer.vue";
|
||||
|
||||
export default {
|
||||
props: ["continuation", "commentData", "showComments"],
|
||||
|
||||
model: {
|
||||
prop: "showComments",
|
||||
event: "changeState",
|
||||
},
|
||||
|
||||
components: {
|
||||
dialogBase,
|
||||
commentsHeaderRenderer,
|
||||
commentThreadRenderer,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: true,
|
||||
comments: [],
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
this.paginate();
|
||||
},
|
||||
|
||||
methods: {
|
||||
getComponents() {
|
||||
return this.$options.components;
|
||||
},
|
||||
|
||||
paginate() {
|
||||
this.loading = true;
|
||||
this.$youtube
|
||||
.getContinuation(this.continuation, "next", "web")
|
||||
.then((result) => {
|
||||
this.comments = this.comments.concat(
|
||||
result.data.onResponseReceivedEndpoints
|
||||
);
|
||||
console.log("comments", this.comments);
|
||||
if (this.comments) this.loading = false;
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -27,6 +27,7 @@ export default VDialog.extend({
|
|||
contentClasses() {
|
||||
return {
|
||||
"swipeable-bottom-sheet__content": true,
|
||||
"swipeable-bottom-sheet__content--active": this.isActive,
|
||||
};
|
||||
},
|
||||
},
|
||||
|
|
|
@ -27,12 +27,18 @@
|
|||
|
||||
&.v-dialog:not(.v-dialog--fullscreen)
|
||||
max-height: 100%
|
||||
height: 100%
|
||||
|
||||
.swipeable-bottom-sheet__content
|
||||
position: sticky !important
|
||||
bottom: 0
|
||||
top: unset
|
||||
overflow-y: auto
|
||||
align-items: center
|
||||
display: flex
|
||||
justify-content: center
|
||||
pointer-events: none
|
||||
position: absolute
|
||||
transition: .2s map-get($transition, 'fast-in-fast-out'), z-index 1ms
|
||||
width: 100%
|
||||
height: 100%
|
||||
outline: none
|
||||
inset: 0
|
||||
|
||||
|
||||
|
|
24
NUXT/components/dialogBase.vue
Normal file
24
NUXT/components/dialogBase.vue
Normal file
|
@ -0,0 +1,24 @@
|
|||
<template>
|
||||
<v-card class="dialog-base">
|
||||
<div class="toolbar-container">
|
||||
<v-toolbar color="background" flat>
|
||||
<slot name="header"></slot>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
<div class="dialog-body background">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style lang="sass">
|
||||
.dialog-base
|
||||
display: flex
|
||||
flex-direction: column
|
||||
height: 100%
|
||||
|
||||
.dialog-body
|
||||
overflow-y: auto
|
||||
height: 100%
|
||||
</style>
|
|
@ -166,33 +166,11 @@
|
|||
attach="#content-container"
|
||||
v-if="loaded && video.commentData"
|
||||
>
|
||||
<v-card height="100%">
|
||||
<div
|
||||
class="toolbar-container"
|
||||
style="position: sticky; inset: -1px; z-index: 2"
|
||||
>
|
||||
<v-toolbar color="background" flat>
|
||||
<v-toolbar-title>
|
||||
<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-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon dark @click="showComments = !showComments">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-toolbar>
|
||||
<v-divider></v-divider>
|
||||
</div>
|
||||
<div scrollable>
|
||||
<v-sheet color="background" v-for="i in 10" :key="i">
|
||||
<v-skeleton-loader type="list-item-avatar-three-line, actions" />
|
||||
</v-sheet>
|
||||
</div>
|
||||
</v-card>
|
||||
<mainCommentRenderer
|
||||
:continuation="video.commentContinuation"
|
||||
:commentData="video.commentData"
|
||||
v-model="showComments"
|
||||
></mainCommentRenderer>
|
||||
</swipeable-bottom-sheet>
|
||||
|
||||
<!-- <swipeable-bottom-sheet
|
||||
|
@ -220,6 +198,7 @@ import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDe
|
|||
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
||||
import vuetubePlayer from "~/components/Player/index.vue";
|
||||
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
||||
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
|
||||
import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
|
||||
|
||||
export default {
|
||||
|
@ -230,6 +209,7 @@ export default {
|
|||
vuetubePlayer,
|
||||
ItemSectionRenderer,
|
||||
SwipeableBottomSheet,
|
||||
mainCommentRenderer,
|
||||
},
|
||||
|
||||
data: function () {
|
||||
|
@ -254,13 +234,15 @@ export default {
|
|||
},
|
||||
},
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.mountedInit();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
},
|
||||
|
||||
methods: {
|
||||
getVideo() {
|
||||
this.loaded = false;
|
||||
|
|
|
@ -117,7 +117,7 @@ class Innertube {
|
|||
continuation: continuation,
|
||||
};
|
||||
let url;
|
||||
switch (type) {
|
||||
switch (type.toLowerCase()) {
|
||||
case "browse":
|
||||
url = `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`;
|
||||
break;
|
||||
|
@ -344,6 +344,19 @@ class Innertube {
|
|||
(content) => content.commentsEntryPointHeaderRenderer
|
||||
)?.commentsEntryPointHeaderRenderer,
|
||||
playbackTracking: responseInfo.playbackTracking,
|
||||
commentContinuation: responseNext.engagementPanels
|
||||
.find(
|
||||
(panel) =>
|
||||
panel.engagementPanelSectionListRenderer.panelIdentifier ==
|
||||
"engagement-panel-comments-section"
|
||||
)
|
||||
?.engagementPanelSectionListRenderer.content.sectionListRenderer.contents.find(
|
||||
(content) => content.itemSectionRenderer
|
||||
)
|
||||
?.itemSectionRenderer.contents.find(
|
||||
(content) => content.continuationItemRenderer
|
||||
)?.continuationItemRenderer.continuationEndpoint.continuationCommand
|
||||
.token,
|
||||
};
|
||||
|
||||
return vidData;
|
||||
|
|
|
@ -133,15 +133,11 @@ const innertubeModule = {
|
|||
},
|
||||
|
||||
async recommendContinuation(continuation, endpoint) {
|
||||
const response = await InnertubeAPI.getContinuationsAsync(
|
||||
continuation,
|
||||
endpoint
|
||||
);
|
||||
const response = await this.getContinuation(continuation, endpoint);
|
||||
const contents =
|
||||
response.data.continuationContents.sectionListContinuation.contents;
|
||||
const final = contents.map((shelves) => {
|
||||
const video = shelves.shelfRenderer?.content?.horizontalListRenderer;
|
||||
|
||||
if (video) return video;
|
||||
});
|
||||
const continuations =
|
||||
|
@ -149,6 +145,26 @@ const innertubeModule = {
|
|||
return { continuations: continuations, contents: final };
|
||||
},
|
||||
|
||||
async getContinuation(continuation, endpoint, mode = "android") {
|
||||
let contextAdditional = {};
|
||||
if (mode.toLowerCase() == "web") {
|
||||
contextAdditional = {
|
||||
...contextAdditional,
|
||||
...{
|
||||
client: {
|
||||
clientName: constants.YT_API_VALUES.CLIENT_WEB,
|
||||
clientVersion: constants.YT_API_VALUES.VERSION_WEB,
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return await InnertubeAPI.getContinuationsAsync(
|
||||
continuation,
|
||||
endpoint,
|
||||
contextAdditional
|
||||
);
|
||||
},
|
||||
|
||||
async search(query) {
|
||||
try {
|
||||
const response = await InnertubeAPI.getSearchAsync(query);
|
||||
|
|
Loading…
Reference in a new issue