mirror of
https://github.com/VueTubeApp/VueTube
synced 2024-11-29 06:33:05 +00:00
Merge branch 'Frontesque:main' into main
This commit is contained in:
commit
9cb5dad3c0
40 changed files with 1461 additions and 997 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
@ -4,6 +4,7 @@ on:
|
||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
|
- dev
|
||||||
|
|
||||||
env:
|
env:
|
||||||
NODE_VERSION: 16
|
NODE_VERSION: 16
|
||||||
|
|
8
.github/workflows/nightly-release.yml
vendored
8
.github/workflows/nightly-release.yml
vendored
|
@ -1,12 +1,10 @@
|
||||||
# This is a basic workflow to help you get started with Actions
|
|
||||||
|
|
||||||
name: nightly-release
|
name: nightly-release
|
||||||
|
|
||||||
# Controls when the workflow will run
|
# Controls when the workflow will run
|
||||||
on:
|
on:
|
||||||
# Triggers the workflow on push or pull request events but only for the main branch
|
# Triggers the workflow on on a schedule
|
||||||
schedule:
|
schedule:
|
||||||
# Runs "at minute 55 past every hour" (see https://crontab.guru)
|
# Runs "at 0:00 UTC every day" (see https://crontab.guru)
|
||||||
- cron: '0 0 * * *'
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
# Allows you to run this workflow manually from the Actions tab
|
# Allows you to run this workflow manually from the Actions tab
|
||||||
|
@ -26,6 +24,8 @@ jobs:
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- name: Checkout
|
||||||
uses: actions/checkout@v2
|
uses: actions/checkout@v2
|
||||||
|
with:
|
||||||
|
ref: main
|
||||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||||
uses: actions/setup-node@v2
|
uses: actions/setup-node@v2
|
||||||
with:
|
with:
|
||||||
|
|
41
NUXT/components/Comments/authorCommentBadgeRenderer.vue
Normal file
41
NUXT/components/Comments/authorCommentBadgeRenderer.vue
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="author-comment-badge-renderer"
|
||||||
|
v-if="metadata && iconTypeMap.hasOwnProperty(metadata.icon.iconType)"
|
||||||
|
>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<v-icon v-bind="attrs" v-on="on" class="author-badge" small>{{
|
||||||
|
iconTypeMap[metadata.icon.iconType]
|
||||||
|
}}</v-icon>
|
||||||
|
</template>
|
||||||
|
<span>{{ metadata.iconTooltip }}</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
metadata: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
iconTypeMap: {
|
||||||
|
CHECK: "mdi-check-circle",
|
||||||
|
CHECK_CIRCLE_THICK: "mdi-check-circle",
|
||||||
|
OFFICIAL_ARTIST_BADGE: "mdi-music-note",
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.author-comment-badge-renderer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -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,201 @@
|
||||||
|
<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 subtitle-2">
|
||||||
|
<div
|
||||||
|
class="author-badge-name mr-1"
|
||||||
|
:class="{ owner: commentRenderer.authorIsChannelOwner }"
|
||||||
|
>
|
||||||
|
<div class="author-name--wrapper">
|
||||||
|
<span class="font-weight-bold author-name" v-emoji>
|
||||||
|
{{ commentRenderer.authorText.simpleText }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<template
|
||||||
|
v-for="(badge, index) in commentRenderer.authorCommentBadge"
|
||||||
|
>
|
||||||
|
<author-comment-badge-renderer
|
||||||
|
:metadata="badge"
|
||||||
|
:key="index"
|
||||||
|
class="ml-1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-for="(badge, index) in commentRenderer.sponsorCommentBadge"
|
||||||
|
>
|
||||||
|
<sponsor-comment-badge-renderer
|
||||||
|
:metadata="badge"
|
||||||
|
:key="index"
|
||||||
|
class="ml-1"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||||
|
class="background--text comment-timestamp"
|
||||||
|
>
|
||||||
|
{{ commentRenderer.publishedTimeText.runs[0].text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<collapsable-text
|
||||||
|
:lines="4"
|
||||||
|
:expandText="
|
||||||
|
commentRenderer.expandButton.buttonRenderer.text.runs[0].text
|
||||||
|
"
|
||||||
|
:collapseText="
|
||||||
|
commentRenderer.collapseButton.buttonRenderer.text.runs[0].text
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<yt-text-formatter :textRuns="commentRenderer.contentText.runs">
|
||||||
|
</yt-text-formatter>
|
||||||
|
</collapsable-text>
|
||||||
|
<div class="toolbar">
|
||||||
|
<v-btn-toggle v-model="voteStatus" group>
|
||||||
|
<div class="toolbar--item mr-1">
|
||||||
|
<v-btn class="toolbar--button like" disabled icon x-small plain>
|
||||||
|
<v-icon small>mdi-thumb-up</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
<span
|
||||||
|
v-if="commentRenderer.voteCount"
|
||||||
|
v-text="commentRenderer.voteCount.simpleText"
|
||||||
|
class="like-count subtitle-2"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar--item">
|
||||||
|
<v-btn class="toolbar--button dislike" disabled icon x-small plain>
|
||||||
|
<v-icon small>mdi-thumb-down</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
</v-btn-toggle>
|
||||||
|
<div class="toolbar--item">
|
||||||
|
<v-btn class="toolbar--button reply ml-2" disabled icon x-small plain>
|
||||||
|
<v-icon small>mdi-comment</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
<div class="toolbar--item" v-if="commentRenderer.replyCount">
|
||||||
|
<span
|
||||||
|
v-text="commentRenderer.replyCount"
|
||||||
|
class="like-count mr-1 subtitle-2"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
.entry {
|
||||||
|
width: 100%; /* Prevent Loading Weirdness */
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-thread {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-basis: auto;
|
||||||
|
width: 100%;
|
||||||
|
padding: 10px 0;
|
||||||
|
|
||||||
|
.avatar-thumbnail {
|
||||||
|
margin-right: 0.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-content {
|
||||||
|
min-width: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: auto;
|
||||||
|
flex-grow: 1;
|
||||||
|
|
||||||
|
.comment-content--header {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
|
||||||
|
.comment-timestamp {
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.author-badge-name {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
min-width: 0;
|
||||||
|
|
||||||
|
.author-name--wrapper {
|
||||||
|
min-width: 0;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.owner {
|
||||||
|
padding: 0 0.6em;
|
||||||
|
background-color: #888888;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 1em;
|
||||||
|
|
||||||
|
&::v-deep .author-badge {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.toolbar--button::v-deep.v-btn--active .v-btn__content {
|
||||||
|
color: var(--v-primary-base);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import collapsableText from "~/components/UtilRenderers/collapsableText.vue";
|
||||||
|
import YtTextFormatter from "~/components/UtilRenderers/YtTextFormatter.vue";
|
||||||
|
import AuthorCommentBadgeRenderer from "~/components/Comments/authorCommentBadgeRenderer.vue";
|
||||||
|
import SponsorCommentBadgeRenderer from "~/components/Comments/sponsorCommentBadgeRenderer.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
collapsableText,
|
||||||
|
YtTextFormatter,
|
||||||
|
AuthorCommentBadgeRenderer,
|
||||||
|
SponsorCommentBadgeRenderer,
|
||||||
|
},
|
||||||
|
props: ["comment"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
commentRenderer: null,
|
||||||
|
voteStatus: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.commentRenderer = this.comment?.comment?.commentRenderer;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
61
NUXT/components/Comments/commentsHeaderRenderer.vue
Normal file
61
NUXT/components/Comments/commentsHeaderRenderer.vue
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<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>
|
||||||
|
<div
|
||||||
|
class="placeholder-text background--text"
|
||||||
|
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||||
|
v-text="
|
||||||
|
boxRenderer.placeholderText.runs[
|
||||||
|
boxRenderer.placeholderText.runs.length - 1
|
||||||
|
].text
|
||||||
|
"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.entry {
|
||||||
|
width: 100%; /* Prevent Loading Weirdness */
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatar-thumbnail {
|
||||||
|
margin-right: 1rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
width: 48px;
|
||||||
|
height: 48px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.comment-header {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-basis: auto;
|
||||||
|
align-items: center;
|
||||||
|
padding: 10px 0;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["comment"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
boxRenderer: null,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
129
NUXT/components/Comments/mainCommentRenderer.vue
Normal file
129
NUXT/components/Comments/mainCommentRenderer.vue
Normal file
|
@ -0,0 +1,129 @@
|
||||||
|
<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 @click="$emit('changeState', false)">
|
||||||
|
<v-icon>mdi-close</v-icon>
|
||||||
|
</v-btn>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-for="(comment, index) in comments">
|
||||||
|
<v-list-item :key="index">
|
||||||
|
<component
|
||||||
|
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||||
|
:is="Object.keys(comment)[0]"
|
||||||
|
:comment="comment[Object.keys(comment)[0]]"
|
||||||
|
@intersect="paginate"
|
||||||
|
></component>
|
||||||
|
</v-list-item>
|
||||||
|
<v-divider
|
||||||
|
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||||
|
:key="index"
|
||||||
|
></v-divider>
|
||||||
|
</template>
|
||||||
|
<div class="loading" v-if="loading">
|
||||||
|
<v-sheet
|
||||||
|
color="background"
|
||||||
|
v-for="i in comments.length <= 0 ? 5 : 1"
|
||||||
|
:key="i"
|
||||||
|
>
|
||||||
|
<v-skeleton-loader type="list-item-avatar-three-line" />
|
||||||
|
</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";
|
||||||
|
import continuationItemRenderer from "~/components/observer.vue";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ["defaultContinuation", "commentData", "showComments"],
|
||||||
|
|
||||||
|
model: {
|
||||||
|
prop: "showComments",
|
||||||
|
event: "changeState",
|
||||||
|
},
|
||||||
|
|
||||||
|
components: {
|
||||||
|
dialogBase,
|
||||||
|
commentsHeaderRenderer,
|
||||||
|
commentThreadRenderer,
|
||||||
|
continuationItemRenderer,
|
||||||
|
},
|
||||||
|
|
||||||
|
data: () => ({
|
||||||
|
loading: true,
|
||||||
|
comments: [],
|
||||||
|
continuation: null,
|
||||||
|
}),
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
if (!this.continuation) this.continuation = this.defaultContinuation;
|
||||||
|
this.paginate();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
getComponents() {
|
||||||
|
return this.$options.components;
|
||||||
|
},
|
||||||
|
|
||||||
|
paginate() {
|
||||||
|
if (this.continuation) {
|
||||||
|
this.loading = true;
|
||||||
|
const watcherIndex = this.comments.findIndex(
|
||||||
|
(comment) => comment.continuationItemRenderer
|
||||||
|
);
|
||||||
|
if (watcherIndex) this.comments.splice(watcherIndex, 1);
|
||||||
|
this.$youtube
|
||||||
|
.getContinuation(this.continuation, "next", "web")
|
||||||
|
.then((result) => {
|
||||||
|
let processed;
|
||||||
|
if (
|
||||||
|
result.data.onResponseReceivedEndpoints.find(
|
||||||
|
(endpoints) => endpoints.reloadContinuationItemsCommand
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
processed = result.data.onResponseReceivedEndpoints.map(
|
||||||
|
(endpoints) =>
|
||||||
|
endpoints.reloadContinuationItemsCommand.continuationItems
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
processed = result.data.onResponseReceivedEndpoints.map(
|
||||||
|
(endpoints) =>
|
||||||
|
endpoints.appendContinuationItemsAction.continuationItems
|
||||||
|
);
|
||||||
|
}
|
||||||
|
processed = processed.flat(1);
|
||||||
|
this.comments = this.comments.concat(processed);
|
||||||
|
this.continuation = this.findContinuation(processed);
|
||||||
|
console.log("comments", this.comments);
|
||||||
|
if (this.comments) this.loading = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
findContinuation(newResponses) {
|
||||||
|
const continuationItemParent = newResponses.find(
|
||||||
|
(item) => item.continuationItemRenderer
|
||||||
|
);
|
||||||
|
|
||||||
|
const newContinuation =
|
||||||
|
continuationItemParent?.continuationItemRenderer.continuationEndpoint
|
||||||
|
.continuationCommand.token;
|
||||||
|
|
||||||
|
return newContinuation;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
43
NUXT/components/Comments/sponsorCommentBadgeRenderer.vue
Normal file
43
NUXT/components/Comments/sponsorCommentBadgeRenderer.vue
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
class="author-comment-badge-renderer"
|
||||||
|
v-if="metadata && metadata.customBadge"
|
||||||
|
>
|
||||||
|
<v-tooltip top>
|
||||||
|
<template v-slot:activator="{ on, attrs }">
|
||||||
|
<img
|
||||||
|
v-bind="attrs"
|
||||||
|
v-on="on"
|
||||||
|
class="badge"
|
||||||
|
:src="metadata.customBadge.thumbnails[0].url"
|
||||||
|
:alt="metadata.tooltip"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<span>{{ metadata.tooltip }}</span>
|
||||||
|
</v-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
metadata: {
|
||||||
|
type: Object,
|
||||||
|
default: () => ({}),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.author-comment-badge-renderer {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
.badge {
|
||||||
|
display: inline-block;
|
||||||
|
width: 1.25em;
|
||||||
|
height: 1.25em;
|
||||||
|
vertical-align: -0.1em;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,136 +1,39 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card
|
<base-video-renderer
|
||||||
class="entry gridVideoRenderer background"
|
:vidId="video.videoId"
|
||||||
:to="`/watch?v=${video.videoId}`"
|
:thumbnails="video.thumbnail.thumbnails"
|
||||||
flat
|
:thumbnailOverlayStyle="thumbnailOverlayStyle"
|
||||||
|
:thumbnailOverlayText="thumbnailOverlayText"
|
||||||
|
:channelUrl="
|
||||||
|
this.$rendererUtils.getNavigationEndpoints(video.shortBylineText.runs[0])
|
||||||
|
"
|
||||||
|
:channelIcon="video.channelThumbnail.thumbnails[0].url"
|
||||||
|
:titles="video.title.runs"
|
||||||
|
:bottomText="parseBottom(video)"
|
||||||
>
|
>
|
||||||
<div style="position: relative" class="thumbnail-container">
|
</base-video-renderer>
|
||||||
<v-img
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
:src="
|
|
||||||
$youtube.getThumbnail(
|
|
||||||
video.videoId,
|
|
||||||
'max',
|
|
||||||
video.thumbnail.thumbnails
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
class="videoRuntimeFloat"
|
|
||||||
:class="
|
|
||||||
'style-' +
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style
|
|
||||||
"
|
|
||||||
style="color: #fff"
|
|
||||||
v-text="
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text
|
|
||||||
.runs[0].text
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="details">
|
|
||||||
<a
|
|
||||||
:href="
|
|
||||||
this.$rendererUtils.getNavigationEndpoints(
|
|
||||||
video.shortBylineText.runs[0]
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="avatar-link pt-2"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
class="avatar-thumbnail"
|
|
||||||
:src="video.channelThumbnail.thumbnails[0].url"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<v-card-text class="video-info pt-2" v-emoji>
|
|
||||||
<div
|
|
||||||
v-for="title in video.title.runs"
|
|
||||||
:key="title.text"
|
|
||||||
style="margin-top: 0.5em"
|
|
||||||
class="font-weight-medium vid-title"
|
|
||||||
>
|
|
||||||
{{ title.text }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="background--text caption"
|
|
||||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
|
||||||
v-text="parseBottom(video)"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.entry {
|
|
||||||
width: 100%; /* Prevent Loading Weirdness */
|
|
||||||
}
|
|
||||||
.videoRuntimeFloat {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0px 4px 0px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-DEFAULT {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-LIVE {
|
|
||||||
background: rgba(255, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 35px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (orientation: landscape) {
|
|
||||||
.entry {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.thumbnail-container {
|
|
||||||
width: 50vh;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
#details {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
.avatar-thumbnail {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
.video-info {
|
|
||||||
padding-top: 0 !important;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import baseVideoRenderer from "~/components/UtilRenderers/baseVideoRenderer.vue";
|
||||||
export default {
|
export default {
|
||||||
props: ["video"],
|
props: ["video"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
baseVideoRenderer,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
thumbnailOverlayText() {
|
||||||
|
return this.video.thumbnailOverlays[0]?.thumbnailOverlayTimeStatusRenderer
|
||||||
|
?.text.runs[0].text;
|
||||||
|
},
|
||||||
|
thumbnailOverlayStyle() {
|
||||||
|
return this.video.thumbnailOverlays[0]?.thumbnailOverlayTimeStatusRenderer
|
||||||
|
?.style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
parseBottom(video) {
|
parseBottom(video) {
|
||||||
const bottomText = [
|
const bottomText = [
|
||||||
|
|
|
@ -1,3 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="swipeableBottomSheet" :open-state="state ? 1 : 0"></div>
|
|
||||||
</template>
|
|
|
@ -0,0 +1,2 @@
|
||||||
|
@import "~vuetify/src/styles/settings/_variables";
|
||||||
|
$bottom-sheet-inset-width: 70% !default;
|
|
@ -0,0 +1,4 @@
|
||||||
|
import swipeableBottomSheet from "./swipeableBottomSheet";
|
||||||
|
|
||||||
|
export { swipeableBottomSheet };
|
||||||
|
export default swipeableBottomSheet;
|
|
@ -0,0 +1,34 @@
|
||||||
|
import "./swipeableBottomSheet.sass";
|
||||||
|
|
||||||
|
// Extensions
|
||||||
|
import { VDialog } from "vuetify/lib";
|
||||||
|
|
||||||
|
/* @vue/component */
|
||||||
|
export default VDialog.extend({
|
||||||
|
name: "swipeable-bottom-sheet",
|
||||||
|
|
||||||
|
props: {
|
||||||
|
inset: Boolean,
|
||||||
|
maxWidth: [String, Number],
|
||||||
|
transition: {
|
||||||
|
type: String,
|
||||||
|
default: "bottom-sheet-transition",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
classes() {
|
||||||
|
return {
|
||||||
|
...VDialog.options.computed.classes.call(this),
|
||||||
|
"swipeable-bottom-sheet": true,
|
||||||
|
"swipeable-bottom-sheet--inset": this.inset,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
contentClasses() {
|
||||||
|
return {
|
||||||
|
"swipeable-bottom-sheet__content": true,
|
||||||
|
"swipeable-bottom-sheet__content--active": this.isActive,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
|
@ -0,0 +1,44 @@
|
||||||
|
@import './_variables.scss'
|
||||||
|
|
||||||
|
// Transition
|
||||||
|
.bottom-sheet-transition
|
||||||
|
&-enter
|
||||||
|
transform: translateY(100%)
|
||||||
|
|
||||||
|
&-leave-to
|
||||||
|
transform: translateY(100%)
|
||||||
|
|
||||||
|
// Block
|
||||||
|
|
||||||
|
.swipeable-bottom-sheet
|
||||||
|
|
||||||
|
&.v-dialog
|
||||||
|
align-self: flex-end
|
||||||
|
border-radius: 0
|
||||||
|
flex: 0 1 auto
|
||||||
|
margin: 0
|
||||||
|
overflow: visible
|
||||||
|
|
||||||
|
&.swipeable-bottom-sheet--inset
|
||||||
|
max-width: $bottom-sheet-inset-width
|
||||||
|
|
||||||
|
@media #{map-get($display-breakpoints, 'xs-only')}
|
||||||
|
max-width: none
|
||||||
|
|
||||||
|
&.v-dialog:not(.v-dialog--fullscreen)
|
||||||
|
max-height: 100%
|
||||||
|
height: 100%
|
||||||
|
|
||||||
|
.swipeable-bottom-sheet__content
|
||||||
|
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
|
||||||
|
|
||||||
|
|
|
@ -1,130 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="content">
|
|
||||||
<v-btn class="pausePlay" text @click="playing = !playing">
|
|
||||||
<v-icon size="5em" color="white">mdi-{{ playing ? "pause" : "play" }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<div class="seekBar">
|
|
||||||
<v-slider
|
|
||||||
dense
|
|
||||||
hide-details
|
|
||||||
min="0"
|
|
||||||
:max="videoEnd"
|
|
||||||
:value="currentTime"
|
|
||||||
@change="scrubTo()"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<video
|
|
||||||
ref="player"
|
|
||||||
autoplay
|
|
||||||
:src="vidSrc"
|
|
||||||
width="100%"
|
|
||||||
style="max-height: 50vh"
|
|
||||||
@webkitfullscreenchange="handleFullscreenChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-for="(source, index) in sources" :key="index">
|
|
||||||
{{ source.qualityLabel }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
sources: {
|
|
||||||
type: Array,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
//--- Basic Information ---//
|
|
||||||
playerVersion: 0.1,
|
|
||||||
vidSrc: null,
|
|
||||||
|
|
||||||
//--- Player State Information ---//
|
|
||||||
playing: true,
|
|
||||||
currentTime: 0,
|
|
||||||
videoEnd: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
watch: {
|
|
||||||
playing() {
|
|
||||||
this.playing ? this.$refs.player.play() : this.$refs.player.pause();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
|
||||||
const src = this.sources[this.sources.length - 1].url;
|
|
||||||
this.vidSrc = src;
|
|
||||||
|
|
||||||
setInterval(this.updateTiming, 100);
|
|
||||||
},
|
|
||||||
|
|
||||||
methods: {
|
|
||||||
handleFullscreenChange() {
|
|
||||||
if (document.fullscreenElement === this.$refs.player) {
|
|
||||||
this.$vuetube.statusBar.hide();
|
|
||||||
this.$vuetube.navigationBar.hide();
|
|
||||||
} else {
|
|
||||||
this.$vuetube.statusBar.show();
|
|
||||||
this.$vuetube.navigationBar.show();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
scrubTo() {
|
|
||||||
const player = this.$refs.player;
|
|
||||||
player.currentTime = 0;
|
|
||||||
console.log(val, this.currentTime, player.currentTime);
|
|
||||||
},
|
|
||||||
|
|
||||||
updateTiming() {
|
|
||||||
const player = this.$refs.player;
|
|
||||||
if (player == undefined) return;
|
|
||||||
this.videoEnd = player.duration;
|
|
||||||
this.currentTime = player.currentTime;
|
|
||||||
console.log(player.currentTime, this.currentTime);
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/*** Overlay Information ***/
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.content video {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.content:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** General Overlay Styling ***/
|
|
||||||
.pausePlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
}
|
|
||||||
.seekBar {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
95
NUXT/components/Player/controls.vue
Normal file
95
NUXT/components/Player/controls.vue
Normal file
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="controls" @click="toggleControls()">
|
||||||
|
<div class="controlsWrap" ref="controlsWrap">
|
||||||
|
|
||||||
|
<div class="centerVideoControls">
|
||||||
|
<v-btn @click="togglePlaying()" text style="height: 5em; width: 5em;">
|
||||||
|
<v-icon size="5em" v-text="playing ? 'mdi-pause' : 'mdi-play' " ref="pausePlayIndicator" />
|
||||||
|
</v-btn>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bottomVideoControls">
|
||||||
|
{{ watched }} <span style="color: #999;">/ {{ video.duration }}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.centerVideoControls {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
}
|
||||||
|
|
||||||
|
.bottomVideoControls {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.pausePlay {
|
||||||
|
min-height: 5em;
|
||||||
|
min-width: 5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controls {
|
||||||
|
position: absolute;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.controlsWrap {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["video"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
playing: true,
|
||||||
|
controls: true,
|
||||||
|
|
||||||
|
watched: 0,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.video.ontimeupdate = () => {
|
||||||
|
console.log(this.video.currentTime)
|
||||||
|
this.watched = this.video.currentTime;
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
togglePlaying() {
|
||||||
|
if (this.video.paused) {
|
||||||
|
this.video.play()
|
||||||
|
this.playing = true;
|
||||||
|
} else {
|
||||||
|
this.video.pause()
|
||||||
|
this.playing = false;
|
||||||
|
};
|
||||||
|
this.toggleControls(); // Prevent Screen From Closing
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleControls() {
|
||||||
|
const setControls = this.controls ? 'none' : 'block';
|
||||||
|
this.$refs.controlsWrap.style.display = setControls;
|
||||||
|
this.controls = !this.controls;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
</script>
|
|
@ -1,70 +1,40 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="position: relative;">
|
||||||
<div @click="toggleControls()" class="content" :style="showControls ? 'background: rgba(0, 0, 0, 0.5);' : '' ">
|
<video
|
||||||
|
ref="player"
|
||||||
|
autoplay
|
||||||
|
:src="vidSrc"
|
||||||
|
width="100%"
|
||||||
|
style="max-height: 50vh; display: block"
|
||||||
|
@webkitfullscreenchange="handleFullscreenChange"
|
||||||
|
/>
|
||||||
|
|
||||||
<div v-show="showControls">
|
|
||||||
<v-btn class="pausePlay" text @click="playing = !playing">
|
|
||||||
<v-icon size="5em" color="white">mdi-{{ playing ? "pause" : "play" }}</v-icon>
|
|
||||||
</v-btn>
|
|
||||||
|
|
||||||
<scrubber class="scrubber" :duration="duration" :endDuration="endDuration" />
|
<seekbar :video=$refs.player v-if="$refs.player" />
|
||||||
</div>
|
<controls v-if="$refs.player" :video="$refs.player" />
|
||||||
|
|
||||||
<video
|
<!-- <v-slider v-model="value" step="0"></v-slider> -->
|
||||||
ref="player"
|
|
||||||
:src="vidSrc"
|
|
||||||
width="100%"
|
|
||||||
style="max-height: 50vh"
|
|
||||||
@webkitfullscreenchange="handleFullscreenChange"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-for="(source, index) in sources" :key="index">
|
|
||||||
{{ source.qualityLabel }}
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import scrubber from "./scrubber.vue";
|
import seekbar from '~/components/Player/seekbar.vue';
|
||||||
|
import controls from '~/components/Player/controls.vue';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
props: ["sources"],
|
||||||
components: {
|
components: {
|
||||||
scrubber,
|
seekbar,
|
||||||
},
|
controls
|
||||||
props: {
|
|
||||||
sources: {
|
|
||||||
type: Array,
|
|
||||||
default: [],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
//--- Basic Information ---//
|
vidSrc: "",
|
||||||
playerVersion: 0.1,
|
|
||||||
vidSrc: null,
|
|
||||||
|
|
||||||
//--- Player State Information ---//
|
|
||||||
showControls: false,
|
|
||||||
playing: false,
|
|
||||||
duration: 0,
|
|
||||||
endDuration: 0,
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
watch: {
|
|
||||||
playing() {
|
|
||||||
this.playing ? this.$refs.player.play() : this.$refs.player.pause();
|
|
||||||
},
|
|
||||||
},
|
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
const src = this.sources[this.sources.length - 1].url;
|
this.vidSrc = this.sources[this.sources.length-1].url;
|
||||||
this.vidSrc = src;
|
|
||||||
|
|
||||||
setInterval(this.updateTiming, 100);
|
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
handleFullscreenChange() {
|
handleFullscreenChange() {
|
||||||
if (document.fullscreenElement === this.$refs.player) {
|
if (document.fullscreenElement === this.$refs.player) {
|
||||||
|
@ -76,54 +46,9 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
updateTiming() {
|
getPlayer() {
|
||||||
const player = this.$refs.player;
|
return this.$refs.player;
|
||||||
if (player == undefined) return;
|
|
||||||
this.duration = player.currentTime;
|
|
||||||
this.endDuration = player.duration;
|
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleControls() {
|
|
||||||
this.showControls = !this.showControls;
|
|
||||||
}
|
|
||||||
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/*** Overlay Information ***/
|
|
||||||
.content {
|
|
||||||
position: relative;
|
|
||||||
width: 500px;
|
|
||||||
margin: 0 auto;
|
|
||||||
}
|
|
||||||
.content video {
|
|
||||||
width: 100%;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
.content:before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/*** General Overlay Styling ***/
|
|
||||||
.pausePlay {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
height: 5em !important;
|
|
||||||
width: 5em !important;
|
|
||||||
}
|
|
||||||
.scrubber {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
</style>
|
|
||||||
|
|
|
@ -9,16 +9,6 @@
|
||||||
style="max-height: 50vh; display: block"
|
style="max-height: 50vh; display: block"
|
||||||
@webkitfullscreenchange="handleFullscreenChange"
|
@webkitfullscreenchange="handleFullscreenChange"
|
||||||
/>
|
/>
|
||||||
<v-progress-linear
|
|
||||||
active
|
|
||||||
background-color="primary"
|
|
||||||
background-opacity="0.5"
|
|
||||||
:buffer-value="buffered"
|
|
||||||
color="primary"
|
|
||||||
height="3"
|
|
||||||
query
|
|
||||||
:value="percentage"
|
|
||||||
/>
|
|
||||||
<!-- <v-slider v-model="value" step="0"></v-slider> -->
|
<!-- <v-slider v-model="value" step="0"></v-slider> -->
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -26,21 +16,6 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
props: ["vidSrc"],
|
props: ["vidSrc"],
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
percentage: 0,
|
|
||||||
buffered: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
let vid = this.$refs.player;
|
|
||||||
vid.ontimeupdate = () => {
|
|
||||||
this.percentage = (vid.currentTime / vid.duration) * 100;
|
|
||||||
};
|
|
||||||
vid.addEventListener("progress", () => {
|
|
||||||
this.buffered = (vid.buffered.end(0) / vid.duration) * 100;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
handleFullscreenChange() {
|
handleFullscreenChange() {
|
||||||
if (document.fullscreenElement === this.$refs.player) {
|
if (document.fullscreenElement === this.$refs.player) {
|
|
@ -1,44 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="scrubber">
|
|
||||||
<div id="progress" class="primary" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
props: {
|
|
||||||
duration: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
endDuration: {
|
|
||||||
type: Number,
|
|
||||||
default: 0,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data() {
|
|
||||||
return {
|
|
||||||
percentage: 0,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
mounted() {
|
|
||||||
const vm = this;
|
|
||||||
setInterval(function () {
|
|
||||||
vm.percentage = (vm.duration / vm.endDuration) * 100;
|
|
||||||
|
|
||||||
document.getElementById("progress").style.width = vm.percentage + "%";
|
|
||||||
}, 100);
|
|
||||||
},
|
|
||||||
};
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.scrubber {
|
|
||||||
width: 100%;
|
|
||||||
height: 5px;
|
|
||||||
background: rgba(255, 255, 255, 0.5);
|
|
||||||
}
|
|
||||||
#progress {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
</style>
|
|
42
NUXT/components/Player/seekbar.vue
Normal file
42
NUXT/components/Player/seekbar.vue
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
|
||||||
|
|
||||||
|
<v-progress-linear
|
||||||
|
active
|
||||||
|
background-color="primary"
|
||||||
|
background-opacity="0.5"
|
||||||
|
:buffer-value="buffered"
|
||||||
|
color="primary"
|
||||||
|
height="3"
|
||||||
|
query
|
||||||
|
:value="percentage"
|
||||||
|
/>
|
||||||
|
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: ["video"],
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
percentage: 0,
|
||||||
|
buffered: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.video.ontimeupdate = () => {
|
||||||
|
this.percentage = (this.video.currentTime / this.video.duration) * 100;
|
||||||
|
};
|
||||||
|
this.video.addEventListener("progress", () => {
|
||||||
|
this.buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
</script>
|
59
NUXT/components/UtilRenderers/YtTextFormatter.vue
Normal file
59
NUXT/components/UtilRenderers/YtTextFormatter.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<template>
|
||||||
|
<div class="yt-text-formatter">
|
||||||
|
<template v-for="(text, index) in textRuns">
|
||||||
|
<template v-if="$rendererUtils.checkInternal(text)">
|
||||||
|
<a
|
||||||
|
@click="openInternal($rendererUtils.getNavigationEndpoints(text))"
|
||||||
|
:key="index"
|
||||||
|
>{{ text.text }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template
|
||||||
|
v-else-if="
|
||||||
|
text.navigationEndpoint && text.navigationEndpoint.urlEndpoint
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
@click="openExternal($rendererUtils.getNavigationEndpoints(text))"
|
||||||
|
:key="index"
|
||||||
|
>{{ text.text }}</a
|
||||||
|
>
|
||||||
|
</template>
|
||||||
|
<template v-else-if="text.emoji && text.emoji.isCustomEmoji">
|
||||||
|
<img
|
||||||
|
:src="
|
||||||
|
text.emoji.image.thumbnails[text.emoji.image.thumbnails.length - 1]
|
||||||
|
.url
|
||||||
|
"
|
||||||
|
:alt="text.text"
|
||||||
|
:key="index"
|
||||||
|
class="emoji"
|
||||||
|
draggable="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<span :key="index" v-emoji>{{ text.text }}</span>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { Browser } from "@capacitor/browser";
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
textRuns: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
async openExternal(url) {
|
||||||
|
await Browser.open({ url: url });
|
||||||
|
},
|
||||||
|
async openInternal(url) {
|
||||||
|
await this.$router.push(url);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
144
NUXT/components/UtilRenderers/baseVideoRenderer.vue
Normal file
144
NUXT/components/UtilRenderers/baseVideoRenderer.vue
Normal file
|
@ -0,0 +1,144 @@
|
||||||
|
<template>
|
||||||
|
<v-card class="entry videoRenderer background" :to="`/watch?v=${vidId}`" flat>
|
||||||
|
<div style="position: relative" class="thumbnail-container">
|
||||||
|
<v-img
|
||||||
|
:aspect-ratio="16 / 9"
|
||||||
|
:src="$youtube.getThumbnail(vidId, 'max', thumbnails)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="thumbnailOverlayText && thumbnailOverlayStyle"
|
||||||
|
class="videoRuntimeFloat"
|
||||||
|
:class="'style-' + thumbnailOverlayStyle"
|
||||||
|
style="color: #fff"
|
||||||
|
v-text="thumbnailOverlayText"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div id="details">
|
||||||
|
<a :href="channelUrl" class="avatar-link pt-2">
|
||||||
|
<v-img class="avatar-thumbnail" :src="channelIcon" />
|
||||||
|
</a>
|
||||||
|
<v-card-text class="video-info pt-2" v-emoji>
|
||||||
|
<span
|
||||||
|
v-for="title in titles"
|
||||||
|
:key="title.text"
|
||||||
|
style="margin-top: 0.5em"
|
||||||
|
class="font-weight-medium vid-title"
|
||||||
|
>
|
||||||
|
{{ title.text }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<div
|
||||||
|
class="background--text text--lighten-5 caption"
|
||||||
|
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||||
|
v-text="bottomText"
|
||||||
|
/>
|
||||||
|
</v-card-text>
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.entry {
|
||||||
|
width: 100%; /* Prevent Loading Weirdness */
|
||||||
|
}
|
||||||
|
.videoRuntimeFloat {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 10px;
|
||||||
|
right: 10px;
|
||||||
|
border-radius: 5px;
|
||||||
|
padding: 0px 4px 0px 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoRuntimeFloat.style-DEFAULT {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoRuntimeFloat.style-LIVE {
|
||||||
|
background: rgba(255, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.videoRuntimeFloat.style-UPCOMING {
|
||||||
|
background: rgba(0, 0, 0, 0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.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: 35px;
|
||||||
|
height: 35px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#details {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
flex-basis: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media screen and (orientation: landscape) {
|
||||||
|
.entry {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
.thumbnail-container {
|
||||||
|
width: 50vh;
|
||||||
|
float: left;
|
||||||
|
}
|
||||||
|
#details {
|
||||||
|
flex-direction: column-reverse;
|
||||||
|
}
|
||||||
|
.avatar-thumbnail {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-left: 16px;
|
||||||
|
}
|
||||||
|
.video-info {
|
||||||
|
padding-top: 0 !important;
|
||||||
|
padding-bottom: 0;
|
||||||
|
margin-top: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
vidId: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
thumbnails: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
channelUrl: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
channelIcon: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
titles: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
bottomText: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
thumbnailOverlayText: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
thumbnailOverlayStyle: {
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
100
NUXT/components/UtilRenderers/collapsableText.vue
Normal file
100
NUXT/components/UtilRenderers/collapsableText.vue
Normal file
|
@ -0,0 +1,100 @@
|
||||||
|
<template>
|
||||||
|
<div class="collapsable-text">
|
||||||
|
<div
|
||||||
|
class="text-content"
|
||||||
|
ref="textContent"
|
||||||
|
:class="expanded ? 'expanded' : 'collapsed'"
|
||||||
|
>
|
||||||
|
<slot></slot>
|
||||||
|
</div>
|
||||||
|
<a
|
||||||
|
class="toggle-collapse background--text font-weight-bold"
|
||||||
|
@click="expanded = !expanded"
|
||||||
|
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||||
|
v-if="expandable"
|
||||||
|
v-text="expanded ? collapseText : expandText"
|
||||||
|
></a>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
props: {
|
||||||
|
lines: {
|
||||||
|
type: Number,
|
||||||
|
default: 4,
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
type: String,
|
||||||
|
default: "",
|
||||||
|
},
|
||||||
|
expandText: {
|
||||||
|
type: String,
|
||||||
|
default: "Show more",
|
||||||
|
},
|
||||||
|
collapseText: {
|
||||||
|
type: String,
|
||||||
|
default: "Show less",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
expanded: false,
|
||||||
|
expandable: false,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted() {
|
||||||
|
this.checkExpandable();
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
checkExpandable() {
|
||||||
|
if (
|
||||||
|
this.$refs.textContent &&
|
||||||
|
this.$refs.textContent.offsetHeight <
|
||||||
|
this.$refs.textContent.scrollHeight
|
||||||
|
) {
|
||||||
|
this.expandable = true;
|
||||||
|
} else {
|
||||||
|
this.expandable = false;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
cssProps() {
|
||||||
|
return {
|
||||||
|
"--lines": this.lines,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@mixin truncate($rows) {
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
display: -webkit-box;
|
||||||
|
-webkit-line-clamp: $rows;
|
||||||
|
-webkit-box-orient: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-collapse {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapsable-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-basis: auto;
|
||||||
|
.text-content {
|
||||||
|
white-space: pre-line;
|
||||||
|
&.collapsed {
|
||||||
|
@include truncate(4);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -1,26 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="description" v-if="render.descriptionBodyText">
|
<div class="description" v-if="render.descriptionBodyText">
|
||||||
<template v-for="(text, index) in render.descriptionBodyText.runs">
|
<yt-text-formatter :textRuns="render.descriptionBodyText.runs">
|
||||||
<template v-if="$rendererUtils.checkInternal(text)">
|
</yt-text-formatter>
|
||||||
<a
|
|
||||||
@click="openInternal($rendererUtils.getNavigationEndpoints(text))"
|
|
||||||
:key="index"
|
|
||||||
>{{ text.text }}</a
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template
|
|
||||||
v-else-if="
|
|
||||||
text.navigationEndpoint && text.navigationEndpoint.urlEndpoint
|
|
||||||
"
|
|
||||||
>
|
|
||||||
<a
|
|
||||||
@click="openExternal($rendererUtils.getNavigationEndpoints(text))"
|
|
||||||
:key="index"
|
|
||||||
>{{ text.text }}</a
|
|
||||||
>
|
|
||||||
</template>
|
|
||||||
<template v-else> {{ text.text }} </template>
|
|
||||||
</template>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -31,17 +12,13 @@
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { Browser } from "@capacitor/browser";
|
import YtTextFormatter from "~/components/UtilRenderers/YtTextFormatter.vue";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
props: ["render"],
|
props: ["render"],
|
||||||
|
|
||||||
methods: {
|
components: {
|
||||||
async openExternal(url) {
|
YtTextFormatter,
|
||||||
await Browser.open({ url: url });
|
|
||||||
},
|
|
||||||
async openInternal(url) {
|
|
||||||
await this.$router.push(url);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
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>
|
|
@ -1,138 +1,41 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card
|
<base-video-renderer
|
||||||
class="entry gridVideoRenderer background"
|
:vidId="video.videoId"
|
||||||
:to="`/watch?v=${video.videoId}`"
|
:thumbnails="video.thumbnail.thumbnails"
|
||||||
flat
|
:thumbnailOverlayStyle="thumbnailOverlayStyle"
|
||||||
|
:thumbnailOverlayText="thumbnailOverlayText"
|
||||||
|
:channelUrl="
|
||||||
|
$rendererUtils.getNavigationEndpoints(
|
||||||
|
video.shortBylineText.runs[0].navigationEndpoint
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:channelIcon="video.channelThumbnail.thumbnails[0].url"
|
||||||
|
:titles="video.title.runs"
|
||||||
|
:bottomText="parseBottom(video)"
|
||||||
>
|
>
|
||||||
<div style="position: relative" class="thumbnail-container">
|
</base-video-renderer>
|
||||||
<v-img
|
|
||||||
v-if="video.thumbnail"
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
:src="
|
|
||||||
$youtube.getThumbnail(
|
|
||||||
video.videoId,
|
|
||||||
'max',
|
|
||||||
video.thumbnail.thumbnails
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="video.thumbnailOverlays"
|
|
||||||
class="videoRuntimeFloat"
|
|
||||||
:class="
|
|
||||||
'style-' +
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style
|
|
||||||
"
|
|
||||||
style="color: #fff"
|
|
||||||
v-text="
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text
|
|
||||||
.runs[0].text
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="details">
|
|
||||||
<a
|
|
||||||
:href="
|
|
||||||
$rendererUtils.getNavigationEndpoints(
|
|
||||||
video.shortBylineText.runs[0].navigationEndpoint
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="avatar-link pt-2"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
class="avatar-thumbnail"
|
|
||||||
:src="video.channelThumbnail.thumbnails[0].url"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<v-card-text class="video-info pt-2" v-emoji>
|
|
||||||
<div
|
|
||||||
v-for="title in video.title.runs"
|
|
||||||
:key="title.text"
|
|
||||||
style="margin-top: 0.5em"
|
|
||||||
class="font-weight-medium vid-title"
|
|
||||||
>
|
|
||||||
{{ title.text }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="background--text text--lighten-5 caption"
|
|
||||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
|
||||||
v-text="parseBottom(video)"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.entry {
|
|
||||||
width: 100%; /* Prevent Loading Weirdness */
|
|
||||||
}
|
|
||||||
.videoRuntimeFloat {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0px 4px 0px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-DEFAULT {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-LIVE {
|
|
||||||
background: rgba(255, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 35px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (orientation: landscape) {
|
|
||||||
.entry {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.thumbnail-container {
|
|
||||||
width: 50vh;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
#details {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
.avatar-thumbnail {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
.video-info {
|
|
||||||
padding-top: 0 !important;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import baseVideoRenderer from "~/components/UtilRenderers/baseVideoRenderer.vue";
|
||||||
export default {
|
export default {
|
||||||
props: ["video"],
|
props: ["video"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
baseVideoRenderer,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
thumbnailOverlayText() {
|
||||||
|
return this.video.thumbnailOverlays[0]?.thumbnailOverlayTimeStatusRenderer
|
||||||
|
?.text.runs[0].text;
|
||||||
|
},
|
||||||
|
thumbnailOverlayStyle() {
|
||||||
|
return this.video.thumbnailOverlays[0]?.thumbnailOverlayTimeStatusRenderer
|
||||||
|
?.style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
parseBottom(video) {
|
parseBottom(video) {
|
||||||
const bottomText = [
|
const bottomText = [
|
||||||
|
|
|
@ -1,141 +1,44 @@
|
||||||
<template>
|
<template>
|
||||||
<v-card
|
<base-video-renderer
|
||||||
class="entry videoWithContextRenderer background"
|
:vidId="video.videoId"
|
||||||
:to="`/watch?v=${video.videoId}`"
|
:thumbnails="video.thumbnail.thumbnails"
|
||||||
flat
|
:thumbnailOverlayStyle="thumbnailOverlayStyle"
|
||||||
|
:thumbnailOverlayText="thumbnailOverlayText"
|
||||||
|
:channelUrl="
|
||||||
|
$rendererUtils.getNavigationEndpoints(
|
||||||
|
video.shortBylineText.runs[0].navigationEndpoint
|
||||||
|
)
|
||||||
|
"
|
||||||
|
:channelIcon="
|
||||||
|
video.channelThumbnail.channelThumbnailWithLinkRenderer.thumbnail
|
||||||
|
.thumbnails[0].url
|
||||||
|
"
|
||||||
|
:titles="video.headline.runs"
|
||||||
|
:bottomText="parseBottom(video)"
|
||||||
>
|
>
|
||||||
<div style="position: relative" class="thumbnail-container">
|
</base-video-renderer>
|
||||||
<v-img
|
|
||||||
v-if="video.thumbnail"
|
|
||||||
:aspect-ratio="16 / 9"
|
|
||||||
:src="
|
|
||||||
$youtube.getThumbnail(
|
|
||||||
video.videoId,
|
|
||||||
'max',
|
|
||||||
video.thumbnail.thumbnails
|
|
||||||
)
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
<div
|
|
||||||
v-if="video.thumbnailOverlays"
|
|
||||||
class="videoRuntimeFloat"
|
|
||||||
:class="
|
|
||||||
'style-' +
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style
|
|
||||||
"
|
|
||||||
style="color: #fff"
|
|
||||||
v-text="
|
|
||||||
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text
|
|
||||||
.runs[0].text
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div id="details">
|
|
||||||
<a
|
|
||||||
:href="
|
|
||||||
$rendererUtils.getNavigationEndpoints(
|
|
||||||
video.shortBylineText.runs[0].navigationEndpoint
|
|
||||||
)
|
|
||||||
"
|
|
||||||
class="avatar-link pt-2"
|
|
||||||
>
|
|
||||||
<v-img
|
|
||||||
class="avatar-thumbnail"
|
|
||||||
:src="
|
|
||||||
video.channelThumbnail.channelThumbnailWithLinkRenderer.thumbnail
|
|
||||||
.thumbnails[0].url
|
|
||||||
"
|
|
||||||
/>
|
|
||||||
</a>
|
|
||||||
<v-card-text class="video-info pt-2" v-emoji>
|
|
||||||
<div
|
|
||||||
v-for="title in video.headline.runs"
|
|
||||||
:key="title.text"
|
|
||||||
style="margin-top: 0.5em"
|
|
||||||
class="font-weight-medium vid-title"
|
|
||||||
>
|
|
||||||
{{ title.text }}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div
|
|
||||||
class="background--text text--lighten-5 caption"
|
|
||||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
|
||||||
v-text="parseBottom(video)"
|
|
||||||
/>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
</v-card>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.entry {
|
|
||||||
width: 100%; /* Prevent Loading Weirdness */
|
|
||||||
}
|
|
||||||
.videoRuntimeFloat {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 10px;
|
|
||||||
right: 10px;
|
|
||||||
border-radius: 5px;
|
|
||||||
padding: 0px 4px 0px 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-DEFAULT {
|
|
||||||
background: rgba(0, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.videoRuntimeFloat.style-LIVE {
|
|
||||||
background: rgba(255, 0, 0, 0.5);
|
|
||||||
}
|
|
||||||
|
|
||||||
.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: 35px;
|
|
||||||
height: 35px;
|
|
||||||
}
|
|
||||||
|
|
||||||
#details {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
flex-basis: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media screen and (orientation: landscape) {
|
|
||||||
.entry {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.thumbnail-container {
|
|
||||||
width: 50vh;
|
|
||||||
float: left;
|
|
||||||
}
|
|
||||||
#details {
|
|
||||||
flex-direction: column-reverse;
|
|
||||||
}
|
|
||||||
.avatar-thumbnail {
|
|
||||||
margin-top: 0;
|
|
||||||
margin-left: 16px;
|
|
||||||
}
|
|
||||||
.video-info {
|
|
||||||
padding-top: 0 !important;
|
|
||||||
padding-bottom: 0;
|
|
||||||
margin-top: 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import baseVideoRenderer from "~/components/UtilRenderers/baseVideoRenderer.vue";
|
||||||
export default {
|
export default {
|
||||||
props: ["video"],
|
props: ["video"],
|
||||||
|
|
||||||
|
components: {
|
||||||
|
baseVideoRenderer,
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
thumbnailOverlayText() {
|
||||||
|
return this.video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer
|
||||||
|
.text.runs[0].text;
|
||||||
|
},
|
||||||
|
thumbnailOverlayStyle() {
|
||||||
|
return this.video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer
|
||||||
|
.style;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
parseBottom(video) {
|
parseBottom(video) {
|
||||||
const bottomText = [
|
const bottomText = [
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="observer" />
|
<div class="observer" style="height: 1px" />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -9,7 +9,7 @@ export default {
|
||||||
observer: null,
|
observer: null,
|
||||||
}),
|
}),
|
||||||
mounted() {
|
mounted() {
|
||||||
const options = this.options || {};
|
const options = this.options || { threshold: 0.0 };
|
||||||
this.observer = new IntersectionObserver(([entry]) => {
|
this.observer = new IntersectionObserver(([entry]) => {
|
||||||
if (entry && entry.isIntersecting) {
|
if (entry && entry.isIntersecting) {
|
||||||
this.$emit("intersect");
|
this.$emit("intersect");
|
||||||
|
|
|
@ -32,14 +32,14 @@
|
||||||
v-for="(item, index) in response"
|
v-for="(item, index) in response"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="px-0"
|
class="px-0"
|
||||||
v-emoji
|
|
||||||
>
|
>
|
||||||
<v-btn
|
<v-btn
|
||||||
text
|
text
|
||||||
tile
|
tile
|
||||||
dense
|
dense
|
||||||
class="searchButton text-left"
|
class="searchButton text-left text-none"
|
||||||
@click="youtubeSearch(item)"
|
@click="youtubeSearch(item)"
|
||||||
|
v-emoji
|
||||||
>
|
>
|
||||||
<v-icon class="mr-5">mdi-magnify</v-icon>
|
<v-icon class="mr-5">mdi-magnify</v-icon>
|
||||||
{{ item[0] || item.text }}
|
{{ item[0] || item.text }}
|
||||||
|
@ -93,18 +93,21 @@ export default {
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
//--- Back Button Listener ---//
|
//--- Back Button Listener ---//
|
||||||
CapacitorApp.addListener("backButton", ({ canGoBack }) => {
|
this.backHandler = CapacitorApp.addListener(
|
||||||
//--- Back Closes Search ---//
|
"backButton",
|
||||||
if (this.search) {
|
({ canGoBack }) => {
|
||||||
this.search = false;
|
//--- Back Closes Search ---//
|
||||||
|
if (this.search) {
|
||||||
|
this.search = false;
|
||||||
|
|
||||||
//--- Back Goes Back ---//
|
//--- Back Goes Back ---//
|
||||||
} else if (!canGoBack) {
|
} else if (!canGoBack) {
|
||||||
CapacitorApp.exitApp();
|
CapacitorApp.exitApp();
|
||||||
} else {
|
} else {
|
||||||
window.history.back();
|
window.history.back();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
);
|
||||||
|
|
||||||
// --- External URL Handling --- //
|
// --- External URL Handling --- //
|
||||||
CapacitorApp.addListener("appUrlOpen", (event) => {
|
CapacitorApp.addListener("appUrlOpen", (event) => {
|
||||||
|
@ -121,6 +124,10 @@ export default {
|
||||||
document.head.appendChild(plugin);
|
document.head.appendChild(plugin);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
beforeDestroy() {
|
||||||
|
if (this.backHandler) this.backHandler.remove();
|
||||||
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
textChanged(text) {
|
textChanged(text) {
|
||||||
if (text.length <= 0) {
|
if (text.length <= 0) {
|
||||||
|
@ -226,13 +233,13 @@ div {
|
||||||
width: 1em;
|
width: 1em;
|
||||||
height: 1em;
|
height: 1em;
|
||||||
vertical-align: -0.1em;
|
vertical-align: -0.1em;
|
||||||
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.searchButton {
|
.searchButton {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
text-transform: none !important;
|
|
||||||
justify-content: left !important;
|
justify-content: left !important;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
7
NUXT/pages/channel/_notice_.vue
Normal file
7
NUXT/pages/channel/_notice_.vue
Normal file
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<center>
|
||||||
|
<img src="/channel_notice.svg" style="width: 80%; margin: 2em;" />
|
||||||
|
<h1>Channel Pages are still being worked on</h1><br><br>
|
||||||
|
<v-btn to="/home">Return Home</v-btn>
|
||||||
|
</center>
|
||||||
|
</template>
|
|
@ -1,19 +0,0 @@
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
Redirecting...
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script>
|
|
||||||
export default {
|
|
||||||
layout: "empty",
|
|
||||||
mounted() {
|
|
||||||
|
|
||||||
this.$router.push(
|
|
||||||
"/watch?v="
|
|
||||||
+ $nuxt.$route.params.redirect_
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
|
@ -11,8 +11,8 @@
|
||||||
<div v-for="(section, index) in recommends" :key="index">
|
<div v-for="(section, index) in recommends" :key="index">
|
||||||
<horizontal-list-renderer :render="section.contents[0]" />
|
<horizontal-list-renderer :render="section.contents[0]" />
|
||||||
</div>
|
</div>
|
||||||
<vid-load-renderer v-if="!loading" :count="1" />
|
<vid-load-renderer v-if="loading" :count="1" />
|
||||||
<observer @intersect="paginate" />
|
<observer @intersect="paginate" v-else-if="recommends.length > 0" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -40,8 +40,12 @@ export default {
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
paginate() {
|
paginate() {
|
||||||
if (this.recommends) {
|
this.loading = true;
|
||||||
this.loading = true;
|
const continuationCode = this.recommends[
|
||||||
|
this.recommends.length - 1
|
||||||
|
].continuations.find((element) => element.nextContinuationData)
|
||||||
|
?.nextContinuationData.continuation;
|
||||||
|
if (continuationCode) {
|
||||||
this.$youtube
|
this.$youtube
|
||||||
.recommendContinuation(
|
.recommendContinuation(
|
||||||
this.recommends[this.recommends.length - 1].continuations.find(
|
this.recommends[this.recommends.length - 1].continuations.find(
|
||||||
|
@ -53,6 +57,8 @@ export default {
|
||||||
this.loading = false;
|
this.loading = false;
|
||||||
this.recommends.push(result);
|
this.recommends.push(result);
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
this.loading = false;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -58,7 +58,7 @@ export default {
|
||||||
await this.$youtube.getAPI();
|
await this.$youtube.getAPI();
|
||||||
this.progressMsg = "Launching";
|
this.progressMsg = "Launching";
|
||||||
|
|
||||||
this.$router.push(`/${localStorage.getItem("startPage") || "home"}`);
|
this.$router.replace(`/${localStorage.getItem("startPage") || "home"}`); // Prevent user from navigating back to the splash screen
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -1,73 +1,89 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="background">
|
<div class="background" id="watch-body">
|
||||||
<v-btn text style="position: fixed; z-index: 69420" to="home">
|
<div id="player-container">
|
||||||
<v-icon>mdi-chevron-down</v-icon>
|
<v-btn text style="position: fixed; z-index: 69420" to="home">
|
||||||
</v-btn>
|
<v-icon>mdi-chevron-down</v-icon>
|
||||||
<!-- VueTube Player V1 -->
|
</v-btn>
|
||||||
<vuetubePlayer :sources="sources" v-if="useBetaPlayer === 'true'" />
|
<!-- VueTube Player V1 -->
|
||||||
<!-- Stock Player -->
|
<vuetubePlayer
|
||||||
<videoPlayer
|
:sources="sources"
|
||||||
id="player"
|
v-if="useBetaPlayer === 'true' && sources.length > 0"
|
||||||
ref="player"
|
/>
|
||||||
v-touch="{ down: () => $router.push('/home') }"
|
|
||||||
style="position: sticky; top: 0; z-index: 42069"
|
|
||||||
class="background"
|
|
||||||
:vid-src="vidSrc"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!---->
|
<!-- Stock Player -->
|
||||||
<v-card v-if="loaded" class="background rounded-0" flat>
|
<legacyPlayer
|
||||||
<div
|
id="player"
|
||||||
v-ripple
|
ref="player"
|
||||||
class="d-flex justify-space-between align-start px-3 pt-3"
|
v-touch="{ down: () => $router.push('/home') }"
|
||||||
@click="showMore = !showMore"
|
class="background"
|
||||||
>
|
:vid-src="vidSrc"
|
||||||
<div class="d-flex flex-column">
|
v-if="useBetaPlayer !== 'true'"
|
||||||
<v-card-title
|
/>
|
||||||
class="pa-0"
|
</div>
|
||||||
style="font-size: 0.95rem; line-height: 1.15rem"
|
|
||||||
v-text="video.title"
|
<div
|
||||||
v-emoji
|
v-bind:class="{
|
||||||
/>
|
'overflow-y-auto': !showComments,
|
||||||
<v-card-text
|
'overflow-y-hidden': showComments,
|
||||||
style="font-size: 0.75rem"
|
}"
|
||||||
class="background--text pa-0"
|
id="content-container"
|
||||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
>
|
||||||
>
|
<v-card v-if="loaded" class="ml-2 mr-2 background rounded-0" flat>
|
||||||
<template
|
<div
|
||||||
v-for="text in video.metadata.contents.find(
|
v-ripple
|
||||||
(content) => content.slimVideoInformationRenderer
|
class="d-flex justify-space-between align-start px-3 pt-3"
|
||||||
).slimVideoInformationRenderer.collapsedSubtitle.runs"
|
@click="showMore = !showMore"
|
||||||
>{{ text.text }}
|
|
||||||
</template>
|
|
||||||
</v-card-text>
|
|
||||||
</div>
|
|
||||||
<v-icon class="ml-4" v-if="showMore">mdi-chevron-up</v-icon>
|
|
||||||
<v-icon class="ml-4" v-else>mdi-chevron-down</v-icon>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex">
|
|
||||||
<v-btn
|
|
||||||
v-for="(item, index) in interactions"
|
|
||||||
:key="index"
|
|
||||||
text
|
|
||||||
fab
|
|
||||||
class="vertical-button ma-1"
|
|
||||||
elevation="0"
|
|
||||||
style="width: 4.2rem !important; height: 4.2rem !important"
|
|
||||||
:disabled="item.disabled"
|
|
||||||
@click="callMethodByName(item.actionName)"
|
|
||||||
>
|
>
|
||||||
<v-icon v-text="item.icon" />
|
<div class="d-flex flex-column">
|
||||||
<div
|
<v-card-title
|
||||||
class="mt-2"
|
class="pa-0"
|
||||||
style="font-size: 0.66rem"
|
style="font-size: 0.95rem; line-height: 1.15rem"
|
||||||
v-text="item.value || item.name"
|
v-text="video.title"
|
||||||
/>
|
v-emoji
|
||||||
</v-btn>
|
/>
|
||||||
<!-- End Scrolling Div For Interactions --->
|
<v-card-text
|
||||||
<!-- <hr /> -->
|
style="font-size: 0.75rem"
|
||||||
</div>
|
class="background--text pa-0"
|
||||||
<!-- <v-bottom-sheet
|
:class="
|
||||||
|
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<div style="margin-bottom: 1rem">
|
||||||
|
<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 class="ml-4" v-if="showMore">mdi-chevron-up</v-icon>
|
||||||
|
<v-icon class="ml-4" v-else>mdi-chevron-down</v-icon>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex">
|
||||||
|
<v-btn
|
||||||
|
v-for="(item, index) in interactions"
|
||||||
|
:key="index"
|
||||||
|
text
|
||||||
|
fab
|
||||||
|
class="vertical-button ma-1"
|
||||||
|
elevation="0"
|
||||||
|
style="width: 4.2rem !important; height: 4.2rem !important"
|
||||||
|
:disabled="item.disabled"
|
||||||
|
@click="callMethodByName(item.actionName)"
|
||||||
|
>
|
||||||
|
<v-icon v-text="item.icon" />
|
||||||
|
<div
|
||||||
|
class="mt-2"
|
||||||
|
style="font-size: 0.66rem"
|
||||||
|
v-text="item.value || item.name"
|
||||||
|
/>
|
||||||
|
</v-btn>
|
||||||
|
<!-- End Scrolling Div For Interactions --->
|
||||||
|
<!-- <hr /> -->
|
||||||
|
</div>
|
||||||
|
<!-- <v-bottom-sheet
|
||||||
v-model="showMore"
|
v-model="showMore"
|
||||||
color="background"
|
color="background"
|
||||||
style="z-index: 9999999"
|
style="z-index: 9999999"
|
||||||
|
@ -84,89 +100,106 @@
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-bottom-sheet> -->
|
</v-bottom-sheet> -->
|
||||||
|
|
||||||
<!-- <v-bottom-sheet v-model="share" color="background" style="z-index: 9999999">
|
<!-- <v-bottom-sheet v-model="share" color="background" style="z-index: 9999999">
|
||||||
<v-sheet style="padding: 1em">
|
<v-sheet style="padding: 1em">
|
||||||
<div class="scroll-y">
|
<div class="scroll-y">
|
||||||
{{ response.renderedData.description }}
|
{{ response.renderedData.description }}
|
||||||
</div>
|
</div>
|
||||||
</v-sheet>
|
</v-sheet>
|
||||||
</v-bottom-sheet> -->
|
</v-bottom-sheet> -->
|
||||||
</v-card>
|
</v-card>
|
||||||
<v-divider />
|
<v-divider />
|
||||||
|
|
||||||
<!-- Channel Bar -->
|
<!-- Channel Bar -->
|
||||||
<div class="channel-container" v-if="loaded">
|
<div class="channel-container" v-if="loaded">
|
||||||
<v-card
|
<v-card
|
||||||
class="channel-section background px-3 rounded-0"
|
class="channel-section background px-3 rounded-0"
|
||||||
:to="video.channelUrl"
|
:to="video.channelUrl"
|
||||||
>
|
|
||||||
<div id="details">
|
|
||||||
<div class="avatar-link mr-3">
|
|
||||||
<v-img class="avatar-thumbnail" :src="video.channelImg" />
|
|
||||||
</div>
|
|
||||||
<div class="channel-byline" v-emoji>
|
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
class="channel-buttons"
|
|
||||||
style="color: rgb(204, 0, 0); text-transform: uppercase"
|
|
||||||
>
|
>
|
||||||
subscribe
|
<div id="details">
|
||||||
</div>
|
<div class="avatar-link mr-3">
|
||||||
</v-card>
|
<v-img class="avatar-thumbnail" :src="video.channelImg" />
|
||||||
<v-divider />
|
</div>
|
||||||
</div>
|
<div class="channel-byline" v-emoji>
|
||||||
|
<div class="channel-name" v-text="video.channelName" />
|
||||||
<!-- Description -->
|
<div
|
||||||
<div v-if="showMore">
|
class="caption background--text"
|
||||||
<div class="scroll-y ma-4">
|
:class="
|
||||||
<slim-video-description-renderer
|
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
|
||||||
:render="video.renderedData.description"
|
"
|
||||||
/>
|
v-text="video.channelSubs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="channel-buttons"
|
||||||
|
style="color: rgb(204, 0, 0); text-transform: uppercase"
|
||||||
|
>
|
||||||
|
subscribe
|
||||||
|
</div>
|
||||||
|
</v-card>
|
||||||
|
<v-divider />
|
||||||
</div>
|
</div>
|
||||||
<v-divider />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Comments -->
|
<!-- Description -->
|
||||||
<div
|
<div v-if="showMore">
|
||||||
v-if="loaded && video.commentData"
|
<div class="scroll-y ma-4">
|
||||||
@click="showComments = !showComments"
|
<slim-video-description-renderer
|
||||||
>
|
:render="video.renderedData.description"
|
||||||
<v-card flat class="background comment-renderer">
|
/>
|
||||||
<v-text class="comment-count keep-spaces">
|
</div>
|
||||||
<template v-for="text in video.commentData.headerText.runs">
|
<v-divider />
|
||||||
<template v-if="text.bold">
|
</div>
|
||||||
<strong :key="text.text">{{ text.text }}</strong>
|
|
||||||
|
<!-- Comments -->
|
||||||
|
<div
|
||||||
|
v-if="loaded && video.commentData"
|
||||||
|
@click="showComments = !showComments"
|
||||||
|
>
|
||||||
|
<v-card flat class="background comment-renderer">
|
||||||
|
<v-text class="comment-count keep-spaces">
|
||||||
|
<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>
|
</template>
|
||||||
<template v-else>{{ text.text }}</template>
|
</v-text>
|
||||||
</template>
|
<v-icon v-if="showComments">mdi-unfold-less-horizontal</v-icon>
|
||||||
</v-text>
|
<v-icon v-else>mdi-unfold-more-horizontal</v-icon>
|
||||||
<v-icon v-if="showComments">mdi-unfold-less-horizontal</v-icon>
|
</v-card>
|
||||||
<v-icon v-else>mdi-unfold-more-horizontal</v-icon>
|
<v-divider />
|
||||||
</v-card>
|
</div>
|
||||||
<v-divider />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<v-card v-if="showComments">
|
<swipeable-bottom-sheet
|
||||||
<v-subheader>Hello World</v-subheader>
|
v-model="showComments"
|
||||||
</v-card>
|
hide-overlay
|
||||||
|
persistent
|
||||||
|
no-click-animation
|
||||||
|
attach="#content-container"
|
||||||
|
v-if="loaded && video.commentData"
|
||||||
|
>
|
||||||
|
<mainCommentRenderer
|
||||||
|
:defaultContinuation="video.commentContinuation"
|
||||||
|
:commentData="video.commentData"
|
||||||
|
v-model="showComments"
|
||||||
|
></mainCommentRenderer>
|
||||||
|
</swipeable-bottom-sheet>
|
||||||
|
|
||||||
<!-- Related Videos -->
|
<!-- <swipeable-bottom-sheet
|
||||||
<div class="loaders" v-if="!loaded">
|
:v-model="showComments"
|
||||||
<v-skeleton-loader
|
style="z-index: 9999999"
|
||||||
type="list-item-two-line, actions, divider, list-item-avatar, divider, list-item-three-line"
|
></swipeable-bottom-sheet> -->
|
||||||
/>
|
|
||||||
<vid-load-renderer :count="5" />
|
<!-- Related Videos -->
|
||||||
|
<div class="loaders" 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" />
|
||||||
</div>
|
</div>
|
||||||
<item-section-renderer v-else :render="recommends" />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -176,8 +209,13 @@ import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
||||||
import { getCpn } from "~/plugins/utils";
|
import { getCpn } from "~/plugins/utils";
|
||||||
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
|
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
|
||||||
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
||||||
|
import legacyPlayer from "~/components/Player/legacy.vue"
|
||||||
import vuetubePlayer from "~/components/Player/index.vue";
|
import vuetubePlayer from "~/components/Player/index.vue";
|
||||||
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
||||||
|
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
|
||||||
|
import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
|
||||||
|
|
||||||
|
import { App as CapacitorApp } from "@capacitor/app";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
@ -185,7 +223,10 @@ export default {
|
||||||
VidLoadRenderer,
|
VidLoadRenderer,
|
||||||
SlimVideoDescriptionRenderer,
|
SlimVideoDescriptionRenderer,
|
||||||
vuetubePlayer,
|
vuetubePlayer,
|
||||||
|
legacyPlayer,
|
||||||
ItemSectionRenderer,
|
ItemSectionRenderer,
|
||||||
|
SwipeableBottomSheet,
|
||||||
|
mainCommentRenderer,
|
||||||
},
|
},
|
||||||
layout: "empty",
|
layout: "empty",
|
||||||
// transition(to) { // TODO: fix layout switching
|
// transition(to) { // TODO: fix layout switching
|
||||||
|
@ -215,13 +256,34 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
mounted() {
|
mounted() {
|
||||||
this.mountedInit();
|
this.mountedInit();
|
||||||
|
|
||||||
|
this.backHandler = CapacitorApp.addListener(
|
||||||
|
"backButton",
|
||||||
|
({ canGoBack }) => {
|
||||||
|
//--- Back Closes Search ---//
|
||||||
|
if (this.showComments) {
|
||||||
|
this.showComments = false;
|
||||||
|
|
||||||
|
//--- Back Goes Back ---//
|
||||||
|
} else if (!canGoBack) {
|
||||||
|
this.$router.replace(
|
||||||
|
`/${localStorage.getItem("startPage") || "home"}`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
destroyed() {
|
beforeDestroy() {
|
||||||
clearInterval(this.interval);
|
clearInterval(this.interval);
|
||||||
|
if (this.backHandler) this.backHandler.remove();
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
getVideo() {
|
getVideo() {
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
|
@ -254,7 +316,7 @@ export default {
|
||||||
this.cpn = getCpn();
|
this.cpn = getCpn();
|
||||||
this.initWatchTime().then(() => {
|
this.initWatchTime().then(() => {
|
||||||
this.sendWatchTime();
|
this.sendWatchTime();
|
||||||
this.interval = setInterval(this.sendWatchTime, 30000);
|
this.interval = setInterval(this.sendWatchTime, 60000);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -369,6 +431,19 @@ export default {
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
#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 {
|
.vertical-button span.v-btn__content {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: space-around;
|
justify-content: space-around;
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="accent">
|
<div>
|
||||||
|
|
||||||
<player :sources="sources" v-if="sources.length > 0" />
|
<player :sources="sources" v-if="sources.length > 0" />
|
||||||
|
|
||||||
|
|
|
@ -13,13 +13,14 @@ const ytApiVal = {
|
||||||
VERSION: "16.25",
|
VERSION: "16.25",
|
||||||
CLIENTNAME: "ANDROID",
|
CLIENTNAME: "ANDROID",
|
||||||
VERSION_WEB: "2.20220411.09.00",
|
VERSION_WEB: "2.20220411.09.00",
|
||||||
CLIENT_WEB: 2,
|
CLIENT_WEB_M: 2,
|
||||||
|
CLIENT_WEB_D: 1,
|
||||||
};
|
};
|
||||||
|
|
||||||
const filesystem = {
|
const filesystem = {
|
||||||
plugins: "vuetube/plugins",
|
plugins: "vuetube/plugins",
|
||||||
temp: "vuetube/temp",
|
temp: "vuetube/temp",
|
||||||
}
|
};
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
URLS: url,
|
URLS: url,
|
||||||
|
|
|
@ -117,7 +117,7 @@ class Innertube {
|
||||||
continuation: continuation,
|
continuation: continuation,
|
||||||
};
|
};
|
||||||
let url;
|
let url;
|
||||||
switch (type) {
|
switch (type.toLowerCase()) {
|
||||||
case "browse":
|
case "browse":
|
||||||
url = `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`;
|
url = `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`;
|
||||||
break;
|
break;
|
||||||
|
@ -159,7 +159,7 @@ class Innertube {
|
||||||
...{
|
...{
|
||||||
context: {
|
context: {
|
||||||
client: {
|
client: {
|
||||||
clientName: constants.YT_API_VALUES.CLIENT_WEB,
|
clientName: constants.YT_API_VALUES.CLIENT_WEB_M,
|
||||||
clientVersion: constants.YT_API_VALUES.VERSION_WEB,
|
clientVersion: constants.YT_API_VALUES.VERSION_WEB,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -344,6 +344,19 @@ class Innertube {
|
||||||
(content) => content.commentsEntryPointHeaderRenderer
|
(content) => content.commentsEntryPointHeaderRenderer
|
||||||
)?.commentsEntryPointHeaderRenderer,
|
)?.commentsEntryPointHeaderRenderer,
|
||||||
playbackTracking: responseInfo.playbackTracking,
|
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;
|
return vidData;
|
||||||
|
|
|
@ -133,15 +133,11 @@ const innertubeModule = {
|
||||||
},
|
},
|
||||||
|
|
||||||
async recommendContinuation(continuation, endpoint) {
|
async recommendContinuation(continuation, endpoint) {
|
||||||
const response = await InnertubeAPI.getContinuationsAsync(
|
const response = await this.getContinuation(continuation, endpoint);
|
||||||
continuation,
|
|
||||||
endpoint
|
|
||||||
);
|
|
||||||
const contents =
|
const contents =
|
||||||
response.data.continuationContents.sectionListContinuation.contents;
|
response.data.continuationContents.sectionListContinuation.contents;
|
||||||
const final = contents.map((shelves) => {
|
const final = contents.map((shelves) => {
|
||||||
const video = shelves.shelfRenderer?.content?.horizontalListRenderer;
|
const video = shelves.shelfRenderer?.content?.horizontalListRenderer;
|
||||||
|
|
||||||
if (video) return video;
|
if (video) return video;
|
||||||
});
|
});
|
||||||
const continuations =
|
const continuations =
|
||||||
|
@ -149,6 +145,26 @@ const innertubeModule = {
|
||||||
return { continuations: continuations, contents: final };
|
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_D,
|
||||||
|
clientVersion: constants.YT_API_VALUES.VERSION_WEB,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return await InnertubeAPI.getContinuationsAsync(
|
||||||
|
continuation,
|
||||||
|
endpoint,
|
||||||
|
contextAdditional
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
async search(query) {
|
async search(query) {
|
||||||
try {
|
try {
|
||||||
const response = await InnertubeAPI.getSearchAsync(query);
|
const response = await InnertubeAPI.getSearchAsync(query);
|
||||||
|
|
1
NUXT/static/channel_notice.svg
Normal file
1
NUXT/static/channel_notice.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.7 KiB |
31
readme.md
31
readme.md
|
@ -25,17 +25,21 @@ Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
|
||||||
<a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a>
|
<a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a>
|
||||||
<a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a>
|
<a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a>
|
||||||
<a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
|
<a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
|
||||||
|
<a href="https://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a>
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
# Features
|
|
||||||
- 🎨 Themes: Light, Dark, OLED, All the colors of the rainbow
|
- 🎨 Themes: Light, Dark, OLED, All the colors of the rainbow
|
||||||
- 🖌️ Customizable UI: You can fully customize the accent color, and other parts of the UI to remove features that you don't use!
|
- 🖌️ Customizable UI: You can fully customize the accent color, and other parts of the UI to remove features that you don't use!
|
||||||
- ⬆️ Auto Update: Be notified when an update is available & downgrade if you dislike it!
|
- ⬆️ Auto Update: Be notified when an update is available & downgrade if you dislike it!
|
||||||
- 👁️ Tracking Protection: No telemetry is sent from your device to YouTube
|
- 👁️ Tracking Protection: No telemetry is sent from your device by default
|
||||||
|
|
||||||
|
## Install
|
||||||
|
|
||||||
# Install
|
|
||||||
To install please visit www.vuetube.app/install
|
To install please visit www.vuetube.app/install
|
||||||
|
|
||||||
# Plans
|
## Plans
|
||||||
|
|
||||||
- 🔍 Advanced Search
|
- 🔍 Advanced Search
|
||||||
- 🗞️ Locally store watch history
|
- 🗞️ Locally store watch history
|
||||||
- 📺 A custom video player
|
- 📺 A custom video player
|
||||||
|
@ -44,24 +48,31 @@ To install please visit www.vuetube.app/install
|
||||||
- 🖼️ Picture in picture mode
|
- 🖼️ Picture in picture mode
|
||||||
- and more!
|
- and more!
|
||||||
|
|
||||||
# Screenshots
|
## Screenshots
|
||||||
|
|
||||||
View on our website: [https://vuetube.app/info/screenshots](https://vuetube.app/info/screenshots)
|
View on our website: [https://vuetube.app/info/screenshots](https://vuetube.app/info/screenshots)
|
||||||
|
|
||||||
## Technologies used
|
### Technologies used
|
||||||
|
|
||||||
<a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368956760074/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
|
<a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368956760074/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
|
||||||
|
|
||||||
## Why am I doing this?
|
### Why am I doing this?
|
||||||
|
|
||||||
Well this has been thrown around on the Return Youtube Dislike discord server for quite some time, so I figured that I should probably take a crack at it!
|
Well this has been thrown around on the Return Youtube Dislike discord server for quite some time, so I figured that I should probably take a crack at it!
|
||||||
|
|
||||||
Also, YouTube Vanced just shut down
|
### Want to contribute?
|
||||||
|
|
||||||
## Want to contribute?
|
|
||||||
Please read our website on how to do so: https://vuetube.app/contributing
|
Please read our website on how to do so: https://vuetube.app/contributing
|
||||||
|
|
||||||
# Contributors
|
## Contributors
|
||||||
|
|
||||||
<a href="https://github.com/Frontesque/VueTube/graphs/contributors">
|
<a href="https://github.com/Frontesque/VueTube/graphs/contributors">
|
||||||
<img src="https://contrib.rocks/image?repo=Frontesque/VueTube" />
|
<img src="https://contrib.rocks/image?repo=Frontesque/VueTube" />
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<sub>Made with [contrib.rocks](https://contrib.rocks). </sub>
|
<sub>Made with [contrib.rocks](https://contrib.rocks). </sub>
|
||||||
|
|
||||||
|
## Acknowledgements
|
||||||
|
|
||||||
|
- Emojis by the [Twemoji team](https://twemoji.twitter.com/), Licensed under [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||||
|
- VueTube Logo by [@afnzmn](https://github.com/afnzmn)
|
||||||
|
|
Loading…
Reference in a new issue