mirror of
https://github.com/VueTubeApp/VueTube
synced 2025-01-03 14:11:08 +00:00
commit
125dfc85d0
56 changed files with 2333 additions and 784 deletions
|
@ -1,12 +1,22 @@
|
|||
<template>
|
||||
<div class="comment-thread" v-if="commentRenderer">
|
||||
<a
|
||||
:href="
|
||||
this.$rendererUtils.getNavigationEndpoints(
|
||||
commentRenderer.authorEndpoint
|
||||
<div
|
||||
v-if="commentRenderer"
|
||||
v-ripple
|
||||
class="comment-thread px-3"
|
||||
@click="$emit('showReplies', comment)"
|
||||
>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
to="/channel"
|
||||
class="avatar-link mr-4"
|
||||
style="height: 3rem; width: 3rem"
|
||||
@click.prevent="
|
||||
$store.dispatch(
|
||||
'channel/fetchChannel',
|
||||
$rendererUtils.getNavigationEndpoints(commentRenderer.authorEndpoint)
|
||||
)
|
||||
"
|
||||
class="avatar-link"
|
||||
>
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
|
@ -16,83 +26,76 @@
|
|||
].url
|
||||
"
|
||||
/>
|
||||
</a>
|
||||
</v-btn>
|
||||
<div class="comment-content">
|
||||
<div class="comment-content--header subtitle-2">
|
||||
<div
|
||||
class="comment-content--header background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-5' : 'text--darken-4'"
|
||||
style="font-size: 0.8rem !important"
|
||||
>
|
||||
<div
|
||||
class="author-badge-name mr-1"
|
||||
:class="{ owner: commentRenderer.authorIsChannelOwner }"
|
||||
class="author-badge-name mr-2"
|
||||
:class="
|
||||
commentRenderer.authorIsChannelOwner
|
||||
? $vuetify.theme.dark
|
||||
? 'owner primary--text background lighten-2'
|
||||
: 'owner primary--text background darken-2'
|
||||
: ''
|
||||
"
|
||||
>
|
||||
<div class="author-name--wrapper">
|
||||
<span class="font-weight-bold author-name" v-emoji>
|
||||
<span class="author-name mr-1" 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"
|
||||
/>
|
||||
<author-comment-badge-renderer :metadata="badge" :key="index" />
|
||||
</template>
|
||||
<template
|
||||
v-for="(badge, index) in commentRenderer.sponsorCommentBadge"
|
||||
>
|
||||
<sponsor-comment-badge-renderer
|
||||
:metadata="badge"
|
||||
:key="index"
|
||||
class="ml-1"
|
||||
/>
|
||||
<sponsor-comment-badge-renderer :metadata="badge" :key="index" />
|
||||
</template>
|
||||
</div>
|
||||
<span
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
class="background--text comment-timestamp"
|
||||
>
|
||||
·
|
||||
<span class="comment-timestamp ml-2">
|
||||
{{ commentRenderer.publishedTimeText.runs[0].text }}
|
||||
</span>
|
||||
</div>
|
||||
<collapsable-text
|
||||
:lines="4"
|
||||
:expandText="
|
||||
:lines="3"
|
||||
:expand-text="
|
||||
commentRenderer.expandButton.buttonRenderer.text.runs[0].text
|
||||
"
|
||||
:collapseText="
|
||||
:collapse-text="
|
||||
commentRenderer.collapseButton.buttonRenderer.text.runs[0].text
|
||||
"
|
||||
>
|
||||
<yt-text-formatter :textRuns="commentRenderer.contentText.runs">
|
||||
<yt-text-formatter
|
||||
style="font-size: 0.9rem"
|
||||
:text-runs="commentRenderer.contentText.runs"
|
||||
>
|
||||
</yt-text-formatter>
|
||||
</collapsable-text>
|
||||
<div class="toolbar">
|
||||
<div class="toolbar mt-2">
|
||||
<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>
|
||||
<v-icon small>mdi-thumb-up-outline</v-icon>
|
||||
<span
|
||||
v-if="commentRenderer.voteCount"
|
||||
class="like-count caption"
|
||||
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>
|
||||
<v-icon class="ml-2" small>mdi-thumb-down-outline</v-icon>
|
||||
</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">
|
||||
<div class="toolbar--item ml-6" v-if="commentRenderer.replyCount">
|
||||
<v-icon small>mdi-comment-outline</v-icon>
|
||||
<span
|
||||
class="like-count caption"
|
||||
v-text="commentRenderer.replyCount"
|
||||
class="like-count mr-1 subtitle-2"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -113,7 +116,6 @@
|
|||
padding: 10px 0;
|
||||
|
||||
.avatar-thumbnail {
|
||||
margin-right: 0.5rem;
|
||||
border-radius: 50%;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
|
@ -157,14 +159,9 @@
|
|||
}
|
||||
|
||||
.owner {
|
||||
padding: 0 0.6em;
|
||||
background-color: #888888;
|
||||
color: #fff;
|
||||
border-radius: 1em;
|
||||
|
||||
&::v-deep .author-badge {
|
||||
color: #fff;
|
||||
}
|
||||
padding: 0 0.3em 0 0.6em;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.toolbar--button::v-deep.v-btn--active .v-btn__content {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="comment-header" v-if="boxRenderer">
|
||||
<div class="comment-header px-3" v-if="boxRenderer">
|
||||
<div class="avatar-container">
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
|
@ -22,6 +22,22 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["comment"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
boxRenderer: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
|
@ -43,19 +59,3 @@
|
|||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["comment"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
boxRenderer: null,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -15,29 +15,43 @@
|
|||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-for="(comment, index) in comments">
|
||||
<v-list-item :key="index">
|
||||
<div
|
||||
v-for="(comment, index) in comments"
|
||||
:key="index"
|
||||
class="commentElement"
|
||||
>
|
||||
<v-list-item class="px-0">
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||
:is="Object.keys(comment)[0]"
|
||||
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||
:comment="comment[Object.keys(comment)[0]]"
|
||||
@intersect="paginate"
|
||||
@showReplies="openReply"
|
||||
></component>
|
||||
</v-list-item>
|
||||
<v-divider
|
||||
v-if="getComponents()[Object.keys(comment)[0]]"
|
||||
:key="index"
|
||||
></v-divider>
|
||||
</template>
|
||||
<v-divider v-if="getComponents()[Object.keys(comment)[0]]"></v-divider>
|
||||
</div>
|
||||
|
||||
<div class="loading" v-if="loading">
|
||||
<v-sheet
|
||||
color="background"
|
||||
v-for="i in comments.length <= 0 ? 5 : 1"
|
||||
:key="i"
|
||||
color="background"
|
||||
>
|
||||
<v-skeleton-loader type="list-item-avatar-three-line" />
|
||||
</v-sheet>
|
||||
</div>
|
||||
|
||||
<template v-slot:reveal>
|
||||
<main-comment-reply-renderer
|
||||
v-if="showReply && replyData"
|
||||
v-model="showReply"
|
||||
:parentComment="replyData.parent"
|
||||
:defaultContinuation="replyData.replyContinuation"
|
||||
class="transition-fast-in-fast-out v-card--reveal"
|
||||
style="height: 100%"
|
||||
></main-comment-reply-renderer>
|
||||
</template>
|
||||
</dialog-base>
|
||||
</template>
|
||||
|
||||
|
@ -46,6 +60,7 @@ 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";
|
||||
import mainCommentReplyRenderer from "~/components/Comments/mainCommentReplyRenderer.vue";
|
||||
|
||||
export default {
|
||||
props: ["defaultContinuation", "commentData", "showComments"],
|
||||
|
@ -60,12 +75,15 @@ export default {
|
|||
commentsHeaderRenderer,
|
||||
commentThreadRenderer,
|
||||
continuationItemRenderer,
|
||||
mainCommentReplyRenderer,
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: true,
|
||||
comments: [],
|
||||
continuation: null,
|
||||
showReply: false,
|
||||
replyData: {},
|
||||
}),
|
||||
|
||||
mounted() {
|
||||
|
@ -124,6 +142,11 @@ export default {
|
|||
|
||||
return newContinuation;
|
||||
},
|
||||
|
||||
openReply(event) {
|
||||
this.showReply = true;
|
||||
this.replyData = { parent: event, replyContinuation: null };
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
48
NUXT/components/Comments/mainCommentReplyRenderer.vue
Normal file
48
NUXT/components/Comments/mainCommentReplyRenderer.vue
Normal file
|
@ -0,0 +1,48 @@
|
|||
<template>
|
||||
<dialog-base>
|
||||
<template v-slot:header>
|
||||
<v-btn icon @click="$emit('changeState', false)">
|
||||
<v-icon>mdi-arrow-left</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title>
|
||||
<strong>Replies</strong>
|
||||
</v-toolbar-title>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn icon @click="$emit('closeComments')">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<template>
|
||||
<comment-thread-renderer :comment="parentComment" />
|
||||
<v-divider></v-divider>
|
||||
<comment-thread-renderer
|
||||
v-for="index in 10"
|
||||
v-bind:key="index"
|
||||
:comment="parentComment"
|
||||
/>
|
||||
</template>
|
||||
</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", "parentComment", "showReplies"],
|
||||
|
||||
model: {
|
||||
prop: "showReplies",
|
||||
event: "changeState",
|
||||
},
|
||||
|
||||
components: {
|
||||
dialogBase,
|
||||
commentsHeaderRenderer,
|
||||
commentThreadRenderer,
|
||||
continuationItemRenderer,
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,15 +1,28 @@
|
|||
<template>
|
||||
<v-card
|
||||
class="entry gridVideoRenderer background"
|
||||
:to="`/watch?v=${video.videoId}`"
|
||||
flat
|
||||
to="/channel"
|
||||
class="entry gridVideoRenderer background"
|
||||
:class="
|
||||
roundThumb && roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: roundThumb ? `${roundTweak / 2}rem` : '0',
|
||||
margin:
|
||||
roundThumb && roundTweak > 0 ? '0 1rem 1rem 1rem' : '0 0 0.25rem 0',
|
||||
}"
|
||||
@click="$store.dispatch('channel/fetchChannel', video.channelId)"
|
||||
>
|
||||
<div id="details">
|
||||
<div id="details" class="pa-4">
|
||||
<a
|
||||
:href="
|
||||
this.$rendererUtils.getNavigationEndpoints(video.navigationEndpoint)
|
||||
"
|
||||
class="avatar-link pt-2"
|
||||
class="avatar-link"
|
||||
>
|
||||
<v-img
|
||||
class="avatar-thumbnail"
|
||||
|
@ -19,12 +32,11 @@
|
|||
"
|
||||
/>
|
||||
</a>
|
||||
<v-card-text class="video-info pt-2" v-emoji>
|
||||
<v-card-text class="video-info py-0" v-emoji>
|
||||
<div
|
||||
v-for="title in video.title.runs"
|
||||
:key="title.text"
|
||||
style="margin-top: 0.5em"
|
||||
class="vid-title"
|
||||
class="vid-title mt-1"
|
||||
>
|
||||
{{ title.text }}
|
||||
</div>
|
||||
|
@ -35,10 +47,41 @@
|
|||
v-text="parseBottom(video)"
|
||||
/>
|
||||
</v-card-text>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
elevation="0"
|
||||
style="width: 50px !important; height: 50px !important; z-index: 420"
|
||||
>
|
||||
<v-icon>mdi-share-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
},
|
||||
roundThumb() {
|
||||
return this.$store.state.tweaks.roundThumb;
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
parseBottom(video) {
|
||||
const bottomText = [
|
||||
video.subscriberCountText?.runs[0].text,
|
||||
video.videoCountText?.runs.map((run) => run.text).join(" "),
|
||||
];
|
||||
return bottomText.join(" · ");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
|
@ -53,8 +96,6 @@
|
|||
}
|
||||
|
||||
.avatar-thumbnail {
|
||||
margin-top: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
border-radius: 50%;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
|
@ -64,7 +105,6 @@
|
|||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-basis: auto;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
@media screen and (orientation: landscape) {
|
||||
|
@ -76,19 +116,3 @@
|
|||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
|
||||
methods: {
|
||||
parseBottom(video) {
|
||||
const bottomText = [
|
||||
video.subscriberCountText?.runs[0].text,
|
||||
video.videoCountText?.runs.map((run) => run.text).join(" "),
|
||||
];
|
||||
return bottomText.join(" · ");
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
<v-list-item
|
||||
v-for="(video, index) in render.items"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
|
|
|
@ -3,11 +3,11 @@
|
|||
<v-list-item
|
||||
v-for="(renderer, index) in render.contents"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(renderer)[0]]"
|
||||
:is="Object.keys(renderer)[0]"
|
||||
v-if="getComponents()[Object.keys(renderer)[0]]"
|
||||
:key="index"
|
||||
:render="renderer[Object.keys(renderer)[0]]"
|
||||
></component>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
<template>
|
||||
<div>
|
||||
<div class="fill-width">
|
||||
<v-list-item
|
||||
v-for="(video, index) in render.items"
|
||||
:key="index"
|
||||
class="pa-0"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
:is="Object.keys(video)[0]"
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
:key="video[Object.keys(video)[0]].videoId"
|
||||
:video="video[Object.keys(video)[0]]"
|
||||
></component>
|
||||
|
|
|
@ -1,95 +1,116 @@
|
|||
<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" />
|
||||
<div ref="controlsWrap" class="controlsWrap">
|
||||
<div class="centerVideoControls">
|
||||
<v-btn
|
||||
text
|
||||
style="height: 5em; width: 5em"
|
||||
color="white"
|
||||
@click="togglePlaying()"
|
||||
>
|
||||
<v-icon
|
||||
ref="pausePlayIndicator"
|
||||
size="5rem"
|
||||
v-text="playing ? 'mdi-pause' : 'mdi-play'"
|
||||
/>
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
<div class="bottomVideoControls">
|
||||
{{ watched }} <span style="color: #999;">/ {{ $vuetube.humanTime(video.duration) }}</span>
|
||||
<div class="bottomVideoControls white--text">
|
||||
<div class="pl-4">
|
||||
{{ watched }}
|
||||
<span style="color: #999">
|
||||
/ {{ $vuetube.humanTime(video.duration) }}
|
||||
</span>
|
||||
</div>
|
||||
<!-- <v-slider
|
||||
id="slider"
|
||||
v-model="video.currentTime"
|
||||
:max="video.duration"
|
||||
style="margin-bottom: -2rem !important"
|
||||
/> -->
|
||||
</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"],
|
||||
export default {
|
||||
props: ["video"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
playing: true,
|
||||
controls: true,
|
||||
data() {
|
||||
return {
|
||||
playing: true,
|
||||
controls: true,
|
||||
|
||||
watched: 0,
|
||||
watched: 0,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
console.log("videovideovideo");
|
||||
console.log(this.video);
|
||||
this.video.ontimeupdate = () => {
|
||||
console.log(this.video.currentTime);
|
||||
this.watched = this.$vuetube.humanTime(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
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.video.ontimeupdate = () => {
|
||||
console.log(this.video.currentTime)
|
||||
this.watched = this.$vuetube.humanTime(this.video.currentTime);
|
||||
};
|
||||
toggleControls() {
|
||||
const setControls = this.controls ? "none" : "block";
|
||||
this.$refs.controlsWrap.style.display = setControls;
|
||||
this.controls = !this.controls;
|
||||
},
|
||||
|
||||
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>
|
||||
|
||||
<style scoped>
|
||||
#slider * {
|
||||
padding: 0 !important;
|
||||
margin: 0 !important;
|
||||
}
|
||||
.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>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
<template>
|
||||
<div style="position: relative;">
|
||||
<div style="position: relative">
|
||||
<video
|
||||
ref="player"
|
||||
autoplay
|
||||
:src="vidSrc"
|
||||
width="100%"
|
||||
style="max-height: 50vh; display: block"
|
||||
style="max-height: 50vh; display: block; overflow: hidden !important"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundWatch
|
||||
? `${$store.state.tweaks.roundTweak / 4}rem`
|
||||
: '0',
|
||||
}"
|
||||
@webkitfullscreenchange="handleFullscreenChange"
|
||||
/>
|
||||
|
||||
|
||||
<seekbar :video=$refs.player v-if="$refs.player" />
|
||||
<seekbar :video="$refs.player" v-if="$refs.player" />
|
||||
<controls v-if="$refs.player" :video="$refs.player" />
|
||||
|
||||
<!-- <v-slider v-model="value" step="0"></v-slider> -->
|
||||
|
@ -18,22 +22,22 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import seekbar from '~/components/Player/seekbar.vue';
|
||||
import controls from '~/components/Player/controls.vue';
|
||||
import seekbar from "~/components/Player/seekbar.vue";
|
||||
import controls from "~/components/Player/controls.vue";
|
||||
|
||||
export default {
|
||||
props: ["sources"],
|
||||
components: {
|
||||
seekbar,
|
||||
controls
|
||||
controls,
|
||||
},
|
||||
props: ["sources"],
|
||||
data() {
|
||||
return {
|
||||
vidSrc: "",
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.vidSrc = this.sources[this.sources.length-1].url;
|
||||
this.vidSrc = this.sources[this.sources.length - 1].url;
|
||||
},
|
||||
methods: {
|
||||
handleFullscreenChange() {
|
||||
|
|
|
@ -1,22 +1,263 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
class="d-flex flex-column"
|
||||
style="position: relative"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundWatch
|
||||
? `${$store.state.tweaks.roundTweak / 3}rem`
|
||||
: '0',
|
||||
}"
|
||||
>
|
||||
<video
|
||||
ref="player"
|
||||
controls
|
||||
autoplay
|
||||
:src="vidSrc"
|
||||
width="100%"
|
||||
style="max-height: 50vh; display: block"
|
||||
:src="vidSrc"
|
||||
@webkitfullscreenchange="handleFullscreenChange"
|
||||
/>
|
||||
<!-- <v-slider v-model="value" step="0"></v-slider> -->
|
||||
<video
|
||||
ref="playerfake"
|
||||
muted
|
||||
autoplay
|
||||
style="display: none"
|
||||
:src="vidSrc"
|
||||
/>
|
||||
<v-progress-linear
|
||||
query
|
||||
active
|
||||
style="width: 100%"
|
||||
background-color="primary"
|
||||
background-opacity="0.5"
|
||||
:buffer-value="buffered"
|
||||
:value="percent"
|
||||
color="primary"
|
||||
height="4"
|
||||
/>
|
||||
|
||||
<!-- <v-btn
|
||||
text
|
||||
tile
|
||||
class="debug"
|
||||
style="position: absolute; top: 0; left: 0; width: 50%; height: 100%"
|
||||
>
|
||||
<v-icon>mdi-rewind</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
class="debug"
|
||||
style="position: absolute; top: 0; left: 50%; width: 50%; height: 100%"
|
||||
>
|
||||
<v-icon>mdi-fast-forward</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
class="debug"
|
||||
style="position: absolute; top: 1rem; left: 1rem; border-radius: 2rem"
|
||||
to="home"
|
||||
disabled
|
||||
>
|
||||
<v-icon>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
class="debug"
|
||||
style="position: absolute; top: 1rem; right: 1rem; border-radius: 2rem"
|
||||
to="home"
|
||||
>
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn> -->
|
||||
|
||||
<!-- <v-btn
|
||||
v-if="$refs.player"
|
||||
fab
|
||||
text
|
||||
style="
|
||||
height: 5rem;
|
||||
width: 5rem;
|
||||
position: absolute;
|
||||
top: calc(50% - 2.5rem);
|
||||
left: calc(50% - 2.5rem);
|
||||
"
|
||||
class="debug"
|
||||
color="white"
|
||||
@click="$refs.player.paused ? $refs.player.play() : $refs.player.pause()"
|
||||
>
|
||||
<v-icon
|
||||
ref="pausePlayIndicator"
|
||||
size="5rem"
|
||||
v-text="$refs.player.paused ? 'mdi-play' : 'mdi-pause'"
|
||||
/>
|
||||
</v-btn> -->
|
||||
|
||||
<!-- <div
|
||||
v-if="$vuetify"
|
||||
class="debug px-4 rounded-xl"
|
||||
style="position: absolute; bottom: 2rem; left: 1rem"
|
||||
>
|
||||
{{ watched }}
|
||||
<span style="color: #999"> / {{ total }} </span>
|
||||
</div>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
class="debug"
|
||||
style="
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
right: 1rem;
|
||||
border-radius: 0 2rem 2rem 0;
|
||||
"
|
||||
>
|
||||
HD
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
class="debug"
|
||||
style="
|
||||
position: absolute;
|
||||
bottom: 2rem;
|
||||
right: 5rem;
|
||||
border-radius: 2rem 0 0 2rem;
|
||||
"
|
||||
>
|
||||
1X
|
||||
</v-btn> -->
|
||||
<v-slider
|
||||
v-if="$refs.player"
|
||||
dense
|
||||
height="4"
|
||||
hide-details
|
||||
style="
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
z-index: 69420;
|
||||
"
|
||||
:value="progress"
|
||||
:max="duration"
|
||||
@start="scrubbing = true"
|
||||
@end="scrubbing = false"
|
||||
@input="seek($event)"
|
||||
@change="scrub($event)"
|
||||
>
|
||||
<template #thumb-label="{ value }">
|
||||
<canvas
|
||||
ref="preview"
|
||||
class="rounded-lg mb-8"
|
||||
style="
|
||||
border: 4px solid var(--v-primary-base);
|
||||
margin-top: -20px !important;
|
||||
top: -20px !important;
|
||||
"
|
||||
:width="$refs.player.clientWidth / 4"
|
||||
:height="$refs.player.clientHeight / 4"
|
||||
></canvas>
|
||||
</template>
|
||||
</v-slider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["vidSrc"],
|
||||
data() {
|
||||
return {
|
||||
scrubbing: false,
|
||||
percent: 0,
|
||||
progress: 0,
|
||||
buffered: 0,
|
||||
duration: 0,
|
||||
watched: 0,
|
||||
total: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
let vid = this.$refs.player;
|
||||
vid.addEventListener("loadeddata", (e) => {
|
||||
console.log("%c loadeddata", "color: #00ff00");
|
||||
console.log(e);
|
||||
//Video should now be loaded but we can add a second check
|
||||
if (vid.readyState >= 3) {
|
||||
vid.ontimeupdate = () => {
|
||||
console.log("%c timeupdate", "color: #aaaaff");
|
||||
this.duration = vid.duration;
|
||||
if (!this.scrubbing) this.progress = vid.currentTime;
|
||||
this.percent = (vid.currentTime / vid.duration) * 100;
|
||||
this.watched = this.$vuetube.humanTime(vid.currentTime);
|
||||
this.total = this.$vuetube.humanTime(vid.duration);
|
||||
};
|
||||
vid.onprogress = () => {
|
||||
console.log("%c progress", "color: #ff00ff");
|
||||
this.buffered = (vid.buffered.end(0) / vid.duration) * 100;
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
// TODO: screenshot-based faster dynamic thumbnail preview
|
||||
// var video = document.getElementById("thumb");
|
||||
// video.addEventListener("loadedmetadata", initScreenshot);
|
||||
// video.addEventListener("playing", startScreenshot);
|
||||
// video.addEventListener("pause", stopScreenshot);
|
||||
// video.addEventListener("ended", stopScreenshot);
|
||||
|
||||
// var canvas = document.getElementById("canvas");
|
||||
// var ctx = canvas.getContext("2d");
|
||||
// var ssContainer = document.getElementById("screenShots");
|
||||
// var videoHeight, videoWidth;
|
||||
// var drawTimer = null;
|
||||
|
||||
// function initScreenshot() {
|
||||
// videoHeight = video.videoHeight;
|
||||
// videoWidth = video.videoWidth;
|
||||
// canvas.width = videoWidth;
|
||||
// canvas.height = videoHeight;
|
||||
// }
|
||||
|
||||
// function startScreenshot() {
|
||||
// if (drawTimer == null) {
|
||||
// drawTimer = setInterval(grabScreenshot, 1000);
|
||||
// }
|
||||
// }
|
||||
|
||||
// function stopScreenshot() {
|
||||
// if (drawTimer) {
|
||||
// clearInterval(drawTimer);
|
||||
// drawTimer = null;
|
||||
// }
|
||||
// }
|
||||
|
||||
// function grabScreenshot() {
|
||||
// ctx.drawImage(video, 0, 0, videoWidth, videoHeight);
|
||||
// var img = new Image();
|
||||
// img.src = canvas.toDataURL("image/png");
|
||||
// img.width = 120;
|
||||
// ssContainer.appendChild(img);
|
||||
// }
|
||||
methods: {
|
||||
seek(e) {
|
||||
console.log(`scrubbing ${e}`);
|
||||
let vid = this.$refs.playerfake;
|
||||
let canvas = this.$refs.preview;
|
||||
this.$refs.playerfake.currentTime = e;
|
||||
canvas
|
||||
.getContext("2d")
|
||||
.drawImage(
|
||||
vid,
|
||||
0,
|
||||
0,
|
||||
this.$refs.player.clientWidth / 4,
|
||||
this.$refs.player.clientHeight / 4
|
||||
);
|
||||
},
|
||||
scrub(e) {
|
||||
this.$refs.player.currentTime = e;
|
||||
},
|
||||
handleFullscreenChange() {
|
||||
if (document.fullscreenElement === this.$refs.player) {
|
||||
this.$vuetube.statusBar.hide();
|
||||
|
@ -26,10 +267,6 @@ export default {
|
|||
this.$vuetube.navigationBar.show();
|
||||
}
|
||||
},
|
||||
|
||||
getPlayer() {
|
||||
return this.$refs.player;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,23 +1,16 @@
|
|||
<template>
|
||||
<div>
|
||||
|
||||
|
||||
<v-progress-linear
|
||||
active
|
||||
background-color="primary"
|
||||
background-opacity="0.5"
|
||||
:buffer-value="buffered"
|
||||
color="primary"
|
||||
height="3"
|
||||
query
|
||||
:value="percentage"
|
||||
/>
|
||||
|
||||
|
||||
</div>
|
||||
<v-progress-linear
|
||||
active
|
||||
background-color="primary"
|
||||
background-opacity="0.5"
|
||||
:buffer-value="buffered"
|
||||
color="primary"
|
||||
height="3"
|
||||
query
|
||||
:value="percentage"
|
||||
/>
|
||||
</template>
|
||||
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
|
@ -25,8 +18,8 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
percentage: 0,
|
||||
buffered: 0
|
||||
}
|
||||
buffered: 0,
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
|
@ -36,7 +29,6 @@ export default {
|
|||
this.video.addEventListener("progress", () => {
|
||||
this.buffered = (this.video.buffered.end(0) / this.video.duration) * 100;
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<div>
|
||||
<div
|
||||
<div class="fill-width">
|
||||
<v-list-item
|
||||
v-for="(video, index) in render.contents"
|
||||
:key="index"
|
||||
class="pa-0 fill-screen"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<component
|
||||
v-if="getComponents()[Object.keys(video)[0]]"
|
||||
|
@ -11,13 +11,15 @@
|
|||
:key="video[Object.keys(video)[0]].videoId"
|
||||
:video="video[Object.keys(video)[0]]"
|
||||
></component>
|
||||
</div>
|
||||
</v-list-item>
|
||||
<div
|
||||
v-if="
|
||||
render.separatorDetails && render.separatorDetails.hasBottomSeparator
|
||||
render.separatorDetails &&
|
||||
render.separatorDetails.hasBottomSeparator &&
|
||||
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0)
|
||||
"
|
||||
class="separator-bottom background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-4' : 'darken-4'"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
:style="{ height: render.separatorDetails.height + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
|
@ -28,10 +30,6 @@
|
|||
width: 100%; /* Prevent Loading Weirdness */
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.fill-screen {
|
||||
width: 100vw; /* Very Hacky */
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
|
|
|
@ -1,32 +1,36 @@
|
|||
<template>
|
||||
<div>
|
||||
<h4 v-if="render.headerRenderer" class="font-weight-bold shelf-header">
|
||||
<div class="fill-width">
|
||||
<h4
|
||||
v-if="render.headerRenderer"
|
||||
class="font-weight-bold shelf-header"
|
||||
:style="{
|
||||
marginLeft:
|
||||
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
|
||||
? '1rem'
|
||||
: '0',
|
||||
}"
|
||||
>
|
||||
{{
|
||||
render.headerRenderer.elementRenderer.newElement.type.componentType
|
||||
.model.shelfHeaderModel.shelfHeaderData.title
|
||||
}}
|
||||
</h4>
|
||||
<component
|
||||
v-if="render.content && getComponents()[Object.keys(render.content)[0]]"
|
||||
:is="Object.keys(render.content)[0]"
|
||||
:render="render.content[Object.keys(render.content)[0]]"
|
||||
></component>
|
||||
<div class="pa-0 min-height-0">
|
||||
<component
|
||||
:is="Object.keys(render.content)[0]"
|
||||
v-if="render.content && getComponents()[Object.keys(render.content)[0]]"
|
||||
:render="render.content[Object.keys(render.content)[0]]"
|
||||
></component>
|
||||
</div>
|
||||
<div
|
||||
v-if="render.separator && render.separator.hasBottomSeparator"
|
||||
class="separator-bottom background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-4' : 'darken-4'"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
:style="{ height: render.separator.height + 'px' }"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.shelf-header {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import verticalListRenderer from "~/components/ListRenderers/verticalListRenderer.vue";
|
||||
import horizontalListRenderer from "~/components/ListRenderers/horizontalListRenderer.vue";
|
||||
|
@ -45,3 +49,10 @@ export default {
|
|||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.shelf-header {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
padding: 10px;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -3,7 +3,9 @@
|
|||
<template v-for="(text, index) in textRuns">
|
||||
<template v-if="$rendererUtils.checkInternal(text)">
|
||||
<a
|
||||
@click="openInternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
@click.stop.prevent="
|
||||
openInternal($rendererUtils.getNavigationEndpoints(text))
|
||||
"
|
||||
:key="index"
|
||||
>{{ text.text }}</a
|
||||
>
|
||||
|
@ -14,7 +16,9 @@
|
|||
"
|
||||
>
|
||||
<a
|
||||
@click="openExternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
@click.stop.prevent="
|
||||
openExternal($rendererUtils.getNavigationEndpoints(text))
|
||||
"
|
||||
:key="index"
|
||||
>{{ text.text }}</a
|
||||
>
|
||||
|
|
|
@ -1,9 +1,28 @@
|
|||
<template>
|
||||
<v-card class="entry videoRenderer background" :to="`/watch?v=${vidId}`" flat>
|
||||
<div style="position: relative" class="thumbnail-container">
|
||||
<v-card
|
||||
class="entry videoRenderer background"
|
||||
:to="`/watch?v=${vidId}`"
|
||||
:class="
|
||||
roundThumb && roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: roundThumb ? `${roundTweak / 2}rem` : '0',
|
||||
margin:
|
||||
roundThumb && roundTweak > 0 ? '0 1rem 1rem 1rem' : '0 0 0.25rem 0',
|
||||
}"
|
||||
flat
|
||||
>
|
||||
<div style="position: relative" class="thumbnail-container overflow-hidden">
|
||||
<v-img
|
||||
:aspect-ratio="16 / 9"
|
||||
:src="$youtube.getThumbnail(vidId, 'max', thumbnails)"
|
||||
:style="{
|
||||
borderRadius: roundThumb ? `${roundTweak / 4}rem` : '0',
|
||||
}"
|
||||
/>
|
||||
<div
|
||||
v-if="thumbnailOverlayText && thumbnailOverlayStyle"
|
||||
|
@ -14,7 +33,13 @@
|
|||
/>
|
||||
</div>
|
||||
<div id="details">
|
||||
<a :href="channelUrl" class="avatar-link pt-2">
|
||||
<a
|
||||
@click.prevent="
|
||||
$store.dispatch('channel/fetchChannel', channelUrl),
|
||||
$router.push('/channel')
|
||||
"
|
||||
class="avatar-link pl-2 pt-2"
|
||||
>
|
||||
<v-img class="avatar-thumbnail" :src="channelIcon" />
|
||||
</a>
|
||||
<v-card-text class="video-info pt-2" v-emoji>
|
||||
|
@ -28,8 +53,8 @@
|
|||
</span>
|
||||
|
||||
<div
|
||||
class="background--text text--lighten-5 caption"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
class="background--text caption"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-5' : 'text--darken-4'"
|
||||
v-text="bottomText"
|
||||
/>
|
||||
</v-card-text>
|
||||
|
@ -37,6 +62,52 @@
|
|||
</v-card>
|
||||
</template>
|
||||
|
||||
<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,
|
||||
},
|
||||
},
|
||||
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
},
|
||||
roundThumb() {
|
||||
return this.$store.state.tweaks.roundThumb;
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%; /* Prevent Loading Weirdness */
|
||||
|
@ -45,8 +116,9 @@
|
|||
position: absolute;
|
||||
bottom: 10px;
|
||||
right: 10px;
|
||||
border-radius: 5px;
|
||||
border-radius: 4px;
|
||||
padding: 0px 4px 0px 4px;
|
||||
font-size: 0.66rem;
|
||||
}
|
||||
|
||||
.videoRuntimeFloat.style-DEFAULT {
|
||||
|
@ -105,40 +177,3 @@
|
|||
}
|
||||
}
|
||||
</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>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
</div>
|
||||
<a
|
||||
class="toggle-collapse background--text font-weight-bold"
|
||||
@click="expanded = !expanded"
|
||||
@click.stop.prevent="expanded = !expanded"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
v-if="expandable"
|
||||
v-text="expanded ? collapseText : expandText"
|
||||
|
|
|
@ -1,22 +1,39 @@
|
|||
<template>
|
||||
<v-bottom-navigation
|
||||
v-model="tabSelection"
|
||||
shift
|
||||
class="bottomNav py-4 transparent"
|
||||
:style="
|
||||
$vuetify.theme.dark
|
||||
? 'border-top: 1px solid var(--v-background-lighten1) !important;'
|
||||
: 'border-top: 1px solid var(--v-background-darken1) !important;'
|
||||
"
|
||||
>
|
||||
<v-btn
|
||||
v-for="(item, i) in tabs"
|
||||
:key="i"
|
||||
v-ripple="false"
|
||||
class="navButton"
|
||||
:to="item.link"
|
||||
plain
|
||||
<div class="bottomNav background">
|
||||
<v-divider v-if="!$store.state.tweaks.roundTweak" />
|
||||
<v-bottom-navigation
|
||||
v-model="tabSelection"
|
||||
style="padding: 0 !important; box-shadow: none !important"
|
||||
class="transparent"
|
||||
shift
|
||||
>
|
||||
<v-btn
|
||||
v-for="(item, i) in tabs"
|
||||
:key="i"
|
||||
v-ripple="false"
|
||||
class="navButton"
|
||||
:to="item.link"
|
||||
plain
|
||||
>
|
||||
<span v-text="item.name" />
|
||||
<v-icon
|
||||
:color="
|
||||
tabSelection == i
|
||||
? 'primary'
|
||||
: $vuetify.theme.dark
|
||||
? 'background lighten-4'
|
||||
: 'background darken-4'
|
||||
"
|
||||
:class="
|
||||
tabSelection == i
|
||||
? $vuetify.theme.dark
|
||||
? 'tab primary darken-4'
|
||||
: 'tab primary lighten-4'
|
||||
: ''
|
||||
"
|
||||
v-text="item.icon"
|
||||
/>
|
||||
<!--
|
||||
<span v-text="item.name" />
|
||||
<v-icon
|
||||
:color="
|
||||
|
@ -34,18 +51,20 @@
|
|||
: ''
|
||||
"
|
||||
v-text="item.icon"
|
||||
/>
|
||||
<!--
|
||||
Add the following to 'v-text- above to make the icons outlined unless active
|
||||
/> -->
|
||||
<!-- Add the following to 'v-text- above to make the icons outlined unless active
|
||||
+ (tabSelection == i ? '' : '-outline')
|
||||
|
||||
|
||||
-->
|
||||
</v-btn>
|
||||
<!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()"
|
||||
><v-icon>mdi-magnify</v-icon></v-btn
|
||||
> -->
|
||||
</v-bottom-navigation>
|
||||
</v-btn>
|
||||
<!-- <v-btn
|
||||
text
|
||||
class="navButton mr-2 fill-height"
|
||||
color="white"
|
||||
@click="searchBtn()"
|
||||
><v-icon>mdi-magnify</v-icon></v-btn
|
||||
> -->
|
||||
</v-bottom-navigation>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -70,22 +89,22 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
this.tabs[0].name = this.$lang('global').home;
|
||||
this.tabs[1].name = this.$lang('global').subscriptions;
|
||||
this.tabs[2].name = this.$lang('global').library;
|
||||
}
|
||||
|
||||
this.tabs[0].name = this.$lang("global").home;
|
||||
this.tabs[1].name = this.$lang("global").subscriptions;
|
||||
this.tabs[2].name = this.$lang("global").library;
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.bottomNav {
|
||||
/* ios gesture nav */
|
||||
bottom: env(safe-area-inset-bottom) !important;
|
||||
/* box-shadow: inset 0 0 10rem var(--v-background-base) !important; */
|
||||
height: calc(4rem + env(safe-area-inset-bottom)) !important;
|
||||
padding-bottom: env(safe-area-inset-bottom) !important;
|
||||
box-shadow: none !important;
|
||||
height: 4rem !important;
|
||||
padding: 0 !important;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
bottom: 0;
|
||||
}
|
||||
.navButton {
|
||||
width: 25vw !important;
|
||||
|
|
71
NUXT/components/communityCard.vue
Normal file
71
NUXT/components/communityCard.vue
Normal file
|
@ -0,0 +1,71 @@
|
|||
<template>
|
||||
<v-card
|
||||
v-ripple
|
||||
flat
|
||||
class="background"
|
||||
:class="
|
||||
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundThumb
|
||||
? `${$store.state.tweaks.roundTweak / 2}rem`
|
||||
: '0',
|
||||
margin:
|
||||
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
|
||||
? '0 1rem 1rem 1rem'
|
||||
: '0 0 0.25rem 0',
|
||||
}"
|
||||
>
|
||||
<div class="d-flex flex-row pa-4">
|
||||
<v-avatar size="50" color="primary" />
|
||||
<div class="d-flex flex-column my-auto pl-4">
|
||||
<b>Work in progress</b>
|
||||
<div
|
||||
class="background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
69 years ago
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
elevation="0"
|
||||
style="width: 50px !important; height: 50px !important"
|
||||
>
|
||||
<v-icon>mdi-share-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<p class="px-4" v-emoji>Blurb Blurb Text Goes Here ...</p>
|
||||
<v-img
|
||||
contain
|
||||
class="background my-4"
|
||||
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
|
||||
style="max-height: 15rem"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 4}rem`,
|
||||
marginLeft:
|
||||
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak) >
|
||||
0
|
||||
? '1rem'
|
||||
: '0',
|
||||
marginRight:
|
||||
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak) >
|
||||
0
|
||||
? '1rem'
|
||||
: '0',
|
||||
}"
|
||||
src="/dev.svg"
|
||||
/>
|
||||
<div class="d-flex flex-row px-4 pb-4">
|
||||
<v-icon>mdi-thumb-up-outline</v-icon>
|
||||
<b class="mx-2">123</b>
|
||||
<v-icon class="ml-2">mdi-thumb-down-outline</v-icon>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
|
@ -1,6 +1,9 @@
|
|||
<template>
|
||||
<v-card class="dialog-base">
|
||||
<div class="toolbar-container">
|
||||
<v-card flat class="dialog-base background">
|
||||
<div
|
||||
class="toolbar-container d-flex flex-column background"
|
||||
style="flex-direction: column !important"
|
||||
>
|
||||
<v-toolbar color="background" flat>
|
||||
<slot name="header"></slot>
|
||||
</v-toolbar>
|
||||
|
@ -9,6 +12,9 @@
|
|||
<div class="dialog-body background">
|
||||
<slot></slot>
|
||||
</div>
|
||||
<v-expand-transition>
|
||||
<slot name="reveal"></slot>
|
||||
</v-expand-transition>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
|
|
80
NUXT/components/playlistCard.vue
Normal file
80
NUXT/components/playlistCard.vue
Normal file
|
@ -0,0 +1,80 @@
|
|||
<template>
|
||||
<v-card
|
||||
v-ripple
|
||||
class="background d-flex flex-row overflow-hidden mb-4 mx-4"
|
||||
style="height: 6rem !important"
|
||||
:class="
|
||||
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundThumb
|
||||
? `${$store.state.tweaks.roundTweak / 2}rem`
|
||||
: '0',
|
||||
}"
|
||||
flat
|
||||
>
|
||||
<v-img
|
||||
contain
|
||||
src="/dev.svg"
|
||||
class="background"
|
||||
style="position: relative; max-width: 8rem !important"
|
||||
:class="$vuetify.theme.dark ? 'lighten-3' : 'darken-3'"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundThumb
|
||||
? `${$store.state.tweaks.roundTweak / 4}rem`
|
||||
: '0',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
class="background d-flex flex-column justify-center align-center"
|
||||
style="
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
width: 40%;
|
||||
height: 100%;
|
||||
opacity: 0.5;
|
||||
"
|
||||
>
|
||||
<div>420</div>
|
||||
<v-icon>mdi-playlist-play</v-icon>
|
||||
</div>
|
||||
</v-img>
|
||||
<div class="pa-4" v-emoji style="font-size: 0.75rem !important">
|
||||
<b>Work in Progress</b>
|
||||
|
||||
<div
|
||||
class="background--text caption mt-2"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
Bottom Text <br />
|
||||
420 videos
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer></v-spacer>
|
||||
<div class="d-flex flex-column">
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
elevation="0"
|
||||
class="flex-grow-1"
|
||||
style="width: 2rem !important"
|
||||
>
|
||||
<v-icon>mdi-playlist-plus</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
elevation="0"
|
||||
class="flex-grow-1"
|
||||
style="width: 2rem !important"
|
||||
>
|
||||
<v-icon>mdi-share-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-card>
|
||||
</template>
|
221
NUXT/components/ryd.vue
Normal file
221
NUXT/components/ryd.vue
Normal file
|
@ -0,0 +1,221 @@
|
|||
<template>
|
||||
<div class="ryd">
|
||||
<div class="ryd__container">
|
||||
<div class="ryd__content">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
apiUrl: "https://returnyoutubedislikeapi.com",
|
||||
storedData: {
|
||||
likes: 0,
|
||||
dislikes: 0,
|
||||
previousState: "NEUTRAL_STATE",
|
||||
},
|
||||
}),
|
||||
methods: {
|
||||
async solvePuzzle(puzzle) {
|
||||
let challenge = Uint8Array.from(atob(puzzle.challenge), (c) =>
|
||||
c.charCodeAt(0)
|
||||
);
|
||||
let buffer = new ArrayBuffer(20);
|
||||
let uInt8View = new Uint8Array(buffer);
|
||||
let uInt32View = new Uint32Array(buffer);
|
||||
let maxCount = Math.pow(2, puzzle.difficulty) * 3;
|
||||
for (let i = 4; i < 20; i++) {
|
||||
uInt8View[i] = challenge[i - 4];
|
||||
}
|
||||
|
||||
for (let i = 0; i < maxCount; i++) {
|
||||
uInt32View[0] = i;
|
||||
let hash = await crypto.subtle.digest("SHA-512", buffer);
|
||||
let hashUint8 = new Uint8Array(hash);
|
||||
if (countLeadingZeroes(hashUint8) >= puzzle.difficulty) {
|
||||
return {
|
||||
solution: btoa(
|
||||
String.fromCharCode.apply(null, uInt8View.slice(0, 4))
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
return {};
|
||||
},
|
||||
generateUserID(length = 36) {
|
||||
const charset =
|
||||
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let result = "";
|
||||
if (crypto && crypto.getRandomValues) {
|
||||
const values = new Uint32Array(length);
|
||||
crypto.getRandomValues(values);
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[values[i] % charset.length];
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
for (let i = 0; i < length; i++) {
|
||||
result += charset[Math.floor(Math.random() * charset.length)];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
},
|
||||
|
||||
storageChangeHandler(changes, area) {
|
||||
if (changes.disableVoteSubmission !== undefined) {
|
||||
handleDisableVoteSubmissionChangeEvent(
|
||||
changes.disableVoteSubmission.newValue
|
||||
);
|
||||
}
|
||||
if (changes.coloredThumbs !== undefined) {
|
||||
handleColoredThumbsChangeEvent(changes.coloredThumbs.newValue);
|
||||
}
|
||||
if (changes.coloredBar !== undefined) {
|
||||
handleColoredBarChangeEvent(changes.coloredBar.newValue);
|
||||
}
|
||||
if (changes.colorTheme !== undefined) {
|
||||
handleColorThemeChangeEvent(changes.colorTheme.newValue);
|
||||
}
|
||||
if (changes.numberDisplayRoundDown !== undefined) {
|
||||
handleNumberDisplayRoundDownChangeEvent(
|
||||
changes.numberDisplayRoundDown.newValue
|
||||
);
|
||||
}
|
||||
if (changes.numberDisplayFormat !== undefined) {
|
||||
handleNumberDisplayFormatChangeEvent(
|
||||
changes.numberDisplayFormat.newValue
|
||||
);
|
||||
}
|
||||
if (changes.numberDisplayReformatLikes !== undefined) {
|
||||
handleNumberDisplayReformatLikesChangeEvent(
|
||||
changes.numberDisplayReformatLikes.newValue
|
||||
);
|
||||
}
|
||||
},
|
||||
|
||||
async sendVote(videoId, vote) {
|
||||
api.storage.sync.get(null, async (storageResult) => {
|
||||
if (!storageResult.userId || !storageResult.registrationConfirmed) {
|
||||
await this.register();
|
||||
}
|
||||
let voteResponse = await fetch(`${apiUrl}/interact/vote`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
userId: storageResult.userId,
|
||||
videoId,
|
||||
value: vote,
|
||||
}),
|
||||
});
|
||||
|
||||
if (voteResponse.status == 401) {
|
||||
await this.register();
|
||||
await this.sendVote(videoId, vote);
|
||||
return;
|
||||
}
|
||||
const voteResponseJson = await voteResponse.json();
|
||||
const solvedPuzzle = await this.solvePuzzle(voteResponseJson);
|
||||
if (!solvedPuzzle.solution) {
|
||||
await this.sendVote(videoId, vote);
|
||||
return;
|
||||
}
|
||||
|
||||
await fetch(`${apiUrl}/interact/confirmVote`, {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
...solvedPuzzle,
|
||||
userId: storageResult.userId,
|
||||
videoId,
|
||||
}),
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
async register() {
|
||||
const userId = this.generateUserID();
|
||||
api.storage.sync.set({ userId });
|
||||
const registrationResponse = await fetch(
|
||||
`${apiUrl}/puzzle/registration?userId=${userId}`,
|
||||
{
|
||||
method: "GET",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
},
|
||||
}
|
||||
).then((response) => response.json());
|
||||
const solvedPuzzle = await this.solvePuzzle(registrationResponse);
|
||||
if (!solvedPuzzle.solution) {
|
||||
await this.register();
|
||||
return;
|
||||
}
|
||||
const result = await fetch(
|
||||
`${apiUrl}/puzzle/registration?userId=${userId}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify(solvedPuzzle),
|
||||
}
|
||||
).then((response) => response.json());
|
||||
if (result === true) {
|
||||
return api.storage.sync.set({ registrationConfirmed: true });
|
||||
}
|
||||
},
|
||||
like() {
|
||||
if (checkForSignInButton() === false) {
|
||||
if (this.storedData.previousState === "DISLIKED_STATE") {
|
||||
sendVote(1);
|
||||
if (this.storedData.dislikes > 0) this.storedData.dislikes--;
|
||||
this.storedData.likes++;
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
setDislikes(numberFormat(this.storedData.dislikes));
|
||||
this.storedData.previousState = "LIKED_STATE";
|
||||
} else if (this.storedData.previousState === "NEUTRAL_STATE") {
|
||||
sendVote(1);
|
||||
this.storedData.likes++;
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
this.storedData.previousState = "LIKED_STATE";
|
||||
} else if ((this.storedData.previousState = "LIKED_STATE")) {
|
||||
sendVote(0);
|
||||
if (this.storedData.likes > 0) this.storedData.likes--;
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
this.storedData.previousState = "NEUTRAL_STATE";
|
||||
}
|
||||
}
|
||||
},
|
||||
dislike() {
|
||||
if (checkForSignInButton() == false) {
|
||||
if (this.storedData.previousState === "NEUTRAL_STATE") {
|
||||
sendVote(-1);
|
||||
this.storedData.dislikes++;
|
||||
setDislikes(numberFormat(this.storedData.dislikes));
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
this.storedData.previousState = "DISLIKED_STATE";
|
||||
} else if (this.storedData.previousState === "DISLIKED_STATE") {
|
||||
sendVote(0);
|
||||
if (this.storedData.dislikes > 0) this.storedData.dislikes--;
|
||||
setDislikes(numberFormat(this.storedData.dislikes));
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
this.storedData.previousState = "NEUTRAL_STATE";
|
||||
} else if (this.storedData.previousState === "LIKED_STATE") {
|
||||
sendVote(-1);
|
||||
if (this.storedData.likes > 0) this.storedData.likes--;
|
||||
this.storedData.dislikes++;
|
||||
setDislikes(numberFormat(this.storedData.dislikes));
|
||||
createRateBar(this.storedData.likes, this.storedData.dislikes);
|
||||
this.storedData.previousState = "DISLIKED_STATE";
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<v-card
|
||||
style="height: 4rem !important; display: flex; box-shadow: none !important"
|
||||
class="rounded-0 pa-3 topNav transparent"
|
||||
>
|
||||
<h3 v-show="!search" class="my-auto ml-4" v-text="page" />
|
||||
<v-card style="display: flex" class="rounded-0 pa-3 topNav background">
|
||||
<h3
|
||||
v-show="!search"
|
||||
class="my-auto ml-4"
|
||||
v-text="
|
||||
$route.path.includes('channel') ? $store.state.channel.title : page
|
||||
"
|
||||
/>
|
||||
|
||||
<v-btn
|
||||
v-if="search"
|
||||
|
@ -20,8 +23,9 @@
|
|||
solo
|
||||
dense
|
||||
flat
|
||||
autofocus
|
||||
label="Search"
|
||||
style="margin-top: 1px"
|
||||
style="margin-top: 7px"
|
||||
:background-color="
|
||||
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
||||
"
|
||||
|
@ -32,7 +36,7 @@
|
|||
<v-spacer v-if="!search" />
|
||||
|
||||
<v-btn
|
||||
v-if="!search"
|
||||
v-if="!search && $route.path.includes('/home')"
|
||||
v-show="page == 'Home'"
|
||||
icon
|
||||
tile
|
||||
|
@ -43,6 +47,7 @@
|
|||
<v-icon>mdi-refresh</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="$route.name !== 'settings' && !$route.path.includes('/mods')"
|
||||
icon
|
||||
tile
|
||||
class="ml-3 my-auto fill-height"
|
||||
|
@ -76,7 +81,7 @@ export default {
|
|||
default: "Home",
|
||||
},
|
||||
},
|
||||
events: ["searchBtn", "textChanged", "closeSearch"],
|
||||
events: ["searchBtn", "textChanged", "closeSearch", "scrollToTop"],
|
||||
data: () => ({
|
||||
text: "",
|
||||
}),
|
||||
|
@ -107,10 +112,13 @@ export default {
|
|||
|
||||
<style scoped>
|
||||
.topNav {
|
||||
/* ios notch */
|
||||
top: env(safe-area-inset-top) !important;
|
||||
/* box-shadow: inset 0 1rem 10rem var(--v-background-base) !important; */
|
||||
height: calc(4rem + env(safe-area-inset-top)) !important;
|
||||
padding-top: env(safe-area-inset-top) !important;
|
||||
box-shadow: none !important;
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.topNavSearch {
|
||||
|
|
|
@ -3,21 +3,54 @@
|
|||
<topNavigation
|
||||
:search="search"
|
||||
:page="page"
|
||||
@close-search="search = !search"
|
||||
style="z-index: 696969"
|
||||
@search-btn="searchBtn"
|
||||
@text-changed="textChanged"
|
||||
@close-search="search = !search"
|
||||
@scroll-to-top="$refs.pgscroll.scrollTop = 0"
|
||||
/>
|
||||
|
||||
<div style="height: 100%; margin-top: 4rem">
|
||||
<div
|
||||
v-show="!search"
|
||||
class="scrollcontainer"
|
||||
style="overflow: hidden; height: calc(100vh - 8rem)"
|
||||
<!-- channel-tabs -->
|
||||
<v-tabs
|
||||
v-if="$route.path.includes('/channel') && !search"
|
||||
mobile-breakpoint="0"
|
||||
style="
|
||||
position: fixed;
|
||||
top: calc(4rem + env(safe-area-inset-top));
|
||||
z-index: 696969;
|
||||
"
|
||||
background-color="background"
|
||||
:color="$vuetify.theme.dark ? 'white' : 'black'"
|
||||
>
|
||||
<v-tab
|
||||
v-for="tab in channelTabs"
|
||||
:key="tab.name"
|
||||
:to="tab.to"
|
||||
exact
|
||||
:v-ripple="false"
|
||||
>
|
||||
{{ tab.name }}
|
||||
</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<div
|
||||
style="
|
||||
height: 100%;
|
||||
padding-bottom: calc(4rem + env(safe-area-inset-bottom));
|
||||
"
|
||||
:style="{
|
||||
paddingTop:
|
||||
$route.path.includes('/channel') && !search
|
||||
? 'calc(7rem + env(safe-area-inset-top))'
|
||||
: 'calc(4rem + env(safe-area-inset-top))',
|
||||
}"
|
||||
>
|
||||
<div v-show="!search">
|
||||
<!-- class="scrollcontainer" -->
|
||||
<!-- style="overflow: hidden; height: calc(100vh - 8rem)" -->
|
||||
<!-- element above removes artifacting from things like v-ripple by -->
|
||||
<!-- scrollbox below must be a standalone div -->
|
||||
<div ref="pgscroll" class="scroll-y" style="height: 100%">
|
||||
<div ref="pgscroll" style="height: 100%">
|
||||
<nuxt />
|
||||
</div>
|
||||
</div>
|
||||
|
@ -29,21 +62,17 @@
|
|||
>
|
||||
<div class="scroll-y" style="height: 100%">
|
||||
<div v-if="search" style="min-width: 180px">
|
||||
<v-list-item
|
||||
v-for="(item, index) in response"
|
||||
:key="index"
|
||||
class="px-0"
|
||||
>
|
||||
<v-list-item v-for="item in response" :key="item[0]" class="px-0">
|
||||
<v-btn
|
||||
v-emoji
|
||||
text
|
||||
tile
|
||||
dense
|
||||
class="searchButton text-left text-none"
|
||||
@click="youtubeSearch(item)"
|
||||
v-emoji
|
||||
>
|
||||
<v-icon class="mr-5">mdi-magnify</v-icon>
|
||||
{{ item[0] || item.text }}
|
||||
{{ item[0] }}
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
</div>
|
||||
|
@ -62,11 +91,20 @@ import { App as CapacitorApp } from "@capacitor/app";
|
|||
import { mapState } from "vuex";
|
||||
import constants from "~/plugins/constants";
|
||||
import { linkParser } from "~/plugins/utils";
|
||||
import backType from "~/plugins/classes/backType";
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
search: false,
|
||||
response: [],
|
||||
channelTabs: [
|
||||
{ name: "Home", to: "/channel" },
|
||||
{ name: "Videos", to: "/channel/videos" },
|
||||
{ name: "Playlists", to: "/channel/playlists" },
|
||||
{ name: "Community", to: "/channel/community" },
|
||||
{ name: "Channels", to: "/channel/channels" },
|
||||
{ name: "About", to: "/channel/about" },
|
||||
],
|
||||
}),
|
||||
|
||||
computed: {
|
||||
|
@ -93,22 +131,7 @@ export default {
|
|||
},
|
||||
|
||||
mounted() {
|
||||
//--- Back Button Listener ---//
|
||||
this.backHandler = CapacitorApp.addListener(
|
||||
"backButton",
|
||||
({ canGoBack }) => {
|
||||
//--- Back Closes Search ---//
|
||||
if (this.search) {
|
||||
this.search = false;
|
||||
|
||||
//--- Back Goes Back ---//
|
||||
} else if (!canGoBack) {
|
||||
CapacitorApp.exitApp();
|
||||
} else {
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
);
|
||||
if (!process.browser) this.$vuetube.resetBackActions();
|
||||
|
||||
// --- External URL Handling --- //
|
||||
CapacitorApp.addListener("appUrlOpen", (event) => {
|
||||
|
@ -136,28 +159,28 @@ export default {
|
|||
return;
|
||||
} // No text found, no point in calling API
|
||||
|
||||
//--- User Pastes Link, Direct Them To Video ---//
|
||||
const isLink = linkParser(text);
|
||||
if (isLink) {
|
||||
this.response = [
|
||||
{
|
||||
text: `Watch Video from ID: ${isLink.searchParams.get("v")}`,
|
||||
id: isLink.searchParams.get("v"),
|
||||
},
|
||||
];
|
||||
return;
|
||||
}
|
||||
//--- End User Pastes Link, Direct Them To Video ---//
|
||||
|
||||
//--- Auto Suggest ---//
|
||||
this.$youtube.autoComplete(text, (res) => {
|
||||
const data = res.replace(/^.*?\(/, "").replace(/\)$/, ""); //Format Response
|
||||
this.response = JSON.parse(data)[1];
|
||||
});
|
||||
|
||||
//--- User Pastes Link, Direct Them To Video ---//
|
||||
const isLink = linkParser(text);
|
||||
if (isLink) {
|
||||
this.response = [
|
||||
`Watch Video from ID: ${isLink.searchParams.get("v")}`,
|
||||
{ id: isLink.searchParams.get("v") },
|
||||
];
|
||||
return;
|
||||
}
|
||||
//--- End User Pastes Link, Direct Them To Video ---//
|
||||
},
|
||||
|
||||
youtubeSearch(item) {
|
||||
const link = item.id ? `/watch?v=${item.id}` : `/search?q=${item[0]}`;
|
||||
const link = item[1].id
|
||||
? `/watch?v=${item[1].id}` // link pasted
|
||||
: `/search?q=${item[0]}`; // regular suggestion
|
||||
this.$router.push(link);
|
||||
this.search = false;
|
||||
},
|
||||
|
@ -177,6 +200,17 @@ export default {
|
|||
}
|
||||
} else {
|
||||
this.search = true;
|
||||
|
||||
// Adds to the back stack
|
||||
const closeSearch = new backType(
|
||||
() => {
|
||||
this.search = false;
|
||||
},
|
||||
() => {
|
||||
return this.search;
|
||||
}
|
||||
);
|
||||
this.$vuetube.addBackAction(closeSearch);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -191,6 +225,39 @@ export default {
|
|||
*:focus::before {
|
||||
opacity: 0 !important;
|
||||
}
|
||||
.v-slide-group__prev {
|
||||
display: none !important;
|
||||
}
|
||||
.v-slide-group__next {
|
||||
display: none !important;
|
||||
}
|
||||
.v-input--selection-controls__input {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
.v-input__slot {
|
||||
margin: 0 !important;
|
||||
}
|
||||
.v-slider {
|
||||
margin: 0 !important;
|
||||
}
|
||||
|
||||
.border-primary {
|
||||
border: 2px solid var(--v-primary-base) !important;
|
||||
}
|
||||
.glassy {
|
||||
backdrop-filter: blur(20px);
|
||||
-webkit-backdrop-filter: blur(20px);
|
||||
}
|
||||
.debug {
|
||||
outline: 1px solid red;
|
||||
}
|
||||
|
||||
.v-card--reveal {
|
||||
bottom: 0;
|
||||
opacity: 1 !important;
|
||||
position: absolute !important;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.scrollcontainer {
|
||||
overflow: hidden;
|
||||
|
@ -208,7 +275,9 @@ export default {
|
|||
html,
|
||||
body {
|
||||
background: var(--v-background-base);
|
||||
/* overflow-x: hidden; */
|
||||
-webkit-overflow-scrolling: touch !important;
|
||||
overflow-y: scroll !important;
|
||||
overflow-x: hidden !important;
|
||||
}
|
||||
|
||||
p,
|
||||
|
@ -231,6 +300,14 @@ div {
|
|||
vertical-align: -0.1em;
|
||||
margin: 0 2px;
|
||||
}
|
||||
|
||||
.min-height-0 {
|
||||
min-height: 0 !important;
|
||||
}
|
||||
|
||||
.fill-width {
|
||||
width: 100% !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
<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>
|
23
NUXT/pages/channel/about.vue
Normal file
23
NUXT/pages/channel/about.vue
Normal file
|
@ -0,0 +1,23 @@
|
|||
<template>
|
||||
<div class="px-6 py-3">
|
||||
<h3 class="my-2">Description</h3>
|
||||
<p>{{ $store.state.channel.descriptionPreview }}</p>
|
||||
<br />
|
||||
<br />
|
||||
<h3 class="my-2">Links</h3>
|
||||
<div v-ripple class="py-2 d-flex flex-row">
|
||||
<v-avatar tile size="20" color="primary"> </v-avatar>
|
||||
<span class="ml-4 primary--text">Social Media</span>
|
||||
</div>
|
||||
<br />
|
||||
<h3 class="my-2">More Info</h3>
|
||||
<div v-ripple class="py-2 d-flex flex-row">
|
||||
<v-icon size="24" color="primary">mdi-web</v-icon>
|
||||
<span class="ml-4">https://www.youtube.com/c/todo</span>
|
||||
</div>
|
||||
<div v-ripple class="py-2 d-flex flex-row">
|
||||
<v-icon size="24" color="primary">mdi-earth</v-icon>
|
||||
<span class="ml-4">United States</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
21
NUXT/pages/channel/channels.vue
Normal file
21
NUXT/pages/channel/channels.vue
Normal file
|
@ -0,0 +1,21 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-list-item
|
||||
v-for="(channel, index) in $store.state.channel.featuredChannels"
|
||||
:key="index"
|
||||
class="pa-0 min-height-0"
|
||||
>
|
||||
<compact-channel-renderer
|
||||
:video="channel.gridChannelRenderer"
|
||||
></compact-channel-renderer>
|
||||
</v-list-item>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import compactChannelRenderer from "../../components/CompactRenderers/compactChannelRenderer.vue";
|
||||
|
||||
export default {
|
||||
components: { compactChannelRenderer },
|
||||
};
|
||||
</script>
|
22
NUXT/pages/channel/community.vue
Normal file
22
NUXT/pages/channel/community.vue
Normal file
|
@ -0,0 +1,22 @@
|
|||
<template>
|
||||
<div>
|
||||
<community-card />
|
||||
<div
|
||||
v-if="
|
||||
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0)
|
||||
"
|
||||
class="separator-bottom background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
style="height: 4px"
|
||||
></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import communityCard from "../../components/communityCard.vue";
|
||||
export default {
|
||||
components: {
|
||||
communityCard,
|
||||
},
|
||||
};
|
||||
</script>
|
45
NUXT/pages/channel/index.vue
Normal file
45
NUXT/pages/channel/index.vue
Normal file
|
@ -0,0 +1,45 @@
|
|||
<template>
|
||||
<div class="d-flex flex-column align-center">
|
||||
<v-img
|
||||
height="120"
|
||||
:src="$store.state.channel.banner"
|
||||
class="background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
></v-img>
|
||||
<v-avatar
|
||||
size="60"
|
||||
class="mt-2 background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
>
|
||||
<img
|
||||
v-if="!$store.state.channel.loading"
|
||||
:src="$store.state.channel.avatar"
|
||||
/>
|
||||
<v-progress-circular v-else indeterminate color="primary" size="60" />
|
||||
</v-avatar>
|
||||
<h2 class="mt-2">{{ $store.state.channel.title }}</h2>
|
||||
<v-btn :aria-label="subscribeAlt" class="py-2" text color="primary">
|
||||
{{ $store.state.channel.subscribe }}
|
||||
</v-btn>
|
||||
<div v-if="!$store.state.channel.loading" style="font-size: 0.75rem">
|
||||
{{ $store.state.channel.subscribers }} ·
|
||||
{{ $store.state.channel.videosCount }}
|
||||
</div>
|
||||
<v-card
|
||||
v-if="!$store.state.channel.loading"
|
||||
flat
|
||||
to="/channel/about"
|
||||
style="font-size: 0.75rem; text-oveflow: ellipsis; overflow: hidden"
|
||||
class="background background--text text-center px-4"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
{{ $store.state.channel.descriptionPreview }}
|
||||
<v-icon
|
||||
class="background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
mdi-chevron-right
|
||||
</v-icon>
|
||||
</v-card>
|
||||
</div>
|
||||
</template>
|
14
NUXT/pages/channel/playlists.vue
Normal file
14
NUXT/pages/channel/playlists.vue
Normal file
|
@ -0,0 +1,14 @@
|
|||
<template>
|
||||
<div>
|
||||
<playlist-card />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import playlistCard from "../../components/playlistCard.vue";
|
||||
export default {
|
||||
components: {
|
||||
playlistCard,
|
||||
},
|
||||
};
|
||||
</script>
|
12
NUXT/pages/channel/videos.vue
Normal file
12
NUXT/pages/channel/videos.vue
Normal file
|
@ -0,0 +1,12 @@
|
|||
<template>
|
||||
<div>
|
||||
<!-- <compact-video-renderer :video="$store.state.channel.videoExample" /> -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import CompactVideoRenderer from "../../components/CompactRenderers/compactVideoRenderer.vue";
|
||||
export default {
|
||||
components: { CompactVideoRenderer },
|
||||
};
|
||||
</script>
|
|
@ -22,22 +22,20 @@ import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
|||
import Observer from "~/components/observer.vue";
|
||||
export default {
|
||||
components: { horizontalListRenderer, VidLoadRenderer, Observer },
|
||||
data: () => ({
|
||||
loading: false,
|
||||
}),
|
||||
|
||||
computed: {
|
||||
recommends: {
|
||||
get() {
|
||||
return this.$store.state.recommendedVideos;
|
||||
return [...this.$store.state.recommendedVideos];
|
||||
},
|
||||
set(val) {
|
||||
this.$store.commit("updateRecommendedVideos", val);
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
data: () => ({
|
||||
loading: false,
|
||||
}),
|
||||
|
||||
methods: {
|
||||
paginate() {
|
||||
this.loading = true;
|
||||
|
|
|
@ -21,7 +21,7 @@ export default {
|
|||
progressMsg: "...",
|
||||
}),
|
||||
async mounted() {
|
||||
this.progressMsg = this.$lang('index').connecting;
|
||||
this.progressMsg = this.$lang("index").connecting;
|
||||
|
||||
this.$store.commit("tweaks/initTweaks");
|
||||
const theming = new Promise((resolve) =>
|
||||
|
@ -41,6 +41,7 @@ export default {
|
|||
if (localStorage.getItem("backgroundLight") != null)
|
||||
this.$vuetify.theme.themes.light.background =
|
||||
localStorage.getItem("backgroundLight");
|
||||
|
||||
this.$vuetube.navigationBar.setTheme(
|
||||
this.$vuetify.theme.currentTheme.background,
|
||||
!this.$vuetify.theme.dark
|
||||
|
@ -49,13 +50,17 @@ export default {
|
|||
this.$vuetify.theme.currentTheme.background,
|
||||
this.$vuetify.theme.dark
|
||||
);
|
||||
|
||||
// this.$vuetube.navigationBar.setTransparent();
|
||||
// this.$vuetube.statusBar.setTransparent();
|
||||
resolve();
|
||||
}, 0)
|
||||
);
|
||||
|
||||
await theming;
|
||||
await this.$youtube.getAPI();
|
||||
this.progressMsg = this.$lang('index').launching;
|
||||
await this.$vuetube.launchBackHandling();
|
||||
this.progressMsg = this.$lang("index").launching;
|
||||
|
||||
this.$router.replace(`/${localStorage.getItem("startPage") || "home"}`); // Prevent user from navigating back to the splash screen
|
||||
},
|
||||
|
@ -74,7 +79,7 @@ export default {
|
|||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
transition-property: opacity, transform;
|
||||
animation: bounce 0.66s ease infinite alternate;
|
||||
animation: bounce 0.66s 0.5s ease 1 forwards;
|
||||
}
|
||||
/* triangles aren't very good at spinning :c */
|
||||
@keyframes bounce {
|
||||
|
|
|
@ -1,25 +1,18 @@
|
|||
<template>
|
||||
<center class="px-4">
|
||||
<v-img
|
||||
contain
|
||||
style="margin-top: 5em; max-width: 80%; max-height: 15em"
|
||||
src="/dev.svg"
|
||||
/>
|
||||
<h2
|
||||
class="background--text mt-4"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
<div>
|
||||
<h4
|
||||
class="ml-7 mb-2 background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
|
||||
>
|
||||
Page Under Construction
|
||||
</h2>
|
||||
<p
|
||||
class="background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
Please read the VueTube FAQ for more information.
|
||||
</p>
|
||||
</center>
|
||||
Local Playlists
|
||||
</h4>
|
||||
<playlist-card />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
import playlistCard from "../components/playlistCard.vue";
|
||||
export default {
|
||||
components: { playlistCard },
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,20 +1,27 @@
|
|||
<template>
|
||||
<div style="padding: 1em">
|
||||
<div style="padding: 1rem">
|
||||
<center>
|
||||
<div class="row pa-4" style="flex-direction: column">
|
||||
<div>
|
||||
<v-img
|
||||
src="/icon.svg"
|
||||
width="100px"
|
||||
:class="$vuetify.theme.dark ? '' : 'invert'"
|
||||
/>
|
||||
<div class="d-flex flex-column justify-center pa-4">
|
||||
<div>
|
||||
<v-img
|
||||
width="69px"
|
||||
src="/icon.svg"
|
||||
:class="$vuetify.theme.dark ? '' : 'invert'"
|
||||
/>
|
||||
</div>
|
||||
<h1 style="font-size: 2rem">VueTube</h1>
|
||||
</div>
|
||||
<h1 class="pageTitle mb-3">VueTube</h1>
|
||||
</div>
|
||||
</center>
|
||||
|
||||
<!-- App Information -->
|
||||
<v-card class="obj" :class="$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}">
|
||||
<v-card
|
||||
flat
|
||||
class="obj"
|
||||
:class="
|
||||
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
||||
"
|
||||
:style="{ borderRadius: `${roundTweak / 2}rem` }"
|
||||
>
|
||||
<v-card-title>{{ languagePack.mods.about.appinformation }}</v-card-title>
|
||||
<v-card-text>
|
||||
<h3>{{ languagePack.mods.about.appversion }}</h3>
|
||||
|
@ -24,17 +31,28 @@
|
|||
<!-- End App Information -->
|
||||
|
||||
<!-- Device Information -->
|
||||
<v-card class="obj" :class="$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}">
|
||||
<v-card-title>{{ languagePack.mods.about.deviceinformation }}</v-card-title>
|
||||
<v-card
|
||||
flat
|
||||
class="obj"
|
||||
:class="
|
||||
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
||||
"
|
||||
:style="{ borderRadius: `${roundTweak / 2}rem` }"
|
||||
>
|
||||
<v-card-title>{{
|
||||
languagePack.mods.about.deviceinformation
|
||||
}}</v-card-title>
|
||||
<v-card-text>
|
||||
<h3>{{ languagePack.mods.about.platform }}</h3>
|
||||
{{ deviceInfo.platform || "Unknown" }}<br>
|
||||
{{ deviceInfo.platform || "Unknown" }}<br />
|
||||
<h3>{{ languagePack.mods.about.os }}</h3>
|
||||
{{ deviceInfo.operatingSystem || "Unknown" }} ({{ deviceInfo.osVersion || "Unknown" }})<br>
|
||||
{{ deviceInfo.operatingSystem || "Unknown" }} ({{
|
||||
deviceInfo.osVersion || "Unknown"
|
||||
}})<br />
|
||||
<h3>{{ languagePack.mods.about.model }}</h3>
|
||||
{{ deviceInfo.model || "Unknown" }}<br>
|
||||
{{ deviceInfo.model || "Unknown" }}<br />
|
||||
<h3>{{ languagePack.mods.about.manufacturer }}</h3>
|
||||
{{ deviceInfo.manufacturer || "Unknown" }}<br>
|
||||
{{ deviceInfo.manufacturer || "Unknown" }}<br />
|
||||
<h3>{{ languagePack.mods.about.emulator }}</h3>
|
||||
{{ deviceInfo.isVirtual ? "yes" : "no" }}
|
||||
</v-card-text>
|
||||
|
@ -42,17 +60,32 @@
|
|||
<!-- End Device Information -->
|
||||
|
||||
<!-- App Links --->
|
||||
<div class="obj">
|
||||
<v-btn @click="openExternal('https://github.com/Frontesque/VueTube')" class="action" :class="$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}">
|
||||
<div class="obj d-flex flex-row gap-1 full-width">
|
||||
<v-btn
|
||||
depressed
|
||||
class="action flex-grow-1"
|
||||
:class="
|
||||
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
||||
"
|
||||
:style="{ borderRadius: `${roundTweak / 2}rem` }"
|
||||
@click="openExternal('https://github.com/Frontesque/VueTube')"
|
||||
>
|
||||
<v-icon x-large class="actionIcon">mdi-github</v-icon>
|
||||
{{ languagePack.mods.about.github }}
|
||||
</v-btn>
|
||||
<v-btn @click="openExternal('https://discord.gg/7P8KJrdd5W')" class="action obj" :class="$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}">
|
||||
<v-btn
|
||||
depressed
|
||||
class="action flex-grow-1 ml-4"
|
||||
:class="
|
||||
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
|
||||
"
|
||||
:style="{ borderRadius: `${roundTweak / 2}rem` }"
|
||||
@click="openExternal('https://discord.gg/7P8KJrdd5W')"
|
||||
>
|
||||
<v-icon x-large class="actionIcon">mdi-discord</v-icon>
|
||||
{{ languagePack.mods.about.discord }}
|
||||
</v-btn>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -61,20 +94,18 @@ import { Browser } from "@capacitor/browser";
|
|||
import { Device } from "@capacitor/device";
|
||||
|
||||
export default {
|
||||
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
version: process.env.appVersion,
|
||||
deviceInfo: "",
|
||||
languagePack: {mods: {about: {}}},
|
||||
languagePack: { mods: { about: {} } },
|
||||
};
|
||||
},
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
},
|
||||
},
|
||||
|
||||
async mounted() {
|
||||
const info = await Device.getInfo();
|
||||
|
@ -91,16 +122,12 @@ export default {
|
|||
</script>
|
||||
|
||||
<style scoped>
|
||||
.pageTitle {
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
.obj {
|
||||
margin-top: 1em;
|
||||
}
|
||||
.action {
|
||||
min-width: 100% !important;
|
||||
min-height: 5em;
|
||||
padding: 0 !important;
|
||||
padding: 1rem;
|
||||
}
|
||||
.actionIcon {
|
||||
margin-right: 0.5em;
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
class="card background"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
flat
|
||||
:style="{borderRadius: `${roundTweak / 2}rem`}"
|
||||
:style="{ borderRadius: `${roundTweak / 2}rem` }"
|
||||
>
|
||||
<v-card-title>
|
||||
<v-chip v-if="item.error" outlined class="errorChip" color="error"
|
||||
|
|
|
@ -1,7 +1,13 @@
|
|||
<template>
|
||||
<client-only>
|
||||
<div class="d-flex flex-column justify-end" style="min-height: 100%">
|
||||
<!-- ----------------------------------------------Background Colors------------------------ -->
|
||||
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_(ツ)_/¯ -->
|
||||
<div
|
||||
class="d-flex flex-column justify-end"
|
||||
style="
|
||||
min-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) !important;
|
||||
"
|
||||
>
|
||||
<!-- ----Background Colors---- -->
|
||||
<v-radio-group v-model="$vuetify.theme.currentTheme.background">
|
||||
<div
|
||||
class="d-flex flex-row px-6 no-wrap"
|
||||
|
@ -40,16 +46,28 @@
|
|||
? '2px solid var(--v-primary-darken4)'
|
||||
: '2px solid var(--v-primary-lighten4)',
|
||||
}"
|
||||
class="py-4 px-4 ma-2 rounded-lg"
|
||||
:value="
|
||||
$vuetify.theme.dark ? experimentalDark : experimentalLight
|
||||
"
|
||||
class="pa-4 ma-2 rounded-lg"
|
||||
:value="$vuetify.theme.dark ? adaptiveDark : adaptiveLight"
|
||||
/>
|
||||
Adaptive
|
||||
</div>
|
||||
<div class="text-center">
|
||||
<!-- Custom Background -->
|
||||
<v-btn
|
||||
icon
|
||||
class="ma-2 rounded-lg background border-primary"
|
||||
style="height: 3.75rem; width: 3.75rem"
|
||||
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
|
||||
@click="(pickerState = true), (pickerMode = 'background')"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
<br />
|
||||
Custom
|
||||
</div>
|
||||
</div>
|
||||
</v-radio-group>
|
||||
<!-- ----------------------------------------------Primary Colors------------------------ -->
|
||||
<!-- ----Primary Colors---- -->
|
||||
<v-radio-group v-model="$vuetify.theme.currentTheme.primary" class="mx-2">
|
||||
<div
|
||||
class="d-flex flex-row px-6 py-2 no-wrap align-center"
|
||||
|
@ -71,40 +89,53 @@
|
|||
class="mr-2 my-auto rounded-xl"
|
||||
:value="color"
|
||||
/>
|
||||
<v-dialog
|
||||
v-model="dialog"
|
||||
width="300"
|
||||
content-class="background rounded-lg"
|
||||
<!-- Custom Primary -->
|
||||
<v-btn
|
||||
icon
|
||||
class="background"
|
||||
style="height: 1.75rem; width: 1.75rem"
|
||||
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
@click="(pickerState = true), (pickerMode = 'primary')"
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
icon
|
||||
class="background"
|
||||
style="height: 1.75rem; width: 1.75rem"
|
||||
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-color-picker
|
||||
v-model="$vuetify.theme.currentTheme.primary"
|
||||
style="min-width: 100%"
|
||||
class="background"
|
||||
hide-mode-switch
|
||||
dot-size="50"
|
||||
mode="hexa"
|
||||
flat
|
||||
/>
|
||||
</v-dialog>
|
||||
<v-icon>mdi-plus</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
</v-radio-group>
|
||||
<!-- ----------------------------------------------Mode Switch------------------------ -->
|
||||
<!-- ----Color Picker---- -->
|
||||
<v-dialog
|
||||
v-model="pickerState"
|
||||
width="300"
|
||||
content-class="background rounded-lg"
|
||||
>
|
||||
<v-color-picker
|
||||
v-model="$vuetify.theme.currentTheme[pickerMode]"
|
||||
style="min-width: 100%"
|
||||
class="background"
|
||||
hide-mode-switch
|
||||
dot-size="50"
|
||||
mode="hexa"
|
||||
flat
|
||||
/>
|
||||
</v-dialog>
|
||||
<!-- ----Mode Switch---- -->
|
||||
<v-divider v-if="!$store.state.tweaks.roundTweak" />
|
||||
<v-card
|
||||
flat
|
||||
class="d-flex flex-row justify-between mx-8 mb-4 px-4 background rounded-lg"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
class="d-flex flex-row justify-between mx-8 mb-8 px-4 py-3 background"
|
||||
:class="
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'lighten-1'
|
||||
: 'darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
|
||||
padding: !$store.state.tweaks.roundTweak ? '2rem !important' : '',
|
||||
margin: !$store.state.tweaks.roundTweak ? '0 !important' : '',
|
||||
}"
|
||||
@click="
|
||||
($vuetify.theme.dark = !$vuetify.theme.dark),
|
||||
$vuetube.haptics.hapticsImpactLight(1)
|
||||
|
@ -124,6 +155,7 @@
|
|||
<v-switch
|
||||
v-model="$vuetify.theme.dark"
|
||||
style="pointer-events: none"
|
||||
class="mt-2"
|
||||
persistent-hint
|
||||
inset
|
||||
/>
|
||||
|
@ -150,9 +182,10 @@ export default {
|
|||
{ name: "Black", color: "#000000" },
|
||||
],
|
||||
backgroundsLight: [{ name: "Normal", color: "#ffffff" }],
|
||||
experimentalLight: "",
|
||||
experimentalDark: "",
|
||||
dialog: false,
|
||||
adaptiveLight: "",
|
||||
adaptiveDark: "",
|
||||
pickerState: false,
|
||||
pickerMode: "bg",
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
|
@ -166,19 +199,30 @@ export default {
|
|||
: localStorage.setItem("backgroundLight", value);
|
||||
this.$vuetube.statusBar.setTheme(value, this.$vuetify.theme.dark);
|
||||
this.$vuetube.navigationBar.setTheme(value, !this.$vuetify.theme.dark);
|
||||
// WIP: luma-based light-dark auto-switching
|
||||
// let bg = this.$vuetify.theme.currentTheme.background;
|
||||
// console.log(this.$vuetube.hexToRgb(bg));
|
||||
// let luma =
|
||||
// 0.2126 * this.$vuetube.hexToRgb(bg).r +
|
||||
// 0.7152 * this.$vuetube.hexToRgb(bg).g +
|
||||
// 0.0722 * this.$vuetube.hexToRgb(bg).b;
|
||||
// if (luma < 40) {
|
||||
// this.$vuetify.theme.dark = true;
|
||||
// this.vuetify.theme.currentTheme.background = bg;
|
||||
// }
|
||||
},
|
||||
"$vuetify.theme.currentTheme.primary"(value) {
|
||||
if (value != undefined) {
|
||||
this.$vuetify.theme.dark
|
||||
? localStorage.setItem("primaryDark", value)
|
||||
: localStorage.setItem("primaryLight", value);
|
||||
let tempD = this.experimentalDark;
|
||||
let tempL = this.experimentalLight;
|
||||
let tempD = this.adaptiveDark;
|
||||
let tempL = this.adaptiveLight;
|
||||
this.adapt();
|
||||
if (this.$vuetify.theme.currentTheme.background === tempD)
|
||||
this.$vuetify.theme.currentTheme.background = this.experimentalDark;
|
||||
this.$vuetify.theme.currentTheme.background = this.adaptiveDark;
|
||||
if (this.$vuetify.theme.currentTheme.background === tempL)
|
||||
this.$vuetify.theme.currentTheme.background = this.experimentalLight;
|
||||
this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
|
||||
}
|
||||
},
|
||||
},
|
||||
|
@ -195,30 +239,21 @@ export default {
|
|||
);
|
||||
// the menace above returns a hex string with A SPACE " " in front of it, that's why substring(1)
|
||||
// the SPACE " " is stored as part of the CSS variable itself to be used for chaining
|
||||
this.experimentalDark = hexD.substring(1).toUpperCase();
|
||||
this.experimentalLight = hexL.substring(1).toUpperCase();
|
||||
this.adaptiveDark = hexD.substring(1).toUpperCase();
|
||||
this.adaptiveLight = hexL.substring(1).toUpperCase();
|
||||
setTimeout(() => {
|
||||
if (
|
||||
this.$vuetify.theme.currentTheme.background ==
|
||||
hexD.substring(1).toUpperCase()
|
||||
)
|
||||
this.$vuetify.theme.currentTheme.background = this.experimentalDark;
|
||||
this.$vuetify.theme.currentTheme.background = this.adaptiveDark;
|
||||
if (
|
||||
this.$vuetify.theme.currentTheme.background ==
|
||||
hexL.substring(1).toUpperCase()
|
||||
)
|
||||
this.$vuetify.theme.currentTheme.background = this.experimentalLight;
|
||||
this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
|
||||
}, 0);
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
.border-primary {
|
||||
border: 2px solid var(--v-primary-base) !important;
|
||||
}
|
||||
.v-input--selection-controls__input {
|
||||
margin-right: 0 !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,8 +1,78 @@
|
|||
<template>
|
||||
<div class="d-flex flex-column justify-end" style="min-height: 100%">
|
||||
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_(ツ)_/¯ -->
|
||||
<div
|
||||
class="d-flex flex-column justify-end"
|
||||
style="
|
||||
min-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) !important;
|
||||
"
|
||||
>
|
||||
<!-- TODO: outer radius -->
|
||||
<!-- TODO: Dense Navbar -->
|
||||
<!-- TODO: Disable Top Bar -->
|
||||
<!-- TODO: Top and Bottom bar color selection -->
|
||||
<v-card
|
||||
flat
|
||||
class="mb-4 background"
|
||||
class="mx-4 my-2 px-4 py-2 d-flex flex-row justify-between background"
|
||||
style="transition-duration: 0.3s; transition-property: border-radius"
|
||||
:class="
|
||||
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
}"
|
||||
>
|
||||
<h3
|
||||
class="my-auto background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
|
||||
>
|
||||
Fullscreen (Soon™)
|
||||
</h3>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
disabled
|
||||
class="mt-2"
|
||||
style="pointer-events: none"
|
||||
persistent-hint
|
||||
inset
|
||||
/>
|
||||
</v-card>
|
||||
|
||||
<v-divider v-if="!roundTweak" />
|
||||
|
||||
<v-card
|
||||
flat
|
||||
class="mx-4 my-2 px-4 py-2 d-flex flex-row justify-between background"
|
||||
style="transition-duration: 0.3s; transition-property: border-radius"
|
||||
:class="
|
||||
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
}"
|
||||
>
|
||||
<h3
|
||||
class="my-auto background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
|
||||
>
|
||||
Navbar Blur (Soon™)
|
||||
</h3>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
disabled
|
||||
class="mt-2"
|
||||
style="pointer-events: none"
|
||||
persistent-hint
|
||||
inset
|
||||
/>
|
||||
</v-card>
|
||||
|
||||
<v-divider v-if="!roundTweak" />
|
||||
|
||||
<h3 class="ml-8 mt-8">Rounded Corners</h3>
|
||||
|
||||
<v-card
|
||||
flat
|
||||
class="mx-4 my-2 background"
|
||||
style="
|
||||
transition-duration: 0.3s;
|
||||
transition-property: border-radius;
|
||||
|
@ -10,55 +80,78 @@
|
|||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0',
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="item in list"
|
||||
:key="item"
|
||||
@click="list.pop(item)"
|
||||
class="pa-4 mb-1 background text-center rounded-sm"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
<!-- margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0', -->
|
||||
<v-card
|
||||
flat
|
||||
class="mb-1 px-4 py-2 d-flex flex-row justify-between background"
|
||||
:class="
|
||||
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 12}rem`,
|
||||
}"
|
||||
@click="
|
||||
(roundThumb = !roundThumb), $vuetube.haptics.hapticsImpactLight(1)
|
||||
"
|
||||
>
|
||||
{{ item }}
|
||||
</div>
|
||||
<v-card-title
|
||||
v-ripple
|
||||
class="pa-4 background primary--text text--lighten-2 rounded-sm"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
@click="list.push('x')"
|
||||
<div
|
||||
class="my-auto background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
Round Thumbnails
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
v-model="roundThumb"
|
||||
style="pointer-events: none"
|
||||
persistent-hint
|
||||
class="mt-2"
|
||||
inset
|
||||
/>
|
||||
</v-card>
|
||||
<v-card
|
||||
flat
|
||||
class="mb-1 px-4 py-2 d-flex flex-row justify-between background"
|
||||
:class="
|
||||
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 12}rem`,
|
||||
}"
|
||||
@click="
|
||||
(roundWatch = !roundWatch), $vuetube.haptics.hapticsImpactLight(1)
|
||||
"
|
||||
>
|
||||
+++++++++++++++++++++++++++++
|
||||
</v-card-title>
|
||||
</v-card>
|
||||
<v-card
|
||||
flat
|
||||
class="px-6 background"
|
||||
style="transition-duration: 0.3s; transition-property: border-radius"
|
||||
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 2}rem`,
|
||||
margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0',
|
||||
}"
|
||||
>
|
||||
<h3 class="mt-5">Rounded Corners</h3>
|
||||
<div
|
||||
class="background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
applies to only a few elements for now
|
||||
</div>
|
||||
<!-- TODO: outer radius -->
|
||||
<!-- TODO: Dense Navbar -->
|
||||
<!-- TODO: Disable Top Bar -->
|
||||
<!-- TODO: Top and Bottom bar color selection -->
|
||||
<div
|
||||
class="my-auto background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
>
|
||||
Round Watch Page Components
|
||||
</div>
|
||||
<v-spacer />
|
||||
<v-switch
|
||||
v-model="roundWatch"
|
||||
style="pointer-events: none"
|
||||
persistent-hint
|
||||
class="mt-2"
|
||||
inset
|
||||
/>
|
||||
</v-card>
|
||||
<v-slider
|
||||
v-model="roundTweak"
|
||||
class="mr-2 mt-5"
|
||||
label="Inner"
|
||||
class="pr-4 pl-4 pt-4 pb-1 background"
|
||||
:max="4"
|
||||
step="1"
|
||||
label="Radius"
|
||||
step=".25"
|
||||
thumb-size="64"
|
||||
:class="
|
||||
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: `${roundTweak / 12}rem`,
|
||||
}"
|
||||
@input="$vuetube.haptics.hapticsImpactLight(0)"
|
||||
>
|
||||
<template #thumb-label="{ value }">
|
||||
|
@ -74,9 +167,6 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
data: () => ({
|
||||
list: ["x", "x"],
|
||||
}),
|
||||
computed: {
|
||||
roundTweak: {
|
||||
get() {
|
||||
|
@ -86,6 +176,22 @@ export default {
|
|||
this.$store.commit("tweaks/setRoundTweak", value);
|
||||
},
|
||||
},
|
||||
roundThumb: {
|
||||
get() {
|
||||
return this.$store.state.tweaks.roundThumb;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("tweaks/setRoundThumb", value);
|
||||
},
|
||||
},
|
||||
roundWatch: {
|
||||
get() {
|
||||
return this.$store.state.tweaks.roundWatch;
|
||||
},
|
||||
set(value) {
|
||||
this.$store.commit("tweaks/setRoundWatch", value);
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div class="py-2">
|
||||
<div>
|
||||
<v-list-item v-for="(item, index) in commits" :key="index" class="my-1">
|
||||
<v-card
|
||||
flat
|
||||
|
|
|
@ -1,124 +1,168 @@
|
|||
<template>
|
||||
<div style="padding-top: 1em">
|
||||
<v-list-item v-for="(item, index) in settingsItems" :key="index">
|
||||
<v-btn text class="entry text-left text-capitalize" :to="item.to" :disabled="item.disabled">
|
||||
<div>
|
||||
<v-list-item
|
||||
v-for="(item, index) in settingsItems"
|
||||
:key="index"
|
||||
:style="{
|
||||
padding:
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? '0 1rem !important'
|
||||
: '0rem !important',
|
||||
}"
|
||||
>
|
||||
<v-btn
|
||||
text
|
||||
class="entry text-left text-capitalize"
|
||||
:to="item.to"
|
||||
:disabled="item.disabled"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
|
||||
paddingLeft:
|
||||
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
|
||||
}"
|
||||
>
|
||||
<v-icon size="30px" class="icon" v-text="item.icon" />
|
||||
{{ item.name }}
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
<v-list-item
|
||||
:style="{
|
||||
padding:
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? '0 1rem !important'
|
||||
: '0rem !important',
|
||||
}"
|
||||
>
|
||||
<!-- Dev Mode Open -->
|
||||
<v-btn
|
||||
v-if="!devmode"
|
||||
text
|
||||
class="entry"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
|
||||
paddingLeft:
|
||||
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
|
||||
}"
|
||||
@click="dev()"
|
||||
/>
|
||||
|
||||
<!-- Dev Mode Open -->
|
||||
<v-btn text class="entry" @click="dev()" v-if="!devmode" />
|
||||
|
||||
<v-btn text class="entry text-left text-capitalize" style="margin: 0 0.75em 0 0.75em;" to="/mods/developer" v-if="devmode">
|
||||
<v-icon size="30px" class="icon" >mdi-database-edit</v-icon>
|
||||
{{ devmodebuttonname }}
|
||||
</v-btn>
|
||||
<v-btn
|
||||
v-if="devmode"
|
||||
text
|
||||
class="entry text-left text-capitalize"
|
||||
to="/mods/developer"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
|
||||
paddingLeft:
|
||||
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
|
||||
}"
|
||||
>
|
||||
<v-icon size="30px" class="icon">mdi-database-edit</v-icon>
|
||||
{{ devmodebuttonname }}
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
<!-- End Dev Mode -->
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
justify-content: left !important;
|
||||
padding: 1.5em 0.5em 1.5em 0.5em !important;
|
||||
}
|
||||
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
devClicks: 0,
|
||||
devmode: false,
|
||||
export default {
|
||||
data() {
|
||||
return {
|
||||
devClicks: 0,
|
||||
devmode: false,
|
||||
|
||||
devmodebuttonname: "Developer Mode",
|
||||
devmodebuttonname: "Developer Mode",
|
||||
|
||||
settingsItems: [{
|
||||
name: "General",
|
||||
icon: "mdi-cog",
|
||||
to: "",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
name: "Theme",
|
||||
icon: "mdi-brush-variant",
|
||||
to: "/mods/theme"
|
||||
},
|
||||
{
|
||||
name: "Player",
|
||||
icon: "mdi-motion-play-outline",
|
||||
to: "",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "UI Tweaker",
|
||||
icon: "mdi-television-guide",
|
||||
to: "/mods/tweaks",
|
||||
},
|
||||
{
|
||||
name: "Startup Options",
|
||||
icon: "mdi-restart",
|
||||
to: "/mods/startup"
|
||||
},
|
||||
{
|
||||
name: "Plugins",
|
||||
icon: "mdi-puzzle",
|
||||
to: "",
|
||||
to: "/mods/plugins",
|
||||
disabled: true
|
||||
},
|
||||
{
|
||||
name: "Updates",
|
||||
icon: "mdi-cloud-download-outline",
|
||||
to: "/mods/updates",
|
||||
},
|
||||
{
|
||||
name: "Logs",
|
||||
icon: "mdi-text-box-outline",
|
||||
to: "/mods/logs"
|
||||
},
|
||||
{
|
||||
name: "About",
|
||||
icon: "mdi-information-outline",
|
||||
to: "/mods/about"
|
||||
},
|
||||
],
|
||||
};
|
||||
settingsItems: [
|
||||
{
|
||||
name: "General",
|
||||
icon: "mdi-cog",
|
||||
to: "",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Theme",
|
||||
icon: "mdi-brush-variant",
|
||||
to: "/mods/theme",
|
||||
},
|
||||
{
|
||||
name: "Player",
|
||||
icon: "mdi-motion-play-outline",
|
||||
to: "",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "UI Tweaker",
|
||||
icon: "mdi-television-guide",
|
||||
to: "/mods/tweaks",
|
||||
},
|
||||
{
|
||||
name: "Startup Options",
|
||||
icon: "mdi-restart",
|
||||
to: "/mods/startup",
|
||||
},
|
||||
{
|
||||
name: "Plugins",
|
||||
icon: "mdi-puzzle",
|
||||
to: "",
|
||||
to: "/mods/plugins",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Updates",
|
||||
icon: "mdi-cloud-download-outline",
|
||||
to: "/mods/updates",
|
||||
},
|
||||
{
|
||||
name: "Logs",
|
||||
icon: "mdi-text-box-outline",
|
||||
to: "/mods/logs",
|
||||
},
|
||||
{
|
||||
name: "About",
|
||||
icon: "mdi-information-outline",
|
||||
to: "/mods/about",
|
||||
},
|
||||
],
|
||||
};
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.settingsItems[0].name = this.$lang("settings").general;
|
||||
this.settingsItems[1].name = this.$lang("settings").theme;
|
||||
this.settingsItems[2].name = this.$lang("settings").player;
|
||||
this.settingsItems[3].name = this.$lang("settings").uitweaker;
|
||||
this.settingsItems[4].name = this.$lang("settings").startupoptions;
|
||||
this.settingsItems[5].name = this.$lang("settings").plugins;
|
||||
this.settingsItems[6].name = this.$lang("settings").updates;
|
||||
this.settingsItems[7].name = this.$lang("settings").logs;
|
||||
this.settingsItems[8].name = this.$lang("settings").about;
|
||||
this.devmodebuttonname = this.$lang("settings").devmode;
|
||||
|
||||
this.devmode = localStorage.getItem("devmode");
|
||||
},
|
||||
|
||||
methods: {
|
||||
dev() {
|
||||
this.devClicks++;
|
||||
if (this.devClicks >= 6) {
|
||||
localStorage.setItem("devmode", "true");
|
||||
this.devmode = true;
|
||||
}
|
||||
},
|
||||
|
||||
mounted() {
|
||||
this.settingsItems[0].name = this.$lang('settings').general;
|
||||
this.settingsItems[1].name = this.$lang('settings').theme;
|
||||
this.settingsItems[2].name = this.$lang('settings').player;
|
||||
this.settingsItems[3].name = this.$lang('settings').uitweaker;
|
||||
this.settingsItems[4].name = this.$lang('settings').startupoptions;
|
||||
this.settingsItems[5].name = this.$lang('settings').plugins;
|
||||
this.settingsItems[6].name = this.$lang('settings').updates;
|
||||
this.settingsItems[7].name = this.$lang('settings').logs;
|
||||
this.settingsItems[8].name = this.$lang('settings').about;
|
||||
this.devmodebuttonname = this.$lang('settings').devmode;
|
||||
|
||||
this.devmode = localStorage.getItem('devmode');
|
||||
},
|
||||
|
||||
methods: {
|
||||
dev() {
|
||||
this.devClicks++;
|
||||
if (this.devClicks >= 6) {
|
||||
localStorage.setItem('devmode', 'true');
|
||||
this.devmode = true;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.entry {
|
||||
width: 100%;
|
||||
font-size: 1.2em;
|
||||
justify-content: left !important;
|
||||
padding: 1.5em 0.5em 1.5em 0.5em !important;
|
||||
}
|
||||
.icon {
|
||||
margin-right: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
<template>
|
||||
<div class="background" id="watch-body">
|
||||
<div id="player-container">
|
||||
<v-btn text style="position: fixed; z-index: 69420" to="home">
|
||||
<v-icon>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
<!-- VueTube Player V1 -->
|
||||
<vuetubePlayer
|
||||
:sources="sources"
|
||||
v-if="useBetaPlayer === 'true' && sources.length > 0"
|
||||
:sources="sources"
|
||||
/>
|
||||
|
||||
<!-- Stock Player -->
|
||||
<legacyPlayer
|
||||
v-if="useBetaPlayer !== 'true'"
|
||||
id="player"
|
||||
ref="player"
|
||||
v-touch="{ down: () => $router.push('/home') }"
|
||||
class="background"
|
||||
:vid-src="vidSrc"
|
||||
v-if="useBetaPlayer !== 'true'"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-bind:class="{
|
||||
id="content-container"
|
||||
:class="{
|
||||
'overflow-y-auto': !showComments,
|
||||
'overflow-y-hidden': showComments,
|
||||
}"
|
||||
id="content-container"
|
||||
>
|
||||
<v-card v-if="loaded" class="ml-2 mr-2 background rounded-0" flat>
|
||||
<v-card v-if="loaded" class="background rounded-0" flat>
|
||||
<div
|
||||
v-ripple
|
||||
class="d-flex justify-space-between align-start px-3 pt-3"
|
||||
|
@ -48,7 +44,7 @@
|
|||
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
|
||||
"
|
||||
>
|
||||
<div style="margin-bottom: 1rem">
|
||||
<div>
|
||||
<template
|
||||
v-for="text in video.metadata.contents.find(
|
||||
(content) => content.slimVideoInformationRenderer
|
||||
|
@ -61,13 +57,13 @@
|
|||
<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">
|
||||
<div class="d-flex pl-2">
|
||||
<v-btn
|
||||
v-for="(item, index) in interactions"
|
||||
:key="index"
|
||||
text
|
||||
fab
|
||||
class="vertical-button ma-1"
|
||||
class="vertical-button mx-1"
|
||||
elevation="0"
|
||||
style="width: 4.2rem !important; height: 4.2rem !important"
|
||||
:disabled="item.disabled"
|
||||
|
@ -108,13 +104,37 @@
|
|||
</v-sheet>
|
||||
</v-bottom-sheet> -->
|
||||
</v-card>
|
||||
<v-divider />
|
||||
|
||||
<v-divider
|
||||
v-if="
|
||||
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- Channel Bar -->
|
||||
<div class="channel-container" v-if="loaded">
|
||||
<div v-if="loaded">
|
||||
<v-card
|
||||
class="channel-section background px-3 rounded-0"
|
||||
:to="video.channelUrl"
|
||||
flat
|
||||
class="channel-section py-2 px-3 background"
|
||||
:class="
|
||||
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'background lighten-1'
|
||||
: 'background darken-1'
|
||||
: ''
|
||||
"
|
||||
to="/channel"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundWatch
|
||||
? `${$store.state.tweaks.roundTweak / 2}rem`
|
||||
: '0',
|
||||
margin:
|
||||
$store.state.tweaks.roundWatch &&
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? '1rem'
|
||||
: '0',
|
||||
}"
|
||||
@click="$store.dispatch('channel/fetchChannel', video.channelUrl)"
|
||||
>
|
||||
<div id="details">
|
||||
<div class="avatar-link mr-3">
|
||||
|
@ -131,16 +151,18 @@
|
|||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="channel-buttons"
|
||||
style="color: rgb(204, 0, 0); text-transform: uppercase"
|
||||
>
|
||||
<div class="primary--text" style="text-transform: uppercase">
|
||||
subscribe
|
||||
</div>
|
||||
</v-card>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
||||
<v-divider
|
||||
v-if="
|
||||
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- Description -->
|
||||
<div v-if="showMore">
|
||||
<div class="scroll-y ma-4">
|
||||
|
@ -148,29 +170,59 @@
|
|||
:render="video.renderedData.description"
|
||||
/>
|
||||
</div>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
||||
<v-divider
|
||||
v-if="
|
||||
showMore &&
|
||||
(!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch)
|
||||
"
|
||||
/>
|
||||
|
||||
<!-- Comments -->
|
||||
<div
|
||||
v-if="loaded && video.commentData"
|
||||
@click="showComments = !showComments"
|
||||
>
|
||||
<v-card flat class="background comment-renderer">
|
||||
<v-text class="comment-count keep-spaces">
|
||||
<div v-if="loaded && video.commentData" @click="toggleComment">
|
||||
<v-card
|
||||
v-ripple
|
||||
flat
|
||||
tile
|
||||
class="comment-renderer px-3 background"
|
||||
:class="
|
||||
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
|
||||
? $vuetify.theme.dark
|
||||
? 'background lighten-1'
|
||||
: 'background darken-1'
|
||||
: ''
|
||||
"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundWatch
|
||||
? `${$store.state.tweaks.roundTweak / 2}rem !important`
|
||||
: '0',
|
||||
margin:
|
||||
$store.state.tweaks.roundWatch &&
|
||||
$store.state.tweaks.roundTweak > 0
|
||||
? '1rem'
|
||||
: '0',
|
||||
}"
|
||||
>
|
||||
<v-card-text class="comment-count keep-spaces px-0">
|
||||
<template v-for="text in video.commentData.headerText.runs">
|
||||
<template v-if="text.bold">
|
||||
<strong :key="text.text">{{ text.text }}</strong>
|
||||
</template>
|
||||
<template v-else>{{ text.text }}</template>
|
||||
</template>
|
||||
</v-text>
|
||||
<v-icon v-if="showComments">mdi-unfold-less-horizontal</v-icon>
|
||||
<v-icon v-else>mdi-unfold-more-horizontal</v-icon>
|
||||
</v-card-text>
|
||||
<v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon>
|
||||
<v-icon v-else dense>mdi-unfold-more-horizontal</v-icon>
|
||||
</v-card>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
||||
<v-divider
|
||||
v-if="
|
||||
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
|
||||
"
|
||||
/>
|
||||
|
||||
<swipeable-bottom-sheet
|
||||
v-model="showComments"
|
||||
hide-overlay
|
||||
|
@ -192,13 +244,19 @@
|
|||
></swipeable-bottom-sheet> -->
|
||||
|
||||
<!-- Related Videos -->
|
||||
<div class="loaders" v-if="!loaded">
|
||||
<div v-if="!loaded">
|
||||
<v-skeleton-loader
|
||||
type="list-item-two-line, actions, divider, list-item-avatar, divider, list-item-three-line"
|
||||
/>
|
||||
<vid-load-renderer :count="5" />
|
||||
</div>
|
||||
<item-section-renderer v-else :render="recommends" />
|
||||
<item-section-renderer
|
||||
v-else
|
||||
:render="recommends"
|
||||
:style="{
|
||||
marginTop: $store.state.tweaks.roundTweak > 0 ? '1rem' : '0',
|
||||
}"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -209,13 +267,13 @@ import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
|||
import { getCpn } from "~/plugins/utils";
|
||||
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
|
||||
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
|
||||
import legacyPlayer from "~/components/Player/legacy.vue"
|
||||
import legacyPlayer from "~/components/Player/legacy.vue";
|
||||
import vuetubePlayer from "~/components/Player/index.vue";
|
||||
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
||||
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
|
||||
import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
|
||||
|
||||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import backType from "~/plugins/classes/backType";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
|
@ -259,29 +317,11 @@ export default {
|
|||
|
||||
mounted() {
|
||||
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();
|
||||
}
|
||||
}
|
||||
);
|
||||
this.$vuetube.resetBackActions();
|
||||
},
|
||||
|
||||
beforeDestroy() {
|
||||
clearInterval(this.interval);
|
||||
if (this.backHandler) this.backHandler.remove();
|
||||
},
|
||||
|
||||
methods: {
|
||||
|
@ -330,7 +370,6 @@ export default {
|
|||
// using item.action in the v-for loop
|
||||
this[name]();
|
||||
},
|
||||
dislike() {},
|
||||
async share() {
|
||||
// this.share = !this.share;
|
||||
await Share.share({
|
||||
|
@ -382,7 +421,8 @@ export default {
|
|||
{
|
||||
name: "Likes",
|
||||
icon: "mdi-thumb-up-outline",
|
||||
// action: null,
|
||||
// action: this.like(),
|
||||
actionName: "like",
|
||||
value: this.likes,
|
||||
disabled: true,
|
||||
},
|
||||
|
@ -401,6 +441,24 @@ export default {
|
|||
actionName: "share",
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
name: "Save",
|
||||
icon: "mdi-plus-box-multiple-outline",
|
||||
actionName: "enqueue",
|
||||
disabled: true,
|
||||
},
|
||||
// {
|
||||
// name: "Quality",
|
||||
// icon: "mdi-high-definition",
|
||||
// actionName: "quality",
|
||||
// disabled: false,
|
||||
// },
|
||||
// {
|
||||
// name: "Speed",
|
||||
// icon: "mdi-speedometer",
|
||||
// actionName: "speed",
|
||||
// disabled: false,
|
||||
// },
|
||||
],
|
||||
showMore: false,
|
||||
showComments: false,
|
||||
|
@ -412,6 +470,7 @@ export default {
|
|||
interval: null,
|
||||
video: null,
|
||||
useBetaPlayer: false,
|
||||
backHierarchy: [],
|
||||
};
|
||||
},
|
||||
|
||||
|
@ -426,6 +485,22 @@ export default {
|
|||
scrollable.scrollTo(0, 0);
|
||||
});
|
||||
},
|
||||
|
||||
// Toggle this.showComments to true or false. If it is true, then add the dismiss function to backStack.
|
||||
toggleComment() {
|
||||
this.showComments = !this.showComments;
|
||||
if (this.showComments) {
|
||||
const dismissComment = new backType(
|
||||
() => {
|
||||
this.showComments = false;
|
||||
},
|
||||
() => {
|
||||
return this.showComments;
|
||||
}
|
||||
);
|
||||
this.$vuetube.addBackAction(dismissComment);
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
@ -453,7 +528,6 @@ export default {
|
|||
.comment-renderer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.channel-section #details,
|
||||
|
|
66
NUXT/plugins/classes/backHander.js
Normal file
66
NUXT/plugins/classes/backHander.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import { Capacitor } from "@capacitor/core";
|
||||
import backType from "./backType";
|
||||
export default class backHandler {
|
||||
constructor() {
|
||||
this.backStack = []; // This should only contain instances of backType. Any other type will be ignored.
|
||||
if (Capacitor.getPlatform() != "web") this.startUp(); // Only run on native platforms.
|
||||
}
|
||||
|
||||
startUp() {
|
||||
this.reset();
|
||||
|
||||
// Add a listener for the back button.
|
||||
this.backHandler = CapacitorApp.addListener("backButton", (context) => {
|
||||
this.back(context);
|
||||
});
|
||||
|
||||
// Start garbage collection. Run every 5 minutes.
|
||||
setInterval(() => {
|
||||
() => {
|
||||
this.garbageCollect;
|
||||
};
|
||||
}, 5 * 60 * 1000);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.backStack = [];
|
||||
}
|
||||
|
||||
back({ canGoBack }) {
|
||||
console.log("backStack", this.backStack);
|
||||
// Check if backStack contains any backType objects. If so, call the goBack() function.
|
||||
if (this.backStack.length > 0) {
|
||||
// Loop through the backStack array.
|
||||
let lastResult = false;
|
||||
while (!lastResult && this.backStack.length > 0) {
|
||||
const backAction = this.backStack.pop();
|
||||
lastResult = backAction.goBack();
|
||||
}
|
||||
// Since a function was successfully called, no need to continue.
|
||||
if (lastResult) return;
|
||||
}
|
||||
if (!canGoBack) {
|
||||
// If we can't go back, then we should exit the app.
|
||||
CapacitorApp.exitApp();
|
||||
} else {
|
||||
// If we can go back, then we should go back.
|
||||
window.history.back();
|
||||
}
|
||||
}
|
||||
|
||||
addAction(callback) {
|
||||
if (callback instanceof backType) {
|
||||
this.backStack.push(callback);
|
||||
} else {
|
||||
throw new TypeError("backType object expected");
|
||||
}
|
||||
}
|
||||
|
||||
// Loops through the backStack array if array larger than 10. If backType.check() returns false, then remove it from the backStack array.
|
||||
garbageCollect() {
|
||||
if (this.backStack.length > 10) {
|
||||
this.backStack = this.backStack.filter((backType) => backType.check());
|
||||
}
|
||||
}
|
||||
}
|
16
NUXT/plugins/classes/backType.js
Normal file
16
NUXT/plugins/classes/backType.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
// This is the class that populates the backStack array in the backHandler class.
|
||||
export default class backType {
|
||||
constructor(callback, check) {
|
||||
this.callback = callback;
|
||||
this.check = check || (() => true);
|
||||
}
|
||||
|
||||
goBack() {
|
||||
if (this.check()) {
|
||||
this.callback();
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -33,6 +33,7 @@ module.exports = {
|
|||
recommendations: "Recommendations",
|
||||
init: "Initialize",
|
||||
innertube: "Innertube",
|
||||
channel: "Channel",
|
||||
},
|
||||
|
||||
INNERTUBE_HEADER: (info) => {
|
||||
|
|
|
@ -76,18 +76,23 @@ class Innertube {
|
|||
|
||||
//--- API Calls ---//
|
||||
|
||||
async browseAsync(action_type) {
|
||||
async browseAsync(action_type, args = {}) {
|
||||
let data = { context: this.context };
|
||||
|
||||
switch (action_type) {
|
||||
case "recommendations":
|
||||
data.browseId = "FEwhat_to_watch";
|
||||
args.browseId = "FEwhat_to_watch";
|
||||
break;
|
||||
case "playlist":
|
||||
data.browseId = args.browse_id;
|
||||
break;
|
||||
case "channel":
|
||||
if (args && args.browseId) {
|
||||
break;
|
||||
} else {
|
||||
throw new ReferenceError("No browseId provided");
|
||||
}
|
||||
default:
|
||||
}
|
||||
data = { ...data, ...args };
|
||||
|
||||
console.log(data);
|
||||
|
||||
|
@ -217,6 +222,28 @@ class Innertube {
|
|||
};
|
||||
}
|
||||
|
||||
async getEndPoint(url) {
|
||||
let data = { context: this.context, url: url };
|
||||
const response = await Http.post({
|
||||
url: `${constants.URLS.YT_BASE_API}/navigation/resolve_url?key=${this.key}`,
|
||||
data: data,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}).catch((error) => error);
|
||||
|
||||
if (response instanceof Error)
|
||||
return {
|
||||
success: false,
|
||||
status_code: response.status,
|
||||
message: response.message,
|
||||
};
|
||||
|
||||
return {
|
||||
success: true,
|
||||
status_code: response.status,
|
||||
data: response.data,
|
||||
};
|
||||
}
|
||||
|
||||
// WARNING: This is tracking the user's activity, but is required for recommendations to properly work
|
||||
async apiStats(params, url) {
|
||||
console.log(params);
|
||||
|
@ -252,10 +279,24 @@ class Innertube {
|
|||
// Simple Wrappers
|
||||
async getRecommendationsAsync() {
|
||||
const rec = await this.browseAsync("recommendations");
|
||||
console.log(rec.data);
|
||||
return rec;
|
||||
}
|
||||
|
||||
async getChannelAsync(url) {
|
||||
const channelEndpoint = await this.getEndPoint(url);
|
||||
if (
|
||||
channelEndpoint.success &&
|
||||
channelEndpoint.data.endpoint?.browseEndpoint
|
||||
) {
|
||||
return await this.browseAsync(
|
||||
"channel",
|
||||
channelEndpoint.data.endpoint?.browseEndpoint
|
||||
);
|
||||
} else {
|
||||
throw new ReferenceError("Cannot find channel");
|
||||
}
|
||||
}
|
||||
|
||||
async VidInfoAsync(id) {
|
||||
let response = await this.getVidAsync(id);
|
||||
|
||||
|
@ -264,11 +305,9 @@ class Innertube {
|
|||
response.data.output?.playabilityStatus?.status == ("ERROR" || undefined)
|
||||
)
|
||||
throw new Error(
|
||||
`Could not get information for video: ${
|
||||
response.status_code ||
|
||||
response.data.output?.playabilityStatus?.status
|
||||
} - ${
|
||||
response.message || response.data.output?.playabilityStatus?.reason
|
||||
`Could not get information for video: ${response.status_code ||
|
||||
response.data.output?.playabilityStatus?.status
|
||||
} - ${response.message || response.data.output?.playabilityStatus?.reason
|
||||
}`
|
||||
);
|
||||
const responseInfo = response.data.output;
|
||||
|
@ -298,7 +337,9 @@ class Innertube {
|
|||
isLive: details.isLiveContent,
|
||||
channelName: details.author,
|
||||
channelSubs: ownerData?.collapsedSubtitle?.runs[0]?.text,
|
||||
channelUrl: rendererUtils.getNavigationEndpoints(ownerData),
|
||||
channelUrl: rendererUtils.getNavigationEndpoints(
|
||||
ownerData.navigationEndpoint
|
||||
),
|
||||
channelImg: ownerData?.thumbnail?.thumbnails[0].url,
|
||||
availableResolutions: resolutions?.formats,
|
||||
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
|
||||
|
|
|
@ -1,25 +1,21 @@
|
|||
// General utility functions for the renderers
|
||||
class rendererUtils {
|
||||
static getNavigationEndpoints(base) {
|
||||
const navEndpoint = base.navigationEndpoint;
|
||||
if (!navEndpoint) return;
|
||||
if (navEndpoint.urlEndpoint) {
|
||||
const params = new Proxy(
|
||||
new URLSearchParams(navEndpoint.urlEndpoint.url),
|
||||
{
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
}
|
||||
);
|
||||
if (!base) return;
|
||||
if (base.urlEndpoint) {
|
||||
const params = new Proxy(new URLSearchParams(base.urlEndpoint.url), {
|
||||
get: (searchParams, prop) => searchParams.get(prop),
|
||||
});
|
||||
if (params.q) return decodeURI(params.q);
|
||||
else return new URL(navEndpoint.urlEndpoint.url).pathname;
|
||||
} else if (navEndpoint.browseEndpoint) {
|
||||
return navEndpoint.browseEndpoint.canonicalBaseUrl;
|
||||
} else if (navEndpoint.watchEndpoint) {
|
||||
return `/watch?v=${navEndpoint.watchEndpoint.videoId}`;
|
||||
} else if (navEndpoint.navigationEndpoint) {
|
||||
else return new URL(base.urlEndpoint.url).pathname;
|
||||
} else if (base.browseEndpoint) {
|
||||
return base.browseEndpoint.canonicalBaseUrl;
|
||||
} else if (base.watchEndpoint) {
|
||||
return `/watch?v=${base.watchEndpoint.videoId}`;
|
||||
} else if (base.navigationEndpoint) {
|
||||
return; //for now
|
||||
} else if (navEndpoint.searchEndpoint) {
|
||||
return `/search?q=${encodeURI(navEndpoint.searchEndpoint.query)}`;
|
||||
} else if (base.searchEndpoint) {
|
||||
return `/search?q=${encodeURI(base.searchEndpoint.query)}`;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,5 +85,5 @@ module.exports = {
|
|||
getMutationByKey,
|
||||
linkParser,
|
||||
delay,
|
||||
parseEmoji
|
||||
parseEmoji,
|
||||
};
|
||||
|
|
|
@ -6,6 +6,7 @@ import constants from "./constants";
|
|||
import { hexToRgb, rgbToHex, parseEmoji } from "./utils";
|
||||
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
||||
import Vue from "vue";
|
||||
import backHandler from "./classes/backHander";
|
||||
|
||||
Vue.directive("emoji", {
|
||||
inserted: function (el) {
|
||||
|
@ -14,6 +15,8 @@ Vue.directive("emoji", {
|
|||
},
|
||||
});
|
||||
|
||||
let backActions;
|
||||
|
||||
const module = {
|
||||
//--- Get GitHub Commits ---//
|
||||
commits: new Promise((resolve, reject) => {
|
||||
|
@ -110,9 +113,25 @@ const module = {
|
|||
return rgbToHex(r, g, b);
|
||||
},
|
||||
|
||||
async launchBackHandling() {
|
||||
backActions = new backHandler();
|
||||
return true;
|
||||
},
|
||||
|
||||
resetBackActions() {
|
||||
backActions.reset();
|
||||
},
|
||||
|
||||
addBackAction(action) {
|
||||
backActions.addAction(action);
|
||||
},
|
||||
|
||||
back(listenerFunc) {
|
||||
backActions.back(listenerFunc);
|
||||
},
|
||||
|
||||
//--- Convert Time To Human Readable String ---//
|
||||
humanTime(seconds=0) {
|
||||
humanTime(seconds = 0) {
|
||||
seconds = Math.floor(seconds); // Not doing this seems to break the calculation
|
||||
let levels = [
|
||||
Math.floor(seconds / 31536000), //Years
|
||||
|
@ -124,18 +143,16 @@ const module = {
|
|||
|
||||
let returntext = new String();
|
||||
for (const i in levels) {
|
||||
const num = levels[i].toString().length == 1 ? "0"+levels[i] : levels[i]; // If Number Is Single Digit, Add 0 In Front
|
||||
const num = levels[i].toString().length == 1 ? "0" + levels[i] : levels[i]; // If Number Is Single Digit, Add 0 In Front
|
||||
|
||||
returntext += ":"+num;
|
||||
returntext += ":" + num;
|
||||
}
|
||||
while (returntext.startsWith(":00")) { returntext = returntext.substring(3); } // Remove Prepending 0s (eg. 00:00:00:01:00)
|
||||
if (returntext.startsWith(":0")) { returntext = returntext.substring(2); } else { returntext = returntext.substring(1); } // Prevent Time Starting With 0 (eg. 01:00)
|
||||
console.log("Human Time:", returntext)
|
||||
// console.log("Human Time:", returntext);
|
||||
return returntext;
|
||||
}
|
||||
//--- End Convert Time To Human Readable String ---//
|
||||
|
||||
|
||||
},
|
||||
//--- End Convert Time To Human Readable String ---//
|
||||
};
|
||||
|
||||
//--- Start ---//
|
||||
|
|
|
@ -109,6 +109,15 @@ const innertubeModule = {
|
|||
else return `https://img.youtube.com/vi/${id}/mqdefault.jpg`;
|
||||
},
|
||||
|
||||
async getChannel(url) {
|
||||
try {
|
||||
const response = await InnertubeAPI.getChannelAsync(url);
|
||||
return response.data;
|
||||
} catch (error) {
|
||||
logger(constants.LOGGER_NAMES.channel, error, true);
|
||||
}
|
||||
},
|
||||
|
||||
// It just works™
|
||||
// Front page recommendation
|
||||
async recommend() {
|
||||
|
|
76
NUXT/store/channel/index.js
Normal file
76
NUXT/store/channel/index.js
Normal file
|
@ -0,0 +1,76 @@
|
|||
const getDefaultState = () => {
|
||||
return {
|
||||
loading: null,
|
||||
error: null,
|
||||
avatar: null,
|
||||
banner: null,
|
||||
title: null,
|
||||
subscribe: null,
|
||||
subscribeAlt: null,
|
||||
descriptionPreview: null,
|
||||
subscribers: null,
|
||||
videosCount: null,
|
||||
featuredChannels: null,
|
||||
videoExample: null,
|
||||
};
|
||||
};
|
||||
export const state = () => {
|
||||
return getDefaultState();
|
||||
};
|
||||
export const actions = {
|
||||
fetchChannel({ state }, channelUrl) {
|
||||
Object.assign(state, getDefaultState());
|
||||
state.loading = true;
|
||||
console.log(channelUrl);
|
||||
const channelRequest =
|
||||
channelUrl.includes("/c/") ||
|
||||
channelUrl.includes("/user/") ||
|
||||
channelUrl.includes("/channel/")
|
||||
? `https://youtube.com/${channelUrl}`
|
||||
: `https://youtube.com/channel/${channelUrl}`;
|
||||
this.$youtube
|
||||
.getChannel(channelRequest)
|
||||
.then((channel) => {
|
||||
// console.log(channel);
|
||||
state.loading = false;
|
||||
state.banner =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelBanner?.image.sources[0].url;
|
||||
state.avatar =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.avatarData.avatar?.image.sources[0].url;
|
||||
state.title =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.title;
|
||||
state.subscribe =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.subscribeButton.subscribeButtonContent.buttonText;
|
||||
state.subscribeAlt =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.subscribeButton.subscribeButtonContent.accessibilityText;
|
||||
state.descriptionPreview =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.descriptionPreview.description;
|
||||
state.subscribers =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.metadata.subscriberCountText;
|
||||
state.videosCount =
|
||||
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.metadata.videosCountText;
|
||||
const featuredSection =
|
||||
channel.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents.find(
|
||||
(i) => {
|
||||
return !!i?.shelfRenderer?.content?.horizontalListRenderer
|
||||
?.items[0].gridChannelRenderer;
|
||||
}
|
||||
);
|
||||
state.featuredChannels =
|
||||
featuredSection.shelfRenderer.content.horizontalListRenderer.items;
|
||||
console.log(
|
||||
channel.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer
|
||||
.content.sectionListRenderer.contents[0].shelfRenderer.content
|
||||
.verticalListRenderer.items[0].elementRenderer.newElement.type
|
||||
.componentType.model
|
||||
);
|
||||
state.videoExample =
|
||||
channel.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents[0].shelfRenderer.content.verticalListRenderer.items[0].elementRenderer.newElement.type.componentType.model.videoWithContextModel.videoWithContextData.videoData;
|
||||
})
|
||||
.catch((err) => {
|
||||
state.loading = false;
|
||||
state.error = err;
|
||||
console.error(err);
|
||||
});
|
||||
},
|
||||
};
|
|
@ -1,12 +1,18 @@
|
|||
export const state = () => ({
|
||||
roundTweak: 0,
|
||||
roundThumb: null,
|
||||
roundWatch: null,
|
||||
});
|
||||
export const mutations = {
|
||||
initTweaks(state) {
|
||||
// NOTE: localStorage is not reactive, so it will only be used on first load
|
||||
// currently called beforeCreate() in pages/default.vue
|
||||
// currently called on mounted() in pages/index.vue
|
||||
if (process.client) {
|
||||
state.roundTweak = localStorage.getItem("roundTweak") || 0;
|
||||
state.roundThumb =
|
||||
JSON.parse(localStorage.getItem("roundThumb")) === true;
|
||||
state.roundWatch =
|
||||
JSON.parse(localStorage.getItem("roundWatch")) === true;
|
||||
}
|
||||
},
|
||||
setRoundTweak(state, payload) {
|
||||
|
@ -15,4 +21,12 @@ export const mutations = {
|
|||
localStorage.setItem("roundTweak", payload);
|
||||
}
|
||||
},
|
||||
setRoundThumb(state, payload) {
|
||||
state.roundThumb = payload;
|
||||
localStorage.setItem("roundThumb", payload);
|
||||
},
|
||||
setRoundWatch(state, payload) {
|
||||
state.roundWatch = payload;
|
||||
localStorage.setItem("roundWatch", payload);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -12,6 +12,6 @@
|
|||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-03-31T22:44:20.197073Z" />
|
||||
<timeTargetWasSelectedWithDropDown value="2022-05-14T02:50:17.689302Z" />
|
||||
</component>
|
||||
</project>
|
|
@ -27,6 +27,7 @@
|
|||
504EC3111FED79650016851F /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = "<group>"; };
|
||||
504EC3131FED79650016851F /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
|
||||
50B271D01FEDC1A000F3C39B /* public */ = {isa = PBXFileReference; lastKnownFileType = folder; path = public; sourceTree = "<group>"; };
|
||||
892EF9F42809CD8A00F0F89E /* App.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = App.entitlements; sourceTree = "<group>"; };
|
||||
AF277DCFFFF123FFC6DF26C7 /* Pods_App.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_App.framework; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||
AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.release.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.release.xcconfig"; sourceTree = "<group>"; };
|
||||
FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-App.debug.xcconfig"; path = "Pods/Target Support Files/Pods-App/Pods-App.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
|
@ -73,6 +74,7 @@
|
|||
504EC3061FED79650016851F /* App */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
892EF9F42809CD8A00F0F89E /* App.entitlements */,
|
||||
50379B222058CBB4000EE86E /* capacitor.config.json */,
|
||||
504EC3071FED79650016851F /* AppDelegate.swift */,
|
||||
504EC30B1FED79650016851F /* Main.storyboard */,
|
||||
|
@ -345,17 +347,20 @@
|
|||
baseConfigurationReference = FC68EB0AF532CFC21C3344DD /* Pods-App.debug.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VRCJ7YWR89;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.Frontesque.vuetube;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -364,16 +369,19 @@
|
|||
baseConfigurationReference = AF51FD2D460BCFE21FA515B2 /* Pods-App.release.xcconfig */;
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
CODE_SIGN_ENTITLEMENTS = App/App.entitlements;
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
DEVELOPMENT_TEAM = VRCJ7YWR89;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.Frontesque.vuetube;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
10
ios/App/App/App.entitlements
Normal file
10
ios/App/App/App.entitlements
Normal file
|
@ -0,0 +1,10 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>com.apple.security.app-sandbox</key>
|
||||
<true/>
|
||||
<key>com.apple.security.network.client</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
|
@ -9,17 +9,17 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunityHttp', :path => '..\..\node_modules\@capacitor-community\http'
|
||||
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||
pod 'CapacitorBrowser', :path => '..\..\node_modules\@capacitor\browser'
|
||||
pod 'CapacitorDevice', :path => '..\..\node_modules\@capacitor\device'
|
||||
pod 'CapacitorFilesystem', :path => '..\..\node_modules\@capacitor\filesystem'
|
||||
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||
pod 'CapacitorShare', :path => '..\..\node_modules\@capacitor\share'
|
||||
pod 'CapacitorSplashScreen', :path => '..\..\node_modules\@capacitor\splash-screen'
|
||||
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||
pod 'CapacitorToast', :path => '..\..\node_modules\@capacitor\toast'
|
||||
pod 'HugotomaziCapacitorNavigationBar', :path => '..\..\node_modules\@hugotomazi\capacitor-navigation-bar'
|
||||
pod 'CapacitorCommunityHttp', :path => '../../node_modules/@capacitor-community/http'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
pod 'CapacitorShare', :path => '../../node_modules/@capacitor/share'
|
||||
pod 'CapacitorSplashScreen', :path => '../../node_modules/@capacitor/splash-screen'
|
||||
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
|
||||
pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast'
|
||||
pod 'HugotomaziCapacitorNavigationBar', :path => '../../node_modules/@hugotomazi/capacitor-navigation-bar'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
|
|
@ -26,6 +26,7 @@ Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
|
|||
<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://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a>
|
||||
</p>
|
||||
|
||||
## Features
|
||||
|
||||
|
@ -76,3 +77,9 @@ Please read our website on how to do so: www.vuetube.app/contributing
|
|||
|
||||
- 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)
|
||||
|
||||
## Disclamer
|
||||
|
||||
The VueTube project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way accociated with YouTube, Google LLC or any of its affiliates and subsidaries. The official YouTube website can be found at [www.youtube.com](https://www.youtube.com).
|
||||
|
||||
Any trademark, service mark, trade name, or other intellectual property rights used in the VueTube project are owned by the respective owners.
|
||||
|
|
Loading…
Reference in a new issue