0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2025-01-03 14:11:08 +00:00

Merge pull request #278 from VueTubeApp/dev

Dev
This commit is contained in:
Kenny 2022-05-25 09:50:31 -04:00 committed by GitHub
commit 125dfc85d0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
56 changed files with 2333 additions and 784 deletions

View file

@ -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"
>
&middot;
<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 {

View file

@ -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>

View file

@ -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>

View 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>

View file

@ -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>

View file

@ -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]]"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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() {

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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
>

View file

@ -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>

View file

@ -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"

View file

@ -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;

View 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>

View file

@ -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>

View 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
View 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>

View file

@ -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 {

View file

@ -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>

View file

@ -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>

View 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>

View 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>

View 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>

View 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 }} &middot;
{{ $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>

View file

@ -0,0 +1,14 @@
<template>
<div>
<playlist-card />
</div>
</template>
<script>
import playlistCard from "../../components/playlistCard.vue";
export default {
components: {
playlistCard,
},
};
</script>

View 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>

View file

@ -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;

View file

@ -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 {

View file

@ -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>

View file

@ -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;

View file

@ -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"

View file

@ -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>

View file

@ -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>

View file

@ -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

View file

@ -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>

View file

@ -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,

View 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());
}
}
}

View 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;
}
}
}

View file

@ -33,6 +33,7 @@ module.exports = {
recommendations: "Recommendations",
init: "Initialize",
innertube: "Innertube",
channel: "Channel",
},
INNERTUBE_HEADER: (info) => {

View file

@ -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,

View file

@ -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)}`;
}
}

View file

@ -85,5 +85,5 @@ module.exports = {
getMutationByKey,
linkParser,
delay,
parseEmoji
parseEmoji,
};

View file

@ -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 ---//

View file

@ -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() {

View 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);
});
},
};

View file

@ -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);
},
};

View file

@ -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>

View file

@ -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;
};

View 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>

View file

@ -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

View file

@ -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.