0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-25 12:45:17 +00:00
This commit is contained in:
Alex 2022-05-30 17:59:05 +12:00
commit 884eb2ac90
93 changed files with 2548 additions and 845 deletions

81
.github/ISSUE_TEMPLATE/bug-report.yml vendored Normal file
View file

@ -0,0 +1,81 @@
name: 🐞 Issue Report
description: Report a issue in VueTube
labels: [bug]
body:
- type: textarea
id: reproduce-steps
attributes:
label: Steps to reproduce
description: Provide an example of the issue.
placeholder: |
Example:
1. First step
2. Second step
3. Issue here
validations:
required: true
- type: textarea
id: expected-behavior
attributes:
label: Expected behavior
placeholder: |
Example:
"This should happen..."
validations:
required: true
- type: textarea
id: actual-behavior
attributes:
label: Actual behavior
placeholder: |
Example:
"This happened instead..."
validations:
required: true
- type: input
id: vuetube-version
attributes:
label: VueTube version
description: |
You can find your VueTube version in **Settings**.
placeholder: |
Example: "1.0"
validations:
required: true
- type: input
id: android-version
attributes:
label: Android version
description: |
You can find this somewhere in your Android settings.
placeholder: |
Example: "Android 12"
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I have updated the app to unstable version **[Latest](https://vuetube.app/install/)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -1,31 +0,0 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: ''
assignees: ''
---
**Describe the bug**
A clear and concise description of what the bug is.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
4. See error
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Device Information**
- OS: [e.g. iOS 15, Android 12]
- App Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View file

@ -0,0 +1,35 @@
name: ⭐ Feature request
description: Suggest a feature to improve the app
labels: [feature request]
body:
- type: textarea
id: feature-description
attributes:
label: Describe your suggested feature
description: How can an existing source be improved?
placeholder: |
Example:
"It should work like this..."
validations:
required: true
- type: textarea
id: other-details
attributes:
label: Other details
placeholder: |
Additional details and attachments.
- type: checkboxes
id: acknowledgements
attributes:
label: Acknowledgements
description: Your issue will be closed if you haven't done these steps.
options:
- label: I have searched the existing issues and this is a new ticket, **NOT** a duplicate or related to another open issue.
required: true
- label: I have written a short but informative title.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View file

@ -1,20 +0,0 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: ''
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.6 KiB

View file

@ -1 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100%" height="100%" version="1.1" viewBox="0 0 512 512" xml:space="preserve" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"><rect width="512" height="512" x="0" y="0" style="fill:url(#_Linear1)"/><g><circle cx="256" cy="256" r="200" style="fill:#fff;fill-opacity:.1"/></g><path d="M318.761,269.869c10.695,-6.153 10.695,-21.584 0,-27.737l-38.665,-22.246l39.873,-18.593l16.745,9.634l54.237,31.205c10.695,6.153 10.695,21.584 0,27.737l-54.219,31.193l-0.018,0.011l-18.071,10.397l-39.873,-18.593l39.991,-23.008Zm-121.761,24.599l-0,17.796c0.005,12.299 13.308,19.994 23.971,13.866l0.008,-0.004l19.658,-11.31l39.872,18.593l-41.577,23.921l-0.028,0.016l-53.925,31.025c-10.667,6.137 -23.979,-1.563 -23.979,-13.869l-0,-96.821l36,16.787Zm84.835,-115.114l-39.873,18.593l-20.983,-12.073l-0.013,-0.007c-10.664,-6.123 -23.966,1.575 -23.966,13.876l-0,19.17l-36,16.787l-0,-98.202c-0,-12.306 13.312,-20.006 23.979,-13.869l53.946,31.037l0.007,0.004l42.903,24.684Z" style="fill:#fff"/><defs><linearGradient id="_Linear1" x1="0" x2="1" y1="0" y2="0" gradientTransform="matrix(512,512,-512,512,0,0)" gradientUnits="userSpaceOnUse"><stop offset="0" style="stop-color:#4a8;stop-opacity:1"/><stop offset="1" style="stop-color:#345;stop-opacity:1"/></linearGradient></defs></svg> <svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="512" height="512" fill="url(#paint0_linear_6_11)"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M327.768 269.391C338.454 263.236 338.454 247.814 327.766 241.66L327.761 241.658L291.527 220.81L331.399 202.218L345.714 210.453L345.736 210.466L386.392 233.857C403.102 243.471 403.102 267.582 386.392 277.196L345.743 300.582L345.714 300.599L328.778 310.343L288.905 291.75L327.761 269.395L327.768 269.391ZM206 290.605V311.783V311.79C206.005 324.092 219.314 331.788 229.979 325.652L252.89 312.471L292.763 331.063L247.932 356.856L247.903 356.873L207.467 380.137C190.801 389.725 170 377.695 170 358.467L170 311.783L170 273.818L206 290.605ZM295.384 181.497L255.512 200.09L229.979 185.4L229.964 185.392C219.3 179.27 206 186.968 206 199.269V223.177L170 239.965L170 199.269L170 152.585C170 133.357 190.801 121.327 207.467 130.915L247.917 154.187L247.932 154.196L295.384 181.497Z" fill="url(#paint1_linear_6_11)"/>
<defs>
<linearGradient id="paint0_linear_6_11" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
<stop stop-color="#1B3245"/>
<stop offset="1" stop-color="#0C2028"/>
</linearGradient>
<linearGradient id="paint1_linear_6_11" x1="170" y1="128" x2="431" y2="389" gradientUnits="userSpaceOnUse">
<stop stop-color="#00E1C3"/>
<stop offset="1" stop-color="#00D1D5"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.3 KiB

After

Width:  |  Height:  |  Size: 1.4 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 8.7 KiB

View file

@ -1,17 +1,22 @@
<template> <template>
<div <div
v-if="commentRenderer"
v-ripple v-ripple
class="comment-thread px-3" class="comment-thread px-3"
v-if="commentRenderer"
@click="$emit('showReplies', comment)" @click="$emit('showReplies', comment)"
> >
<a <v-btn
:href=" fab
this.$rendererUtils.getNavigationEndpoints( text
commentRenderer.authorEndpoint to="/channel"
class="avatar-link mr-4"
style="height: 35px; width: 35px"
@click.prevent="
$store.dispatch(
'channel/fetchChannel',
$rendererUtils.getNavigationEndpoints(commentRenderer.authorEndpoint)
) )
" "
class="avatar-link"
> >
<v-img <v-img
class="avatar-thumbnail" class="avatar-thumbnail"
@ -21,83 +26,76 @@
].url ].url
" "
/> />
</a> </v-btn>
<div class="comment-content"> <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 <div
class="author-badge-name mr-1" class="author-badge-name mr-2"
:class="{ owner: commentRenderer.authorIsChannelOwner }" :class="
commentRenderer.authorIsChannelOwner
? $vuetify.theme.dark
? 'owner primary--text background lighten-2'
: 'owner primary--text background darken-2'
: ''
"
> >
<div class="author-name--wrapper"> <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 }} {{ commentRenderer.authorText.simpleText }}
</span> </span>
</div> </div>
<template <template
v-for="(badge, index) in commentRenderer.authorCommentBadge" v-for="(badge, index) in commentRenderer.authorCommentBadge"
> >
<author-comment-badge-renderer <author-comment-badge-renderer :metadata="badge" :key="index" />
:metadata="badge"
:key="index"
class="ml-1"
/>
</template> </template>
<template <template
v-for="(badge, index) in commentRenderer.sponsorCommentBadge" v-for="(badge, index) in commentRenderer.sponsorCommentBadge"
> >
<sponsor-comment-badge-renderer <sponsor-comment-badge-renderer :metadata="badge" :key="index" />
:metadata="badge"
:key="index"
class="ml-1"
/>
</template> </template>
</div> </div>
<span &middot;
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'" <span class="comment-timestamp ml-2">
class="background--text comment-timestamp"
>
{{ commentRenderer.publishedTimeText.runs[0].text }} {{ commentRenderer.publishedTimeText.runs[0].text }}
</span> </span>
</div> </div>
<collapsable-text <collapsable-text
:lines="4" :lines="3"
:expandText=" :expand-text="
commentRenderer.expandButton.buttonRenderer.text.runs[0].text commentRenderer.expandButton.buttonRenderer.text.runs[0].text
" "
:collapseText=" :collapse-text="
commentRenderer.collapseButton.buttonRenderer.text.runs[0].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> </yt-text-formatter>
</collapsable-text> </collapsable-text>
<div class="toolbar"> <div class="toolbar mt-2">
<v-btn-toggle v-model="voteStatus" group> <v-btn-toggle v-model="voteStatus" group>
<div class="toolbar--item mr-1"> <div class="toolbar--item mr-1">
<v-btn class="toolbar--button like" disabled icon x-small plain> <v-icon small>mdi-thumb-up-outline</v-icon>
<v-icon small>mdi-thumb-up</v-icon>
</v-btn>
<span <span
v-if="commentRenderer.voteCount" v-if="commentRenderer.voteCount"
class="like-count caption"
v-text="commentRenderer.voteCount.simpleText" v-text="commentRenderer.voteCount.simpleText"
class="like-count subtitle-2"
></span> ></span>
</div> <v-icon class="ml-2" small>mdi-thumb-down-outline</v-icon>
<div class="toolbar--item">
<v-btn class="toolbar--button dislike" disabled icon x-small plain>
<v-icon small>mdi-thumb-down</v-icon>
</v-btn>
</div> </div>
</v-btn-toggle> </v-btn-toggle>
<div class="toolbar--item"> <div class="toolbar--item ml-6" v-if="commentRenderer.replyCount">
<v-btn class="toolbar--button reply ml-2" disabled icon x-small plain> <v-icon small>mdi-comment-outline</v-icon>
<v-icon small>mdi-comment</v-icon>
</v-btn>
</div>
<div class="toolbar--item" v-if="commentRenderer.replyCount">
<span <span
class="like-count caption"
v-text="commentRenderer.replyCount" v-text="commentRenderer.replyCount"
class="like-count mr-1 subtitle-2"
></span> ></span>
</div> </div>
</div> </div>
@ -118,10 +116,9 @@
padding: 10px 0; padding: 10px 0;
.avatar-thumbnail { .avatar-thumbnail {
margin-right: 0.5rem;
border-radius: 50%; border-radius: 50%;
width: 48px; width: 35px;
height: 48px; height: 35px;
} }
.comment-content { .comment-content {
@ -162,14 +159,9 @@
} }
.owner { .owner {
padding: 0 0.6em;
background-color: #888888;
color: #fff;
border-radius: 1em; border-radius: 1em;
padding: 0 0.3em 0 0.6em;
&::v-deep .author-badge { font-weight: bold;
color: #fff;
}
} }
.toolbar--button::v-deep.v-btn--active .v-btn__content { .toolbar--button::v-deep.v-btn--active .v-btn__content {

View file

@ -22,6 +22,22 @@
</div> </div>
</template> </template>
<script>
export default {
props: ["comment"],
data() {
return {
boxRenderer: null,
};
},
mounted() {
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
},
};
</script>
<style scoped> <style scoped>
.entry { .entry {
width: 100%; /* Prevent Loading Weirdness */ width: 100%; /* Prevent Loading Weirdness */
@ -43,19 +59,3 @@
width: 100%; width: 100%;
} }
</style> </style>
<script>
export default {
props: ["comment"],
data() {
return {
boxRenderer: null,
};
},
mounted() {
this.boxRenderer = this.comment?.createRenderer?.commentSimpleboxRenderer;
},
};
</script>

View file

@ -15,27 +15,28 @@
</v-btn> </v-btn>
</template> </template>
<template v-for="(comment, index) in comments"> <div
<v-list-item :key="index" class="px-0"> v-for="(comment, index) in comments"
:key="index"
class="commentElement"
>
<v-list-item class="px-0">
<component <component
v-if="getComponents()[Object.keys(comment)[0]]"
:is="Object.keys(comment)[0]" :is="Object.keys(comment)[0]"
v-if="getComponents()[Object.keys(comment)[0]]"
:comment="comment[Object.keys(comment)[0]]" :comment="comment[Object.keys(comment)[0]]"
@intersect="paginate" @intersect="paginate"
@showReplies="openReply" @showReplies="openReply"
></component> ></component>
</v-list-item> </v-list-item>
<v-divider <v-divider v-if="getComponents()[Object.keys(comment)[0]]"></v-divider>
v-if="getComponents()[Object.keys(comment)[0]]" </div>
:key="index"
></v-divider>
</template>
<div class="loading" v-if="loading"> <div class="loading" v-if="loading">
<v-sheet <v-sheet
color="background"
v-for="i in comments.length <= 0 ? 5 : 1" v-for="i in comments.length <= 0 ? 5 : 1"
:key="i" :key="i"
color="background"
> >
<v-skeleton-loader type="list-item-avatar-three-line" /> <v-skeleton-loader type="list-item-avatar-three-line" />
</v-sheet> </v-sheet>

View file

@ -15,9 +15,11 @@
<template> <template>
<comment-thread-renderer :comment="parentComment" /> <comment-thread-renderer :comment="parentComment" />
<v-divider></v-divider> <v-divider></v-divider>
<template v-for="index in 10"> <comment-thread-renderer
<comment-thread-renderer :comment="parentComment" v-bind:key="index" /> v-for="index in 10"
</template> v-bind:key="index"
:comment="parentComment"
/>
</template> </template>
</dialog-base> </dialog-base>
</template> </template>

View file

@ -1,15 +1,28 @@
<template> <template>
<v-card <v-card
class="entry gridVideoRenderer background"
:to="`/watch?v=${video.videoId}`"
flat 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 <a
:href=" :href="
this.$rendererUtils.getNavigationEndpoints(video.navigationEndpoint) this.$rendererUtils.getNavigationEndpoints(video.navigationEndpoint)
" "
class="avatar-link pt-2" class="avatar-link"
> >
<v-img <v-img
class="avatar-thumbnail" class="avatar-thumbnail"
@ -19,12 +32,11 @@
" "
/> />
</a> </a>
<v-card-text class="video-info pt-2" v-emoji> <v-card-text class="video-info py-0" v-emoji>
<div <div
v-for="title in video.title.runs" v-for="title in video.title.runs"
:key="title.text" :key="title.text"
style="margin-top: 0.5em" class="vid-title mt-1"
class="vid-title"
> >
{{ title.text }} {{ title.text }}
</div> </div>
@ -35,10 +47,43 @@
v-text="parseBottom(video)" v-text="parseBottom(video)"
/> />
</v-card-text> </v-card-text>
<v-btn
fab
text
elevation="0"
class="background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
style="width: 50px !important; height: 50px !important; z-index: 420"
>
<v-icon>mdi-share-outline</v-icon>
</v-btn>
</div> </div>
</v-card> </v-card>
</template> </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> <style scoped>
.entry { .entry {
width: 100%; /* Prevent Loading Weirdness */ width: 100%; /* Prevent Loading Weirdness */
@ -53,8 +98,6 @@
} }
.avatar-thumbnail { .avatar-thumbnail {
margin-top: 0.5rem;
margin-left: 0.5rem;
border-radius: 50%; border-radius: 50%;
width: 50px; width: 50px;
height: 50px; height: 50px;
@ -64,7 +107,6 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
flex-basis: auto; flex-basis: auto;
padding: 10px;
} }
@media screen and (orientation: landscape) { @media screen and (orientation: landscape) {
@ -76,19 +118,3 @@
} }
} }
</style> </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

@ -6,8 +6,8 @@
class="pa-0 min-height-0" class="pa-0 min-height-0"
> >
<component <component
v-if="getComponents()[Object.keys(renderer)[0]]"
:is="Object.keys(renderer)[0]" :is="Object.keys(renderer)[0]"
v-if="getComponents()[Object.keys(renderer)[0]]"
:key="index" :key="index"
:render="renderer[Object.keys(renderer)[0]]" :render="renderer[Object.keys(renderer)[0]]"
></component> ></component>

View file

@ -1,13 +1,13 @@
<template> <template>
<div> <div class="fill-width">
<v-list-item <v-list-item
v-for="(video, index) in render.items" v-for="(video, index) in render.items"
:key="index" :key="index"
class="pa-0 min-height-0" class="pa-0 min-height-0"
> >
<component <component
v-if="getComponents()[Object.keys(video)[0]]"
:is="Object.keys(video)[0]" :is="Object.keys(video)[0]"
v-if="getComponents()[Object.keys(video)[0]]"
:key="video[Object.keys(video)[0]].videoId" :key="video[Object.keys(video)[0]].videoId"
:video="video[Object.keys(video)[0]]" :video="video[Object.keys(video)[0]]"
></component> ></component>

View file

@ -0,0 +1,11 @@
<template>
<v-btn
fab
text
small
disabled
style="position: absolute; top: 0.25rem; right: 3rem"
>
<v-icon>mdi-closed-caption-outline</v-icon>
</v-btn>
</template>

View file

@ -0,0 +1,13 @@
<template>
<!-- TODO: change /home to $router.goBack() or $router.go(-1) -->
<v-btn
fab
text
small
style="position: absolute; top: 0.25rem; right: 0.25rem"
to="/home"
color="white"
>
<v-icon>mdi-close</v-icon>
</v-btn>
</template>

View file

@ -1,95 +0,0 @@
<template>
<div class="controls" @click="toggleControls()">
<div class="controlsWrap" ref="controlsWrap">
<div class="centerVideoControls">
<v-btn @click="togglePlaying()" text style="height: 5em; width: 5em;">
<v-icon size="5em" v-text="playing ? 'mdi-pause' : 'mdi-play' " ref="pausePlayIndicator" />
</v-btn>
</div>
<div class="bottomVideoControls">
{{ watched }} <span style="color: #999;">/ {{ $vuetube.humanTime(video.duration) }}</span>
</div>
</div>
</div>
</template>
<style scoped>
.centerVideoControls {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.bottomVideoControls {
position: absolute;
width: 100%;
bottom: 0;
}
.pausePlay {
min-height: 5em;
min-width: 5em;
}
.controls {
position: absolute;
width: 100%;
height: 100%;
top: 0;
}
.controlsWrap {
position: relative;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
}
</style>
<script>
export default {
props: ["video"],
data() {
return {
playing: true,
controls: true,
watched: 0,
}
},
mounted() {
this.video.ontimeupdate = () => {
console.log(this.video.currentTime)
this.watched = this.$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
},
toggleControls() {
const setControls = this.controls ? 'none' : 'block';
this.$refs.controlsWrap.style.display = setControls;
this.controls = !this.controls;
}
}
}
</script>

View file

@ -0,0 +1,24 @@
<template>
<v-btn
fab
text
small
color="white"
style="position: absolute; bottom: 0.25rem; right: 0.25rem"
@click.stop="$emit('fullscreen')"
>
<v-icon>{{ fullscreen ? "mdi-fullscreen-exit" : "mdi-fullscreen" }}</v-icon>
</v-btn>
</template>
<script>
export default {
props: {
fullscreen: {
type: Boolean,
required: true,
},
},
emits: ["fullscreen"],
};
</script>

View file

@ -1,54 +1,268 @@
<template> <template>
<div style="position: relative;"> <!-- // TODO: down: () => minimize, -->
<div
ref="vidcontainer"
v-touch="{
up: () => (contain = false),
down: () => (contain = true),
}"
class="d-flex flex-column"
style="position: relative"
:style="{
borderRadius: $store.state.tweaks.roundWatch
? `${$store.state.tweaks.roundTweak / 3}rem`
: '0',
}"
@click="controls = !controls"
>
<video <video
ref="player" ref="player"
autoplay autoplay
:src="vidSrc"
width="100%" width="100%"
style="max-height: 50vh; display: block" :height="isFullscreen ? '100%' : 'auto'"
@webkitfullscreenchange="handleFullscreenChange" :src="vidSrc"
style="transition: filter 0.15s ease-in-out"
:class="controls || seeking ? 'dim' : ''"
:style="contain ? 'object-fit: contain;' : 'object-fit: cover;'"
/> />
<div
v-if="isFullscreen && controls"
style="
position: absolute;
width: calc(100% - 12rem);
left: 3rem;
top: 0.5rem;
"
>
<h4>{{ video.title }}</h4>
<div style="color: #aaa; font-size: 0.75rem">{{ video.channelName }}</div>
</div>
<seekbar :video=$refs.player v-if="$refs.player" /> <!-- // TODO: merge the bottom 2 into 1 reusable component -->
<controls v-if="$refs.player" :video="$refs.player" /> <v-btn
text
tile
style="
opacity: 0;
position: absolute;
top: 0;
left: 0;
width: 50%;
height: 100%;
"
@dblclick.stop="$refs.player.currentTime -= 10"
>
<v-icon>mdi-rewind</v-icon>
</v-btn>
<!-- <v-slider v-model="value" step="0"></v-slider> --> <v-btn
text
tile
style="
opacity: 0;
position: absolute;
top: 0;
left: 50%;
width: 50%;
height: 100%;
"
@dblclick.stop="$refs.player.currentTime += 10"
>
<v-icon>mdi-fast-forward</v-icon>
</v-btn>
<div
style="transition: opacity 0.15s ease-in-out"
:style="controls ? 'opacity: 1;' : 'opacity: 0; pointer-events: none'"
>
<minimize />
<loop />
<captions />
<close />
<v-btn
fab
text
small
style="
position: absolute;
top: calc(50% - 1.25rem);
left: calc(50% - 10rem);
"
color="white"
@click.stop="$refs.player.currentTime -= 5"
>
<v-icon size="1rem">mdi-rewind-5</v-icon>
</v-btn>
<v-btn
fab
text
style="
position: absolute;
top: calc(50% - 1.75rem);
left: calc(50% - 6.5rem);
"
color="white"
disabled
@click.stop=""
>
<v-icon size="2rem">mdi-skip-previous</v-icon>
</v-btn>
<playpause
v-if="$refs.player"
:video="$refs.player"
@close="controls = false"
/>
<v-btn
fab
text
style="
position: absolute;
top: calc(50% - 1.75rem);
left: calc(50% + 3rem);
"
color="white"
disabled
@click.stop=""
>
<v-icon size="2rem">mdi-skip-next</v-icon>
</v-btn>
<v-btn
fab
text
small
style="
position: absolute;
top: calc(50% - 1.25rem);
left: calc(50% + 7rem);
"
color="white"
@click.stop="$refs.player.currentTime += 5"
>
<v-icon size="1rem">mdi-fast-forward-5</v-icon>
</v-btn>
<watchtime v-if="$refs.player" :video="$refs.player" />
<!-- // TODO: merge the bottom 2 into 1 reusable component -->
<quality v-if="$refs.player" :video="$refs.player" :sources="sources" />
<speed v-if="$refs.player" :video="$refs.player" />
<fullscreen
:fullscreen="isFullscreen"
@fullscreen="(controls = $refs.player.paused), handleFullscreenChange()"
/>
</div>
<!-- NOTE: breaks in fullscreen -->
<seekbar
v-if="$refs.player"
v-show="!isFullscreen || controls"
:fullscreen="isFullscreen"
:video="$refs.player"
:sources="sources"
:controls="controls"
@seeking="seeking = !seeking"
/>
</div> </div>
</template> </template>
<script> <script>
import seekbar from '~/components/Player/seekbar.vue'; import loop from "~/components/Player/loop.vue";
import controls from '~/components/Player/controls.vue'; import close from "~/components/Player/close.vue";
import speed from "~/components/Player/speed.vue";
import seekbar from "~/components/Player/seekbar.vue";
import quality from "~/components/Player/quality.vue";
import minimize from "~/components/Player/minimize.vue";
import captions from "~/components/Player/captions.vue";
import playpause from "~/components/Player/playpause.vue";
import watchtime from "~/components/Player/watchtime.vue";
import fullscreen from "~/components/Player/fullscreen.vue";
export default { export default {
props: ["sources"],
components: { components: {
fullscreen,
watchtime,
playpause,
captions,
minimize,
quality,
seekbar, seekbar,
controls speed,
close,
loop,
},
props: {
sources: {
type: Array,
required: true,
},
video: {
type: Object,
required: true,
},
}, },
data() { data() {
return { return {
isFullscreen: false,
controls: false,
seeking: false,
contain: true,
vidSrc: "", vidSrc: "",
}; };
}, },
mounted() { mounted() {
this.vidSrc = this.sources[this.sources.length-1].url; console.log("sources", this.sources);
this.vidSrc = this.sources[this.sources.length - 1].url;
// TODO: detect orientation change and enter fullscreen
// TODO: detect video loading state and send this.loading to play button :loading = loading
},
beforeDestroy() {
if (this.isFullscreen) this.exitFullscreen();
}, },
methods: { methods: {
handleFullscreenChange() { handleFullscreenChange() {
if (document.fullscreenElement === this.$refs.player) { if (document?.fullscreenElement === this.$refs.vidcontainer) {
this.$vuetube.statusBar.hide(); this.exitFullscreen();
this.$vuetube.navigationBar.hide();
} else { } else {
this.$vuetube.statusBar.show(); this.openFullscreen();
this.$vuetube.navigationBar.show();
} }
}, },
exitFullscreen() {
const cancellFullScreen =
document.exitFullscreen ||
document.mozCancelFullScreen ||
document.webkitExitFullscreen ||
document.msExitFullscreen;
cancellFullScreen.call(document);
screen.orientation.lock("portrait");
screen.orientation.unlock();
this.$vuetube.navigationBar.show();
this.$vuetube.statusBar.show();
this.isFullscreen = false;
},
openFullscreen() {
const element = this.$refs.vidcontainer;
const requestFullScreen =
element.requestFullscreen ||
element.webkitRequestFullScreen ||
element.mozRequestFullScreen ||
element.msRequestFullScreen;
requestFullScreen.call(element);
screen.orientation.lock("landscape");
this.$vuetube.navigationBar.hide();
this.$vuetube.statusBar.hide();
this.isFullscreen = true;
},
getPlayer() { getPlayer() {
return this.$refs.player; return this.$refs.player;
}, },
}, },
}; };
</script> </script>
<style>
.dim {
filter: brightness(42%);
}
.invisible {
opacity: 0;
}
</style>

View file

@ -1,35 +0,0 @@
<template>
<div>
<video
ref="player"
controls
autoplay
:src="vidSrc"
width="100%"
style="max-height: 50vh; display: block"
@webkitfullscreenchange="handleFullscreenChange"
/>
<!-- <v-slider v-model="value" step="0"></v-slider> -->
</div>
</template>
<script>
export default {
props: ["vidSrc"],
methods: {
handleFullscreenChange() {
if (document.fullscreenElement === this.$refs.player) {
this.$vuetube.statusBar.hide();
this.$vuetube.navigationBar.hide();
} else {
this.$vuetube.statusBar.show();
this.$vuetube.navigationBar.show();
}
},
getPlayer() {
return this.$refs.player;
},
},
};
</script>

View file

@ -0,0 +1,11 @@
<template>
<v-btn
fab
text
small
disabled
style="position: absolute; top: 0.25rem; right: 6rem"
>
<v-icon>mdi-sync</v-icon>
</v-btn>
</template>

View file

@ -0,0 +1,11 @@
<template>
<v-btn
fab
text
small
disabled
style="position: absolute; top: 0.25rem; left: 0.25rem"
>
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
</template>

View file

@ -0,0 +1,27 @@
<template>
<v-btn
fab
text
large
style="position: absolute; top: calc(50% - 2rem); left: calc(50% - 2rem)"
color="white"
@click.stop="
(paused = !video.paused),
video.paused ? (video.play(), $emit('close')) : video.pause()
"
>
<v-icon size="3.5rem">
{{ paused ? "mdi-play" : "mdi-pause" }}
</v-icon>
</v-btn>
</template>
<script>
export default {
props: ["video"],
emits: ["close"],
data: () => ({
paused: false,
}),
};
</script>

View file

@ -0,0 +1,66 @@
<template>
<div>
<v-bottom-sheet
v-model="sheet"
:attach="$parent.$refs.vidcontainer"
scrollable
>
<template #activator="{ on, attrs }">
<v-btn
fab
text
small
style="position: absolute; bottom: 0.25rem; right: 3rem"
v-bind="attrs"
v-on="on"
>
{{ sources.find((src) => src.url == video.src).qualityLabel }}
</v-btn>
</template>
<v-card
v-touch="{
down: () => (sheet = false),
}"
class="background"
>
<v-subheader>Quality for current video</v-subheader>
<v-card-text style="max-height: 50vh" class="pa-0">
<v-list-item
v-for="src in sources"
:key="src"
@click="(sheet = false), (video.src = src.url)"
>
<v-list-item-avatar>
<v-icon
:color="
video.src === src.url
? 'primary'
: $vuetify.theme.dark
? 'background lighten-2'
: 'background darken-2'
"
v-text="
video.src === src.url
? 'mdi-radiobox-marked'
: 'mdi-radiobox-blank'
"
></v-icon>
</v-list-item-avatar>
<v-list-item-title>
{{ src.qualityLabel }} ({{ src.quality }})
</v-list-item-title>
</v-list-item>
</v-card-text>
</v-card>
</v-bottom-sheet>
</div>
</template>
<script>
export default {
props: ["video", "sources"],
data: () => ({
sheet: false,
}),
};
</script>

View file

@ -1,42 +1,251 @@
<template> <template>
<div> <div>
<video
ref="playerfake"
muted
autoplay
style="display: none"
:src="vidWrs"
/>
<v-progress-linear <v-progress-linear
query
active active
background-color="primary" style="width: 100%; background: #ffffff22"
background-opacity="0.5" background-opacity="0.5"
background-color="primary"
:buffer-value="buffered" :buffer-value="buffered"
:value="percent"
color="primary" color="primary"
height="3" height="3"
query :style="
:value="percentage" fullscreen
? 'width: calc(100% - 2rem); left: 1rem; position: absolute; bottom: 3rem;'
: 'width: 100%'
"
/> />
<!-- Scrubber -->
<v-slider
id="scrubber"
hide-details
height="2"
dense
track-color="transparent"
:class="!controls && !fullscreen && !scrubbing ? 'invisible' : ''"
style="position: absolute; z-index: 2"
:style="
fullscreen
? 'width: calc(100% - 2rem); left: 1rem; bottom: 3rem;'
: 'width: calc(100% - 0.8rem); left: 0.4rem; bottom: 0;'
"
:thumb-size="0"
:max="duration"
:value="progress"
@start="(scrubbing = true), $emit('seeking')"
@end="(scrubbing = false), $emit('seeking')"
@change="scrub($event)"
@input="scrubbing ? seek($event) : null"
>
<template #thumb-label="{ value }">
<div style="transform: translateY(-50%)">
<canvas
ref="preview"
class="white"
:width="video.clientWidth / 3"
:height="video.clientHeight / 3"
style="border: 2px solid white"
:style="{
borderRadius: $store.state.tweaks.roundWatch
? `${$store.state.tweaks.roundTweak / 3}rem`
: '0',
}"
></canvas>
<div class="text-center pb-4" style="font-size: 0.8rem">
<b>{{ $vuetube.humanTime(value) }}</b>
</div>
</div>
</template>
</v-slider>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
props: ["video"], props: ["sources", "video", "controls", "fullscreen"],
data() { data() {
return { return {
percentage: 0, scrubbing: false,
buffered: 0 percent: 0,
} progress: 0,
}, buffered: 0,
duration: 0,
mounted() { vidSrc: "",
this.video.ontimeupdate = () => { vidWrs: "",
this.percentage = (this.video.currentTime / this.video.duration) * 100;
}; };
this.video.addEventListener("progress", () => { },
this.buffered = (this.video.buffered.end(0) / this.video.duration) * 100; mounted() {
console.log("sources", this.sources);
this.vidSrc = this.sources[this.sources.length - 1].url;
this.vidWrs = this.sources[1].url;
let vid = this.video;
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;
};
vid.onprogress = () => {
// console.log("%c progress", "color: #ff00ff");
this.buffered = (vid.buffered.end(0) / vid.duration) * 100;
};
}
}); });
} },
methods: {
// TODO: better scrubbing preview
loadVideoFrames() {
// Exit loop if desired number of frames have been extracted
if (this.frames.length >= frameCount) {
this.visibleFrame = 0;
} // Append all canvases to container div
this.frames.forEach((frame) => {
this.frameContainerElement.appendChild(frame);
});
return;
}
// If extraction hasnt started, set desired time for first frame
if (this.frames.length === 0) {
this.requestedTime = 0;
} else {
this.requestedTime = this.requestedTime + this.frameTimestep;
}
// Send seek request to video player for the next frame.
this.videoElement.currentTime = this.requestedTime;
},
extractFrame(videoWidth, videoHeight) {
// Create DOM canvas object
var canvas = document.createElement("canvas");
canvas.className = "video-scrubber-frame";
canvas.height = videoHeight;
canvas.width = videoWidth;
// Copy current frame to canvas
var context = canvas.getContext("2d");
context.drawImage(this.videoElement, 0, 0, videoWidth, videoHeight);
this.frames.push(canvas);
// Load the next frame
loadVideoFrames();
},
prefetch_file(url, fetched_callback, progress_callback, error_callback) {
var xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.responseType = "blob";
xhr.addEventListener(
"load",
function () {
if (xhr.status === 200) {
var URL = window.URL || window.webkitURL;
var blob_url = URL.createObjectURL(xhr.response);
fetched_callback(blob_url);
} else {
error_callback();
}
},
false
);
var prev_pc = 0;
xhr.addEventListener("progress", function (event) {
if (event.lengthComputable) {
var pc = Math.round((event.loaded / event.total) * 100);
if (pc != prev_pc) {
prev_pc = pc;
progress_callback(pc);
}
}
});
xhr.send();
},
async extractFramesFromVideo(videoUrl, fps = 25) {
// fully download it first (no buffering):
console.log(videoUrl);
console.log(fps);
let videoBlob = await fetch(videoUrl, {
headers: { range: "bytes=0-567139" },
}).then((r) => r.blob());
console.log(videoBlob);
let videoObjectUrl = URL.createObjectURL(videoBlob);
let video = document.createElement("video");
let seekResolve;
video.addEventListener("seeked", async function () {
if (seekResolve) seekResolve();
});
video.src = videoObjectUrl;
// workaround chromium metadata bug (https://stackoverflow.com/q/38062864/993683)
while (
(video.duration === Infinity || isNaN(video.duration)) &&
video.readyState < 2
) {
await new Promise((r) => setTimeout(r, 1000));
video.currentTime = 10000000 * Math.random();
}
let duration = video.duration;
let canvas = document.createElement("canvas");
let context = canvas.getContext("2d");
let [w, h] = [video.videoWidth, video.videoHeight];
canvas.width = w;
canvas.height = h;
let interval = 1;
let currentTime = 0;
while (currentTime < duration) {
video.currentTime = currentTime;
await new Promise((r) => (seekResolve = r));
context.drawImage(video, 0, 0, w, h);
let base64ImageData = canvas.toDataURL();
console.log(base64ImageData);
this.frames.push(base64ImageData);
currentTime += interval;
}
console.log("%c frames", "color: #00ff00");
console.log(this.frames);
},
// TODO: scrubbing preview end
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.video.clientWidth / 3,
this.video.clientHeight / 3
);
},
scrub(e) {
this.video.currentTime = e;
},
},
};
</script> </script>

View file

@ -0,0 +1,64 @@
<template>
<div>
<v-bottom-sheet
v-model="sheet"
:attach="$parent.$refs.vidcontainer"
scrollable
>
<template #activator="{ on, attrs }">
<v-btn
fab
text
small
style="position: absolute; bottom: 0.25rem; right: 6rem"
v-bind="attrs"
v-on="on"
>
{{ video.playbackRate }}X
</v-btn>
</template>
<v-card
v-touch="{
down: () => (sheet = false),
}"
class="background"
>
<v-subheader>Playback Speed</v-subheader>
<v-card-text style="height: 50vh" class="pa-0">
<v-list-item
v-for="sped in speeds"
:key="sped"
@click="(sheet = false), (video.playbackRate = sped)"
>
<!-- // TODO: save playbackRate to localStorage and manage via store/video/index.js -->
<v-list-item-avatar>
<v-icon
:color="
video.playbackRate === sped
? 'primary'
: $vuetify.theme.dark
? 'background lighten-2'
: 'background darken-2'
"
v-text="
video.playbackRate === sped ? 'mdi-check' : 'mdi-speedometer'
"
></v-icon>
</v-list-item-avatar>
<v-list-item-title>{{ sped }}X</v-list-item-title>
</v-list-item>
</v-card-text>
</v-card>
</v-bottom-sheet>
</div>
</template>
<script>
export default {
props: ["video"],
data: () => ({
sheet: false,
speeds: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4, 8, 16],
}),
};
</script>

View file

@ -0,0 +1,32 @@
<template>
<div
style="
color: #fff;
left: 1rem;
bottom: 1rem;
font-size: 0.75rem;
position: absolute;
"
>
{{ watched }}
<span style="color: #aaa"> / {{ duration }} </span>
</div>
</template>
<script>
export default {
props: ["video"],
data() {
return {
watched: 0,
duration: 0,
};
},
mounted() {
this.video.addEventListener("timeupdate", () => {
this.duration = this.$vuetube.humanTime(this.video.duration);
this.watched = this.$vuetube.humanTime(this.video.currentTime);
});
},
};
</script>

View file

@ -14,7 +14,9 @@
</v-list-item> </v-list-item>
<div <div
v-if=" 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="separator-bottom background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"

View file

@ -1,34 +1,36 @@
<template> <template>
<div class="fill-width"> <div class="fill-width">
<h4 v-if="render.headerRenderer" class="font-weight-bold shelf-header"> <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 render.headerRenderer.elementRenderer.newElement.type.componentType
.model.shelfHeaderModel.shelfHeaderData.title .model.shelfHeaderModel.shelfHeaderData.title
}} }}
</h4> </h4>
<v-list-item class="pa-0 min-height-0"> <div class="pa-0 min-height-0">
<component <component
v-if="render.content && getComponents()[Object.keys(render.content)[0]]"
:is="Object.keys(render.content)[0]" :is="Object.keys(render.content)[0]"
v-if="render.content && getComponents()[Object.keys(render.content)[0]]"
:render="render.content[Object.keys(render.content)[0]]" :render="render.content[Object.keys(render.content)[0]]"
></component ></component>
></v-list-item> </div>
<!-- <div <div
v-if="render.separator && render.separator.hasBottomSeparator" v-if="render.separator && render.separator.hasBottomSeparator"
class="separator-bottom background" class="separator-bottom background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
:style="{ height: render.separator.height + 'px' }" :style="{ height: render.separator.height + 'px' }"
></div> --> ></div>
</div> </div>
</template> </template>
<style scoped>
.shelf-header {
width: 100%; /* Prevent Loading Weirdness */
padding: 10px;
}
</style>
<script> <script>
import verticalListRenderer from "~/components/ListRenderers/verticalListRenderer.vue"; import verticalListRenderer from "~/components/ListRenderers/verticalListRenderer.vue";
import horizontalListRenderer from "~/components/ListRenderers/horizontalListRenderer.vue"; import horizontalListRenderer from "~/components/ListRenderers/horizontalListRenderer.vue";
@ -47,3 +49,10 @@ export default {
}, },
}; };
</script> </script>
<style scoped>
.shelf-header {
width: 100%; /* Prevent Loading Weirdness */
padding: 10px;
}
</style>

View file

@ -1,13 +1,11 @@
<template> <template>
<v-card <v-card
class="entry videoRenderer background" class="entry videoRenderer background overflow-hidden"
:to="`/watch?v=${vidId}`" :to="`/watch?v=${vidId}`"
:style="{ :style="{
borderRadius: `${roundTweak / 2.5}rem`, borderRadius: roundThumb ? `${roundTweak / 2}rem` : '0',
margin: margin:
$store.state.tweaks.roundTweak > 0 roundThumb && roundTweak > 0 ? '0 1rem 1rem 1rem' : '0 0 0.25rem 0',
? '0 1rem 1rem 1rem'
: '0 0 0.25rem 0',
}" }"
flat flat
> >
@ -16,7 +14,7 @@
:aspect-ratio="16 / 9" :aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(vidId, 'max', thumbnails)" :src="$youtube.getThumbnail(vidId, 'max', thumbnails)"
:style="{ :style="{
borderRadius: `${roundTweak / 2.5}rem`, borderRadius: roundThumb ? `${roundTweak / 12}rem` : '0',
}" }"
/> />
<div <div
@ -27,8 +25,27 @@
v-text="thumbnailOverlayText" v-text="thumbnailOverlayText"
/> />
</div> </div>
<div id="details"> <div
<a :href="channelUrl" class="avatar-link pl-2 pt-2"> id="details"
class="background mt-1"
:class="
roundThumb && roundTweak > 0
? $vuetify.theme.dark
? 'lighten-1'
: 'darken-1'
: ''
"
:style="{
borderRadius: roundThumb ? `${roundTweak / 12}rem` : '0',
}"
>
<a
class="avatar-link pl-2 pt-2"
@click.prevent="
$store.dispatch('channel/fetchChannel', channelUrl),
$router.push('/channel')
"
>
<v-img class="avatar-thumbnail" :src="channelIcon" /> <v-img class="avatar-thumbnail" :src="channelIcon" />
</a> </a>
<v-card-text class="video-info pt-2" v-emoji> <v-card-text class="video-info pt-2" v-emoji>
@ -42,8 +59,8 @@
</span> </span>
<div <div
class="background--text text--lighten-5 caption" class="background--text caption"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'" :class="$vuetify.theme.dark ? 'text--lighten-5' : 'text--darken-4'"
v-text="bottomText" v-text="bottomText"
/> />
</v-card-text> </v-card-text>
@ -51,6 +68,52 @@
</v-card> </v-card>
</template> </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> <style scoped>
.entry { .entry {
width: 100%; /* Prevent Loading Weirdness */ width: 100%; /* Prevent Loading Weirdness */
@ -59,8 +122,9 @@
position: absolute; position: absolute;
bottom: 10px; bottom: 10px;
right: 10px; right: 10px;
border-radius: 5px; border-radius: 4px;
padding: 0px 4px 0px 4px; padding: 0px 4px 0px 4px;
font-size: 0.66rem;
} }
.videoRuntimeFloat.style-DEFAULT { .videoRuntimeFloat.style-DEFAULT {
@ -119,46 +183,3 @@
} }
} }
</style> </style>
<script>
export default {
computed: {
roundTweak() {
return this.$store.state.tweaks.roundTweak;
},
},
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

@ -1,23 +1,39 @@
<template> <template>
<!-- hide-on-scroll --> <div class="bottomNav background">
<v-bottom-navigation <v-divider v-if="!$store.state.tweaks.roundTweak" />
v-model="tabSelection" <v-bottom-navigation
shift v-model="tabSelection"
class="bottomNav py-4 background" style="padding: 0 !important; box-shadow: none !important"
:style=" class="transparent"
$vuetify.theme.dark shift
? '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
> >
<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" /> <span v-text="item.name" />
<v-icon <v-icon
:color=" :color="
@ -35,18 +51,20 @@
: '' : ''
" "
v-text="item.icon" 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') + (tabSelection == i ? '' : '-outline')
--> -->
</v-btn> </v-btn>
<!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()" <!-- <v-btn
><v-icon>mdi-magnify</v-icon></v-btn text
> --> class="navButton mr-2 fill-height"
</v-bottom-navigation> color="white"
@click="searchBtn()"
><v-icon>mdi-magnify</v-icon></v-btn
> -->
</v-bottom-navigation>
</div>
</template> </template>
<script> <script>
@ -56,31 +74,37 @@ export default {
tabSelection: 0, tabSelection: 0,
tabs: [ tabs: [
// TODO: pull from Vuex & localStorage for customizations // TODO: pull from Vuex & localStorage for customizations
{ name: "Home", icon: "mdi-home", link: "/home" }, { name: "...", icon: "mdi-home", link: "/home" },
//{ name: "Shorts", icon: "mdi-lightning-bolt", link: "/shorts" }, //{ name: "Shorts", icon: "mdi-lightning-bolt", link: "/shorts" },
//{ name: "Upload", icon: "mdi-plus", link: "/upload" }, //{ name: "Upload", icon: "mdi-plus", link: "/upload" },
{ {
name: "Subscriptions", name: "...",
icon: "mdi-youtube-subscription", icon: "mdi-youtube-subscription",
link: "/subscriptions", link: "/subscriptions",
}, },
{ name: "Library", icon: "mdi-view-list", link: "/library" }, { name: "...", icon: "mdi-view-list", link: "/library" },
// { name: "Settings", icon: "mdi-menu", link: "/settings" }, // { name: "Settings", icon: "mdi-menu", link: "/settings" },
], ],
}; };
}, },
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;
},
}; };
</script> </script>
<style scoped> <style scoped>
.bottomNav { .bottomNav {
/* box-shadow: inset 0 1rem 10rem var(--v-background-base) !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; box-shadow: none !important;
/* ios gesture nav */
bottom: env(safe-area-inset-bottom) !important;
height: 4rem !important;
padding: 0 !important;
position: fixed; position: fixed;
width: 100%;
bottom: 0;
} }
.navButton { .navButton {
width: 25vw !important; 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,9 +1,9 @@
<template> <template>
<v-card class="dialog-base"> <v-card flat class="dialog-base background">
<v-expand-transition> <div
<slot name="reveal"></slot> class="toolbar-container d-flex flex-column background"
</v-expand-transition> style="flex-direction: column !important"
<div class="toolbar-container"> >
<v-toolbar color="background" flat> <v-toolbar color="background" flat>
<slot name="header"></slot> <slot name="header"></slot>
</v-toolbar> </v-toolbar>
@ -12,6 +12,9 @@
<div class="dialog-body background"> <div class="dialog-body background">
<slot></slot> <slot></slot>
</div> </div>
<v-expand-transition>
<slot name="reveal"></slot>
</v-expand-transition>
</v-card> </v-card>
</template> </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 / 2}rem`
: '0',
}"
>
<div
class="d-flex flex-column justify-center align-center"
style="
position: absolute;
top: 0;
right: 0;
width: 50%;
height: 100%;
background: linear-gradient(var(--v-background-base) -1000%, #00000000 1000%);
"
>
<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-share-outline</v-icon>
</v-btn>
<v-btn
text
tile
elevation="0"
class="flex-grow-1"
style="width: 2rem !important"
>
<v-icon>mdi-playlist-plus</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,15 +1,14 @@
<template> <template>
<v-card <v-card style="display: flex" class="rounded-0 pa-3 topNav background">
scroll-off-screen <!-- opacity with vue 😉 -->
style="height: 4rem !important; display: flex" <!-- :style="{ background: $vuetify.theme.currentTheme.primary + '55' }" -->
class="rounded-0 pa-3 topNav background" <h3
> v-show="!search"
<!-- :style=" class="my-auto ml-4"
$vuetify.theme.dark v-text="
? 'border-bottom: 1px solid var(--v-background-lighten1) !important;' $route.path.includes('channel') ? $store.state.channel.title : page
: 'border-bottom: 1px solid var(--v-background-darken1) !important;' "
" --> />
<h3 v-show="!search" class="my-auto ml-4" v-text="page" />
<v-btn <v-btn
v-if="search" v-if="search"
@ -26,8 +25,9 @@
solo solo
dense dense
flat flat
autofocus
label="Search" label="Search"
style="margin-top: 1px" style="margin-top: 7px"
:background-color=" :background-color="
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1' $vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
" "
@ -49,6 +49,7 @@
<v-icon>mdi-refresh</v-icon> <v-icon>mdi-refresh</v-icon>
</v-btn> </v-btn>
<v-btn <v-btn
v-if="$route.name !== 'settings' && !$route.path.includes('/mods')"
icon icon
tile tile
class="ml-3 my-auto fill-height" class="ml-3 my-auto fill-height"
@ -82,7 +83,7 @@ export default {
default: "Home", default: "Home",
}, },
}, },
events: ["searchBtn", "textChanged", "closeSearch"], events: ["searchBtn", "textChanged", "closeSearch", "scrollToTop"],
data: () => ({ data: () => ({
text: "", text: "",
}), }),
@ -113,12 +114,15 @@ export default {
<style scoped> <style scoped>
.topNav { .topNav {
/* box-shadow: inset 0 1rem 10rem var(--v-background-base) !important; */ /* opacity with hex, wow 😉 */
/* background: linear-gradient(var(--v-background-base) -1000%, #00000000 1000%); */
/* box-shadow: inset 0 0 5rem 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; box-shadow: none !important;
/* ios notch */
top: env(safe-area-inset-top) !important;
position: fixed; position: fixed;
width: 100%; width: 100%;
top: 0;
} }
.topNavSearch { .topNavSearch {

View file

@ -4,13 +4,15 @@
:search="search" :search="search"
:page="page" :page="page"
style="z-index: 696969" style="z-index: 696969"
@close-search="search = !search"
@search-btn="searchBtn" @search-btn="searchBtn"
@text-changed="textChanged" @text-changed="textChanged"
@close-search="search = !search"
@scroll-to-top="$refs.pgscroll.scrollTop = 0" @scroll-to-top="$refs.pgscroll.scrollTop = 0"
/> />
<!-- channel-tabs -->
<v-tabs <v-tabs
v-if="$route.path.includes('/channel')" v-if="$route.path.includes('/channel') && !search"
mobile-breakpoint="0" mobile-breakpoint="0"
style=" style="
position: fixed; position: fixed;
@ -24,6 +26,7 @@
v-for="tab in channelTabs" v-for="tab in channelTabs"
:key="tab.name" :key="tab.name"
:to="tab.to" :to="tab.to"
exact
:v-ripple="false" :v-ripple="false"
> >
{{ tab.name }} {{ tab.name }}
@ -31,9 +34,15 @@
</v-tabs> </v-tabs>
<div <div
style="height: 100%; padding-bottom: 4rem" style="
height: 100%;
padding-bottom: calc(4rem + env(safe-area-inset-bottom));
"
:style="{ :style="{
marginTop: $route.path.includes('/channel') ? '7rem' : '4rem', paddingTop:
$route.path.includes('/channel') && !search
? 'calc(7rem + env(safe-area-inset-top))'
: 'calc(4rem + env(safe-area-inset-top))',
}" }"
> >
<div v-show="!search"> <div v-show="!search">
@ -53,21 +62,17 @@
> >
<div class="scroll-y" style="height: 100%"> <div class="scroll-y" style="height: 100%">
<div v-if="search" style="min-width: 180px"> <div v-if="search" style="min-width: 180px">
<v-list-item <v-list-item v-for="item in response" :key="item[0]" class="px-0">
v-for="(item, index) in response"
:key="index"
class="px-0"
>
<v-btn <v-btn
v-emoji
text text
tile tile
dense dense
class="searchButton text-left text-none" class="searchButton text-left text-none"
@click="youtubeSearch(item)" @click="youtubeSearch(item)"
v-emoji
> >
<v-icon class="mr-5">mdi-magnify</v-icon> <v-icon class="mr-5">mdi-magnify</v-icon>
{{ item[0] || item.text }} {{ item[0] }}
</v-btn> </v-btn>
</v-list-item> </v-list-item>
</div> </div>
@ -154,28 +159,28 @@ export default {
return; return;
} // No text found, no point in calling API } // 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 ---// //--- Auto Suggest ---//
this.$youtube.autoComplete(text, (res) => { this.$youtube.autoComplete(text, (res) => {
const data = res.replace(/^.*?\(/, "").replace(/\)$/, ""); //Format Response const data = res.replace(/^.*?\(/, "").replace(/\)$/, ""); //Format Response
this.response = JSON.parse(data)[1]; 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) { 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.$router.push(link);
this.search = false; this.search = false;
}, },
@ -226,11 +231,39 @@ export default {
.v-slide-group__next { .v-slide-group__next {
display: none !important; 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 { .glassy {
backdrop-filter: blur(20px); backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px); -webkit-backdrop-filter: blur(20px);
} }
.transparent-lighten-1 {
background: #ffffff22;
}
.transparent-darken-1 {
background: #00000022;
}
.debug {
outline: 1px solid red;
}
.v-card--reveal {
bottom: 0;
opacity: 1 !important;
position: absolute !important;
width: 100%;
}
.scrollcontainer { .scrollcontainer {
overflow: hidden; overflow: hidden;
@ -248,7 +281,9 @@ export default {
html, html,
body { body {
background: var(--v-background-base); background: var(--v-background-base);
/* overflow-x: hidden; */ -webkit-overflow-scrolling: touch !important;
overflow-y: scroll !important;
overflow-x: hidden !important;
} }
p, p,

View file

@ -17,6 +17,7 @@ export default {
{ src: "~/plugins/vuetube", mode: "client" }, { src: "~/plugins/vuetube", mode: "client" },
{ src: "~/plugins/ryd", mode: "client" }, { src: "~/plugins/ryd", mode: "client" },
{ src: "~/plugins/thirdPartyPluginLoader", mode: "client" }, { src: "~/plugins/thirdPartyPluginLoader", mode: "client" },
{ src: "~/plugins/language", mode: "client" },
], ],
generate: { generate: {
dir: "../dist", dir: "../dist",

View file

@ -1,83 +0,0 @@
<template>
<div class="d-flex flex-column align-center">
<v-img
height="120"
:src="banner"
class="background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
></v-img>
<v-avatar size="60" class="mt-2 primary">
<img :src="avatar" />
</v-avatar>
<h2 class="mt-2">{{ title }}</h2>
<v-btn
:aria-label="subscribeAlt"
class="mt-2"
text
color="primary"
style="height: 1rem"
>
{{ subscribe }}
</v-btn>
<div style="font-size: 0.75rem" class="mt-2">
{{ subscribers }} &middot; {{ videos }}
</div>
<div
style="font-size: 0.75rem"
class="background--text text-center px-4"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
{{ descriptionPreview }}
<v-icon
class="background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
mdi-chevron-right
</v-icon>
</div>
</div>
</template>
<script>
export default {
data: () => ({
channel: null,
avatar: null,
banner: null,
title: null,
subscribe: null,
subscribeAlt: null,
descriptionPreview: null,
subscribers: null,
videos: null,
}),
mounted() {
console.log(
"%c getChannel ",
"color: black; font-weight: bold; background: #f00; padding: .5rem .25rem; border-radius: .25rem;"
);
this.$youtube
.getChannel(`https://youtube.com/channel/${this.$route.params.id}`)
.then((channel) => {
this.channel = channel;
console.log(channel);
this.banner =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelBanner.image.sources[0].url;
this.avatar =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.avatarData.avatar.image.sources[0].url;
this.title =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.title;
this.subscribe =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.subscribeButton.subscribeButtonContent.buttonText;
this.subscribeAlt =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.subscribeButton.subscribeButtonContent.accessibilityText;
this.descriptionPreview =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.descriptionPreview.description;
this.subscribers =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.metadata.subscriberCountText;
this.videos =
channel.header.channelMobileHeaderRenderer.channelHeader.elementRenderer.newElement.type.componentType.model.channelHeaderModel.channelProfile.metadata.videosCountText;
});
},
};
</script>

View file

@ -1,3 +1,23 @@
<template> <template>
<div></div> <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> </template>

View file

@ -1,3 +1,21 @@
<template> <template>
<div></div> <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> </template>
<script>
import compactChannelRenderer from "../../components/CompactRenderers/compactChannelRenderer.vue";
export default {
components: { compactChannelRenderer },
};
</script>

View file

@ -1,3 +1,22 @@
<template> <template>
<div></div> <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> </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

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

View file

@ -1,3 +1,12 @@
<template> <template>
<div></div> <div>
<!-- <compact-video-renderer :video="$store.state.channel.videoExample" /> -->
</div>
</template> </template>
<script>
import CompactVideoRenderer from "../../components/CompactRenderers/compactVideoRenderer.vue";
export default {
components: { CompactVideoRenderer },
};
</script>

View file

@ -18,9 +18,11 @@ export default {
layout: "empty", layout: "empty",
data: () => ({ data: () => ({
progressMsg: "Connecting", progressMsg: "...",
}), }),
async mounted() { async mounted() {
this.progressMsg = this.$lang("index").connecting;
this.$store.commit("tweaks/initTweaks"); this.$store.commit("tweaks/initTweaks");
const theming = new Promise((resolve) => const theming = new Promise((resolve) =>
// Set timeout is required for $vuetify.theme... dont ask me why -Front // Set timeout is required for $vuetify.theme... dont ask me why -Front
@ -48,17 +50,17 @@ export default {
this.$vuetify.theme.currentTheme.background, this.$vuetify.theme.currentTheme.background,
this.$vuetify.theme.dark this.$vuetify.theme.dark
); );
// this.$vuetube.statusBar.setTransparent();
// this.$vuetube.navigationBar.setTransparent(); // this.$vuetube.navigationBar.setTransparent();
// this.$vuetube.statusBar.setTransparent();
resolve(); resolve();
}, 0) }, 0)
); );
await theming; await theming;
await this.$youtube.getAPI(); await this.$youtube.getAPI();
this.progressMsg = "Launching";
await this.$vuetube.launchBackHandling(); await this.$vuetube.launchBackHandling();
this.progressMsg = "Navigating"; this.progressMsg = this.$lang("index").launching;
this.$router.replace(`/${localStorage.getItem("startPage") || "home"}`); // Prevent user from navigating back to the splash screen this.$router.replace(`/${localStorage.getItem("startPage") || "home"}`); // Prevent user from navigating back to the splash screen
}, },

View file

@ -1,25 +1,18 @@
<template> <template>
<center class="px-4"> <div>
<v-img <h4
contain class="ml-7 mb-2 background--text"
style="margin-top: 5em; max-width: 80%; max-height: 15em" :class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
src="/dev.svg"
/>
<h2
class="background--text mt-4"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
> >
Page Under Construction Local Playlists
</h2> </h4>
<p <playlist-card />
class="background--text" </div>
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
Please read the VueTube FAQ for more information.
</p>
</center>
</template> </template>
<script> <script>
export default {}; import playlistCard from "../components/playlistCard.vue";
export default {
components: { playlistCard },
};
</script> </script>

View file

@ -22,9 +22,9 @@
" "
:style="{ borderRadius: `${roundTweak / 2}rem` }" :style="{ borderRadius: `${roundTweak / 2}rem` }"
> >
<v-card-title>App Information</v-card-title> <v-card-title>{{ languagePack.mods.about.appinformation }}</v-card-title>
<v-card-text> <v-card-text>
<h3>App Version</h3> <h3>{{ languagePack.mods.about.appversion }}</h3>
{{ version.substring(0, 7) || "Unknown" }} {{ version.substring(0, 7) || "Unknown" }}
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -39,19 +39,21 @@
" "
:style="{ borderRadius: `${roundTweak / 2}rem` }" :style="{ borderRadius: `${roundTweak / 2}rem` }"
> >
<v-card-title>Device Information</v-card-title> <v-card-title>{{
languagePack.mods.about.deviceinformation
}}</v-card-title>
<v-card-text> <v-card-text>
<h3>Platform</h3> <h3>{{ languagePack.mods.about.platform }}</h3>
{{ deviceInfo.platform || "Unknown" }}<br /> {{ deviceInfo.platform || "Unknown" }}<br />
<h3>Operating System</h3> <h3>{{ languagePack.mods.about.os }}</h3>
{{ deviceInfo.operatingSystem || "Unknown" }} ({{ {{ deviceInfo.operatingSystem || "Unknown" }} ({{
deviceInfo.osVersion || "Unknown" deviceInfo.osVersion || "Unknown"
}})<br /> }})<br />
<h3>Model</h3> <h3>{{ languagePack.mods.about.model }}</h3>
{{ deviceInfo.model || "Unknown" }}<br /> {{ deviceInfo.model || "Unknown" }}<br />
<h3>Manufacturer</h3> <h3>{{ languagePack.mods.about.manufacturer }}</h3>
{{ deviceInfo.manufacturer || "Unknown" }}<br /> {{ deviceInfo.manufacturer || "Unknown" }}<br />
<h3>Emulator</h3> <h3>{{ languagePack.mods.about.emulator }}</h3>
{{ deviceInfo.isVirtual ? "yes" : "no" }} {{ deviceInfo.isVirtual ? "yes" : "no" }}
</v-card-text> </v-card-text>
</v-card> </v-card>
@ -69,7 +71,7 @@
@click="openExternal('https://github.com/Frontesque/VueTube')" @click="openExternal('https://github.com/Frontesque/VueTube')"
> >
<v-icon x-large class="actionIcon">mdi-github</v-icon> <v-icon x-large class="actionIcon">mdi-github</v-icon>
Github {{ languagePack.mods.about.github }}
</v-btn> </v-btn>
<v-btn <v-btn
depressed depressed
@ -81,7 +83,7 @@
@click="openExternal('https://discord.gg/7P8KJrdd5W')" @click="openExternal('https://discord.gg/7P8KJrdd5W')"
> >
<v-icon x-large class="actionIcon">mdi-discord</v-icon> <v-icon x-large class="actionIcon">mdi-discord</v-icon>
Discord {{ languagePack.mods.about.discord }}
</v-btn> </v-btn>
</div> </div>
</div> </div>
@ -96,6 +98,7 @@ export default {
return { return {
version: process.env.appVersion, version: process.env.appVersion,
deviceInfo: "", deviceInfo: "",
languagePack: { mods: { about: {} } },
}; };
}, },
computed: { computed: {
@ -107,6 +110,8 @@ export default {
async mounted() { async mounted() {
const info = await Device.getInfo(); const info = await Device.getInfo();
this.deviceInfo = info; this.deviceInfo = info;
this.languagePack = this.$lang();
}, },
methods: { methods: {
async openExternal(url) { async openExternal(url) {

View file

@ -5,7 +5,7 @@
class="card background" class="card background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
flat flat
:style="{borderRadius: `${roundTweak / 2}rem`}" :style="{ borderRadius: `${roundTweak / 2}rem` }"
> >
<v-card-title> <v-card-title>
<v-chip v-if="item.error" outlined class="errorChip" color="error" <v-chip v-if="item.error" outlined class="errorChip" color="error"

View file

@ -1,59 +1,62 @@
<template> <template>
<div class="mainContainer pt-1"> <div class="mainContainer pt-1">
<v-card <v-card flat class="pb-5 background" :class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}">
flat
class="pb-5 background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
:style="{borderRadius: `${roundTweak / 2}rem`}"
>
<v-card-title>Default Page</v-card-title> <v-card-title>Default Page</v-card-title>
<v-card-text> <v-card-text>
<v-select <v-select v-model="page" background-color="background" :items="pages" label="Default Page" solo></v-select>
v-model="page"
background-color="background"
:items="pages"
label="Default Page"
solo
></v-select>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
computed: { computed: {
roundTweak() { roundTweak() {
return this.$store.state.tweaks.roundTweak; return this.$store.state.tweaks.roundTweak;
} }
},
data() {
return {
page: "home",
pages: ["home", "subscriptions", "library"],
};
},
watch: {
page: function (newVal) {
localStorage.setItem("startPage", newVal);
}, },
},
mounted() { data() {
this.page = localStorage.getItem("startPage") || "home"; return {
}, page: "home",
}; pages: [],
};
},
watch: {
page: function (newVal) {
localStorage.setItem("startPage", newVal);
},
},
mounted() {
this.page = localStorage.getItem("startPage") || "home";
const langPack = this.$lang('global');
this.pages = [{
value: "home",
text: langPack.home
}, {
value: "subscriptions",
text: langPack.subscriptions
}, {
value: "library",
text: langPack.library
}];
}
};
</script> </script>
<style scoped> <style scoped>
.v-card { .v-card {
margin: 1em; margin: 1em;
} }
section {
padding: 0 1em 1em 1em;
}
section {
padding: 0 1em 1em 1em;
}
</style> </style>

View file

@ -1,10 +1,13 @@
<template> <template>
<client-only> <client-only>
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_()_/¯ -->
<div <div
class="d-flex flex-column justify-end" class="d-flex flex-column justify-end"
style="min-height: calc(100vh - 8rem)" style="
min-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) !important;
"
> >
<!-- ----------------------------------------------Background Colors------------------------ --> <!-- ----Background Colors---- -->
<v-radio-group v-model="$vuetify.theme.currentTheme.background"> <v-radio-group v-model="$vuetify.theme.currentTheme.background">
<div <div
class="d-flex flex-row px-6 no-wrap" class="d-flex flex-row px-6 no-wrap"
@ -43,16 +46,28 @@
? '2px solid var(--v-primary-darken4)' ? '2px solid var(--v-primary-darken4)'
: '2px solid var(--v-primary-lighten4)', : '2px solid var(--v-primary-lighten4)',
}" }"
class="py-4 px-4 ma-2 rounded-lg" class="pa-4 ma-2 rounded-lg"
:value=" :value="$vuetify.theme.dark ? adaptiveDark : adaptiveLight"
$vuetify.theme.dark ? experimentalDark : experimentalLight
"
/> />
Adaptive Adaptive
</div> </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> </div>
</v-radio-group> </v-radio-group>
<!-- ----------------------------------------------Primary Colors------------------------ --> <!-- ----Primary Colors---- -->
<v-radio-group v-model="$vuetify.theme.currentTheme.primary" class="mx-2"> <v-radio-group v-model="$vuetify.theme.currentTheme.primary" class="mx-2">
<div <div
class="d-flex flex-row px-6 py-2 no-wrap align-center" class="d-flex flex-row px-6 py-2 no-wrap align-center"
@ -74,40 +89,53 @@
class="mr-2 my-auto rounded-xl" class="mr-2 my-auto rounded-xl"
:value="color" :value="color"
/> />
<v-dialog <!-- Custom Primary -->
v-model="dialog" <v-btn
width="300" icon
content-class="background rounded-lg" 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-icon>mdi-plus</v-icon>
<v-btn </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>
</div> </div>
</v-radio-group> </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 <v-card
flat flat
class="d-flex flex-row justify-between mx-8 mb-8 px-4 background rounded-lg" class="d-flex flex-row justify-between mx-8 mb-8 px-4 py-3 background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :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=" @click="
($vuetify.theme.dark = !$vuetify.theme.dark), ($vuetify.theme.dark = !$vuetify.theme.dark),
$vuetube.haptics.hapticsImpactLight(1) $vuetube.haptics.hapticsImpactLight(1)
@ -127,6 +155,7 @@
<v-switch <v-switch
v-model="$vuetify.theme.dark" v-model="$vuetify.theme.dark"
style="pointer-events: none" style="pointer-events: none"
class="mt-2"
persistent-hint persistent-hint
inset inset
/> />
@ -153,9 +182,10 @@ export default {
{ name: "Black", color: "#000000" }, { name: "Black", color: "#000000" },
], ],
backgroundsLight: [{ name: "Normal", color: "#ffffff" }], backgroundsLight: [{ name: "Normal", color: "#ffffff" }],
experimentalLight: "", adaptiveLight: "",
experimentalDark: "", adaptiveDark: "",
dialog: false, pickerState: false,
pickerMode: "bg",
}; };
}, },
watch: { watch: {
@ -169,19 +199,30 @@ export default {
: localStorage.setItem("backgroundLight", value); : localStorage.setItem("backgroundLight", value);
this.$vuetube.statusBar.setTheme(value, this.$vuetify.theme.dark); this.$vuetube.statusBar.setTheme(value, this.$vuetify.theme.dark);
this.$vuetube.navigationBar.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) { "$vuetify.theme.currentTheme.primary"(value) {
if (value != undefined) { if (value != undefined) {
this.$vuetify.theme.dark this.$vuetify.theme.dark
? localStorage.setItem("primaryDark", value) ? localStorage.setItem("primaryDark", value)
: localStorage.setItem("primaryLight", value); : localStorage.setItem("primaryLight", value);
let tempD = this.experimentalDark; let tempD = this.adaptiveDark;
let tempL = this.experimentalLight; let tempL = this.adaptiveLight;
this.adapt(); this.adapt();
if (this.$vuetify.theme.currentTheme.background === tempD) 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) if (this.$vuetify.theme.currentTheme.background === tempL)
this.$vuetify.theme.currentTheme.background = this.experimentalLight; this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
} }
}, },
}, },
@ -198,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 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 // the SPACE " " is stored as part of the CSS variable itself to be used for chaining
this.experimentalDark = hexD.substring(1).toUpperCase(); this.adaptiveDark = hexD.substring(1).toUpperCase();
this.experimentalLight = hexL.substring(1).toUpperCase(); this.adaptiveLight = hexL.substring(1).toUpperCase();
setTimeout(() => { setTimeout(() => {
if ( if (
this.$vuetify.theme.currentTheme.background == this.$vuetify.theme.currentTheme.background ==
hexD.substring(1).toUpperCase() hexD.substring(1).toUpperCase()
) )
this.$vuetify.theme.currentTheme.background = this.experimentalDark; this.$vuetify.theme.currentTheme.background = this.adaptiveDark;
if ( if (
this.$vuetify.theme.currentTheme.background == this.$vuetify.theme.currentTheme.background ==
hexL.substring(1).toUpperCase() hexL.substring(1).toUpperCase()
) )
this.$vuetify.theme.currentTheme.background = this.experimentalLight; this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
}, 0); }, 0);
}, },
}, },
}; };
</script> </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,67 +1,157 @@
<template> <template>
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_()_/¯ -->
<div <div
class="d-flex flex-column justify-end" class="d-flex flex-column justify-end"
style="min-height: calc(100vh - 8rem)" 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 <v-card
flat 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=" style="
transition-duration: 0.3s; transition-duration: 0.3s;
transition-property: border-radius; transition-property: border-radius;
overflow: hidden; overflow: hidden;
" "
:style="{
borderRadius: `${roundTweak / 2}rem`,
margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0',
}"
>
<div
v-for="item in list"
:key="item"
class="pa-4 mb-1 background text-center rounded-sm"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
@click="list.pop(item)"
>
{{ 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')"
>
+++++++++++++++++++++++++++++
</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="{ :style="{
borderRadius: `${roundTweak / 2}rem`, borderRadius: `${roundTweak / 2}rem`,
}" }"
> >
<!-- margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0', --> <!-- margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0', -->
<h3 class="mt-5">Rounded Corners</h3> <v-card
<div flat
class="background--text" class="mb-1 px-4 py-2 d-flex flex-row justify-between background"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'" :class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 12}rem`,
}"
@click="
(roundThumb = !roundThumb), $vuetube.haptics.hapticsImpactLight(1)
"
> >
applies to only a few elements for now <div
</div> class="my-auto background--text"
<!-- TODO: outer radius --> :class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
<!-- TODO: Dense Navbar --> >
<!-- TODO: Disable Top Bar --> Round Thumbnails
<!-- TODO: Top and Bottom bar color selection --> </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)
"
>
<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-slider
v-model="roundTweak" v-model="roundTweak"
class="mr-2 mt-5" class="pr-4 pl-4 pt-4 pb-1 background"
label="Inner"
:max="4" :max="4"
step="1" label="Radius"
step=".25"
thumb-size="64" thumb-size="64"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 12}rem`,
}"
@input="$vuetube.haptics.hapticsImpactLight(0)" @input="$vuetube.haptics.hapticsImpactLight(0)"
> >
<template #thumb-label="{ value }"> <template #thumb-label="{ value }">
@ -77,9 +167,6 @@
<script> <script>
export default { export default {
data: () => ({
list: ["x", "x"],
}),
computed: { computed: {
roundTweak: { roundTweak: {
get() { get() {
@ -89,6 +176,22 @@ export default {
this.$store.commit("tweaks/setRoundTweak", value); 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> </script>

View file

@ -1,5 +1,5 @@
<template> <template>
<div class="py-2"> <div>
<v-list-item v-for="(item, index) in commits" :key="index" class="my-1"> <v-list-item v-for="(item, index) in commits" :key="index" class="my-1">
<v-card <v-card
flat flat

View file

@ -1,19 +1,67 @@
<template> <template>
<div style="padding-top: 1em"> <div>
<v-list-item v-for="(item, index) in settingsItems" :key="index"> <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 <v-btn
text text
class="entry text-left text-capitalize" class="entry text-left text-capitalize"
:to="item.to" :to="item.to"
:disabled="item.disabled" :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" /> <v-icon size="30px" class="icon" v-text="item.icon" />
{{ item.name }} {{ item.name }}
</v-btn> </v-btn>
</v-list-item> </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
<v-btn text class="entry" @click="dev()" /> 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> </div>
</template> </template>
@ -22,10 +70,22 @@ export default {
data() { data() {
return { return {
devClicks: 0, devClicks: 0,
devmode: false,
devmodebuttonname: "Developer Mode",
settingsItems: [ settingsItems: [
{ name: "General", icon: "mdi-cog", to: "", disabled: true }, {
{ name: "Theme", icon: "mdi-brush-variant", to: "/mods/theme" }, name: "General",
icon: "mdi-cog",
to: "",
disabled: true,
},
{
name: "Theme",
icon: "mdi-brush-variant",
to: "/mods/theme",
},
{ {
name: "Player", name: "Player",
icon: "mdi-motion-play-outline", icon: "mdi-motion-play-outline",
@ -37,10 +97,15 @@ export default {
icon: "mdi-television-guide", icon: "mdi-television-guide",
to: "/mods/tweaks", to: "/mods/tweaks",
}, },
{ name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" }, {
name: "Startup Options",
icon: "mdi-restart",
to: "/mods/startup",
},
{ {
name: "Plugins", name: "Plugins",
icon: "mdi-puzzle", icon: "mdi-puzzle",
to: "",
to: "/mods/plugins", to: "/mods/plugins",
disabled: true, disabled: true,
}, },
@ -49,16 +114,41 @@ export default {
icon: "mdi-cloud-download-outline", icon: "mdi-cloud-download-outline",
to: "/mods/updates", to: "/mods/updates",
}, },
{ name: "Logs", icon: "mdi-text-box-outline", to: "/mods/logs" }, {
{ name: "About", icon: "mdi-information-outline", to: "/mods/about" }, 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: { methods: {
dev() { dev() {
this.devClicks++; this.devClicks++;
if (this.devClicks >= 6) { if (this.devClicks >= 6) {
this.$router.push("/mods/developer"); localStorage.setItem("devmode", "true");
this.devmode = true;
} }
}, },
}, },

View file

@ -1,23 +1,13 @@
<template> <template>
<div class="background" id="watch-body"> <div class="background" id="watch-body">
<div id="player-container"> <div id="player-container">
<v-btn text style="position: fixed; z-index: 69420" to="home"> <!-- // TODO: move component to default.vue -->
<v-icon>mdi-chevron-down</v-icon> <!-- // TODO: pass sources through vuex instead of props -->
</v-btn> <player
<!-- VueTube Player V1 --> v-if="sources.length > 0 && video.title && video.channelName"
<vuetubePlayer
:sources="sources"
v-if="useBetaPlayer === 'true' && sources.length > 0"
/>
<!-- Stock Player -->
<legacyPlayer
id="player"
ref="player" ref="player"
v-touch="{ down: () => $router.push('/home') }" :video="video"
class="background" :sources="sources"
:vid-src="vidSrc"
v-if="useBetaPlayer !== 'true'"
/> />
</div> </div>
@ -69,14 +59,18 @@
fab fab
class="vertical-button mx-1" class="vertical-button mx-1"
elevation="0" elevation="0"
style="width: 4.2rem !important; height: 4.2rem !important" style="
width: 4.2rem !important;
height: 4.2rem !important;
text-transform: none !important;
"
:disabled="item.disabled" :disabled="item.disabled"
@click="callMethodByName(item.actionName)" @click="callMethodByName(item.actionName)"
> >
<v-icon v-text="item.icon" /> <v-icon v-text="item.icon" />
<div <div
class="mt-2" class="mt-1"
style="font-size: 0.66rem" style="font-size: 0.6rem"
v-text="item.value || item.name" v-text="item.value || item.name"
/> />
</v-btn> </v-btn>
@ -108,14 +102,37 @@
</v-sheet> </v-sheet>
</v-bottom-sheet> --> </v-bottom-sheet> -->
</v-card> </v-card>
<v-divider />
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<!-- Channel Bar --> <!-- Channel Bar -->
<div v-if="loaded"> <div v-if="loaded">
<v-card <v-card
flat flat
class="channel-section background py-2 px-3 rounded-0" class="channel-section py-2 px-3 background"
:to="video.channelUrl" :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 id="details">
<div class="avatar-link mr-3"> <div class="avatar-link mr-3">
@ -136,9 +153,14 @@
subscribe subscribe
</div> </div>
</v-card> </v-card>
<v-divider />
</div> </div>
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<!-- Description --> <!-- Description -->
<div v-if="showMore"> <div v-if="showMore">
<div class="scroll-y ma-4"> <div class="scroll-y ma-4">
@ -146,12 +168,40 @@
:render="video.renderedData.description" :render="video.renderedData.description"
/> />
</div> </div>
<v-divider />
</div> </div>
<v-divider
v-if="
showMore &&
(!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch)
"
/>
<!-- Comments --> <!-- Comments -->
<div v-if="loaded && video.commentData" @click="toggleComment"> <div v-if="loaded && video.commentData" @click="toggleComment">
<v-card flat tile class="background comment-renderer px-3"> <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"> <v-card-text class="comment-count keep-spaces px-0">
<template v-for="text in video.commentData.headerText.runs"> <template v-for="text in video.commentData.headerText.runs">
<template v-if="text.bold"> <template v-if="text.bold">
@ -163,21 +213,26 @@
<v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon> <v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon>
<v-icon v-else dense>mdi-unfold-more-horizontal</v-icon> <v-icon v-else dense>mdi-unfold-more-horizontal</v-icon>
</v-card> </v-card>
<v-divider />
</div> </div>
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<swipeable-bottom-sheet <swipeable-bottom-sheet
v-if="loaded && video.commentData"
v-model="showComments" v-model="showComments"
hide-overlay hide-overlay
persistent persistent
no-click-animation no-click-animation
attach="#content-container" attach="#content-container"
v-if="loaded && video.commentData"
> >
<mainCommentRenderer <mainCommentRenderer
:defaultContinuation="video.commentContinuation"
:commentData="video.commentData"
v-model="showComments" v-model="showComments"
:comment-data="video.commentData"
:default-continuation="video.commentContinuation"
></mainCommentRenderer> ></mainCommentRenderer>
</swipeable-bottom-sheet> </swipeable-bottom-sheet>
@ -205,29 +260,27 @@
</template> </template>
<script> <script>
import player from "~/components/Player/index.vue";
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
import { getCpn } from "~/plugins/utils"; 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 vuetubePlayer from "~/components/Player/index.vue";
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue"; import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue"; import mainCommentRenderer from "~/components/Comments/mainCommentRenderer.vue";
import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet"; import SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
import backType from "~/plugins/classes/backType"; import backType from "~/plugins/classes/backType";
export default { export default {
components: { components: {
player,
ShelfRenderer, ShelfRenderer,
VidLoadRenderer, VidLoadRenderer,
SlimVideoDescriptionRenderer,
vuetubePlayer,
legacyPlayer,
ItemSectionRenderer, ItemSectionRenderer,
SwipeableBottomSheet,
mainCommentRenderer, mainCommentRenderer,
SwipeableBottomSheet,
SlimVideoDescriptionRenderer,
}, },
layout: "empty", layout: "empty",
// transition(to) { // TODO: fix layout switching // transition(to) { // TODO: fix layout switching
@ -247,7 +300,6 @@ export default {
// Exit fullscreen if currently in fullscreen // Exit fullscreen if currently in fullscreen
// if (this.$refs.player) this.$refs.player.webkitExitFullscreen(); // if (this.$refs.player) this.$refs.player.webkitExitFullscreen();
// Reset player and run getVideo function again // Reset player and run getVideo function again
// this.vidSrc = "";
// this.startTime = Math.floor(Date.now() / 1000); // this.startTime = Math.floor(Date.now() / 1000);
// this.getVideo(); // this.getVideo();
clearInterval(this.interval); clearInterval(this.interval);
@ -272,18 +324,10 @@ export default {
this.loaded = false; this.loaded = false;
this.$youtube.getVid(this.$route.query.v).then((result) => { this.$youtube.getVid(this.$route.query.v).then((result) => {
this.video = result; // TODO: add other resolutions as well
console.log("Video info data", result);
console.log(result.availableResolutions);
//--- VueTube Player v1 ---//
this.sources = result.availableResolutions; this.sources = result.availableResolutions;
console.log("Video info data", result);
//--- Legacy Player ---// this.video = result;
this.vidSrc =
result.availableResolutions[
result.availableResolutions.length - 1
].url; // Takes the highest available resolution with both video and Audio. Note this will be lower than the actual highest resolution
//--- Content Stuff ---// //--- Content Stuff ---//
this.likes = result.metadata.likes.toLocaleString(); this.likes = result.metadata.likes.toLocaleString();
@ -313,7 +357,6 @@ export default {
// using item.action in the v-for loop // using item.action in the v-for loop
this[name](); this[name]();
}, },
dislike() {},
async share() { async share() {
// this.share = !this.share; // this.share = !this.share;
await Share.share({ await Share.share({
@ -365,7 +408,8 @@ export default {
{ {
name: "Likes", name: "Likes",
icon: "mdi-thumb-up-outline", icon: "mdi-thumb-up-outline",
// action: null, // action: this.like(),
actionName: "like",
value: this.likes, value: this.likes,
disabled: true, disabled: true,
}, },
@ -384,17 +428,33 @@ export default {
actionName: "share", actionName: "share",
disabled: false, 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, showMore: false,
showComments: false, showComments: false,
// share: false, // share: false,
vidSrc: null,
sources: [], sources: [],
recommends: null, recommends: null,
loaded: false, loaded: false,
interval: null, interval: null,
video: null, video: null,
useBetaPlayer: false,
backHierarchy: [], backHierarchy: [],
}; };
}, },
@ -402,7 +462,6 @@ export default {
mountedInit() { mountedInit() {
this.startTime = Math.floor(Date.now() / 1000); this.startTime = Math.floor(Date.now() / 1000);
this.getVideo(); this.getVideo();
this.useBetaPlayer = localStorage.getItem("debug.BetaPlayer");
// Reset vertical scrolling // Reset vertical scrolling
const scrollableList = document.querySelectorAll(".overflow-y-auto"); const scrollableList = document.querySelectorAll(".overflow-y-auto");

12
NUXT/plugins/language.js Normal file
View file

@ -0,0 +1,12 @@
function module(text) {
const selectedLanguage = localStorage.getItem(text) || "english";
const languagePack = require('./languages/'+selectedLanguage);
if (!text) return languagePack;
return languagePack[text];
}
export default ({ app }, inject) => {
inject("lang", module);
};

View file

@ -0,0 +1,44 @@
module.exports = {
name: "English",
global: {
home: "Home",
subscriptions: "Subscriptions",
library: "Library"
},
index: {
connecting: "Connecting",
launching: "Launching"
},
settings: {
general: "General",
theme: "Theme",
player: "Player",
uitweaker: "UI Tweaker",
startupoptions: "Startup Options",
plugins: "Plugins",
updates: "Updates",
logs: "Logs",
about: "About",
devmode: "Registry Editor"
},
mods: {
about: {
appinformation: "App Information",
appversion: "App Version",
deviceinformation: "Device Information",
platform: "Platform",
os: "Operating System",
model: "Model",
manufacturer: "Manufacturer",
emulator: "Emulator",
github: "GitHub",
discord: "Discord"
}
}
}

View file

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

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 = () => ({ export const state = () => ({
roundTweak: 0, roundTweak: 0,
roundThumb: null,
roundWatch: null,
}); });
export const mutations = { export const mutations = {
initTweaks(state) { initTweaks(state) {
// NOTE: localStorage is not reactive, so it will only be used on first load // 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) { if (process.client) {
state.roundTweak = localStorage.getItem("roundTweak") || 0; state.roundTweak = JSON.parse(localStorage.getItem("roundTweak")) || 0;
state.roundThumb =
JSON.parse(localStorage.getItem("roundThumb")) === true;
state.roundWatch =
JSON.parse(localStorage.getItem("roundWatch")) === true;
} }
}, },
setRoundTweak(state, payload) { setRoundTweak(state, payload) {
@ -15,4 +21,12 @@ export const mutations = {
localStorage.setItem("roundTweak", payload); 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

@ -1,17 +1,17 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project version="4"> <project version="4">
<component name="deploymentTargetDropDown"> <component name="deploymentTargetDropDown">
<runningDeviceTargetSelectedWithDropDown> <targetSelectedWithDropDown>
<Target> <Target>
<type value="RUNNING_DEVICE_TARGET" /> <type value="QUICK_BOOT_TARGET" />
<deviceKey> <deviceKey>
<Key> <Key>
<type value="SERIAL_NUMBER" /> <type value="VIRTUAL_DEVICE_PATH" />
<value value="adb-97QAY11P1S-NELaqI._adb-tls-connect._tcp." /> <value value="$USER_HOME$/.android/avd/Pixel_3a_API_31_arm64-v8a.avd" />
</Key> </Key>
</deviceKey> </deviceKey>
</Target> </Target>
</runningDeviceTargetSelectedWithDropDown> </targetSelectedWithDropDown>
<timeTargetWasSelectedWithDropDown value="2022-05-05T23:23:16.786886Z" /> <timeTargetWasSelectedWithDropDown value="2022-05-14T02:50:17.689302Z" />
</component> </component>
</project> </project>

View file

@ -3,7 +3,7 @@ apply plugin: 'com.android.application'
android { android {
compileSdkVersion rootProject.ext.compileSdkVersion compileSdkVersion rootProject.ext.compileSdkVersion
defaultConfig { defaultConfig {
applicationId "com.Frontesque.youtube" applicationId "com.Frontesque.vuetube"
minSdkVersion rootProject.ext.minSdkVersion minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 1 versionCode 1

View file

@ -4,7 +4,7 @@
"type": "APK", "type": "APK",
"kind": "Directory" "kind": "Directory"
}, },
"applicationId": "com.Frontesque.youtube", "applicationId": "com.Frontesque.vuetube",
"variantName": "release", "variantName": "release",
"elements": [ "elements": [
{ {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 957 B

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 4.8 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 692 B

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.1 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.5 KiB

After

Width:  |  Height:  |  Size: 5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.8 KiB

After

Width:  |  Height:  |  Size: 7.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.9 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 32 KiB

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2 KiB

After

Width:  |  Height:  |  Size: 4.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 63 KiB

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 20 KiB

After

Width:  |  Height:  |  Size: 24 KiB

View file

@ -2,5 +2,9 @@
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" /> <access origin="*" />
<feature name="CDVOrientation">
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation"/>
</feature>
</widget> </widget>

View file

@ -0,0 +1,98 @@
/*
*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*
*/
package cordova.plugins.screenorientation;
import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.json.JSONArray;
import org.json.JSONException;
import android.app.Activity;
import android.content.pm.ActivityInfo;
import android.util.Log;
public class CDVOrientation extends CordovaPlugin {
private static final String TAG = "YoikScreenOrientation";
/**
* Screen Orientation Constants
*/
private static final String ANY = "any";
private static final String PORTRAIT_PRIMARY = "portrait-primary";
private static final String PORTRAIT_SECONDARY = "portrait-secondary";
private static final String LANDSCAPE_PRIMARY = "landscape-primary";
private static final String LANDSCAPE_SECONDARY = "landscape-secondary";
private static final String PORTRAIT = "portrait";
private static final String LANDSCAPE = "landscape";
@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
Log.d(TAG, "execute action: " + action);
// Route the Action
if (action.equals("screenOrientation")) {
return routeScreenOrientation(args, callbackContext);
}
// Action not found
callbackContext.error("action not recognised");
return false;
}
private boolean routeScreenOrientation(JSONArray args, CallbackContext callbackContext) {
String action = args.optString(0);
String orientation = args.optString(1);
Log.d(TAG, "Requested ScreenOrientation: " + orientation);
Activity activity = cordova.getActivity();
if (orientation.equals(ANY)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} else if (orientation.equals(LANDSCAPE_PRIMARY)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
} else if (orientation.equals(PORTRAIT_PRIMARY)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
} else if (orientation.equals(LANDSCAPE)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
} else if (orientation.equals(PORTRAIT)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
} else if (orientation.equals(LANDSCAPE_SECONDARY)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
} else if (orientation.equals(PORTRAIT_SECONDARY)) {
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
}
callbackContext.success();
return true;
}
}

View file

@ -2,5 +2,9 @@
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0"> <widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<access origin="*" /> <access origin="*" />
<feature name="CDVOrientation">
<param name="ios-package" value="CDVOrientation"/>
</feature>
</widget> </widget>

View file

@ -20,6 +20,7 @@ def capacitor_pods
pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar' pod 'CapacitorStatusBar', :path => '../../node_modules/@capacitor/status-bar'
pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast' pod 'CapacitorToast', :path => '../../node_modules/@capacitor/toast'
pod 'HugotomaziCapacitorNavigationBar', :path => '../../node_modules/@hugotomazi/capacitor-navigation-bar' pod 'HugotomaziCapacitorNavigationBar', :path => '../../node_modules/@hugotomazi/capacitor-navigation-bar'
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
end end
target 'App' do target 'App' do

View file

@ -14,6 +14,8 @@
"@capacitor/status-bar": "^1.0.8", "@capacitor/status-bar": "^1.0.8",
"@capacitor/toast": "^1.0.8", "@capacitor/toast": "^1.0.8",
"@hugotomazi/capacitor-navigation-bar": "^1.1.1", "@hugotomazi/capacitor-navigation-bar": "^1.1.1",
"cordova-plugin-screen-orientation": "^3.0.2",
"es6-promise-plugin": "^4.2.2",
"iconv-lite": "^0.6.3" "iconv-lite": "^0.6.3"
} }
} }

View file

@ -12,20 +12,21 @@ Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
</p> </p>
<p align="center"> <p align="center">
<a href="https://github.com/Frontesque/VueTube/commits/main"><img src="https://img.shields.io/github/commit-activity/m/Frontesque/VueTube?label=Commits" alt="Commits"></img></a> <a href="https://github.com/VueTubeApp/VueTube/commits/main"><img src="https://img.shields.io/github/commit-activity/m/VueTubeApp/VueTube?label=Commits" alt="Commits"></img></a>
<a href="https://github.com/Frontesque/VueTube/issues" alt="Issues"><img src="https://img.shields.io/github/issues/Frontesque/VueTube"></img></a> <a href="https://github.com/VueTubeApp/VueTube/issues" alt="Issues"><img src="https://img.shields.io/github/issues/VueTubeApp/VueTube"></img></a>
<a><img src="https://img.shields.io/github/languages/count/Frontesque/VueTube" alt="Languages"></img></a> <a><img src="https://img.shields.io/github/languages/count/VueTubeApp/VueTube" alt="Languages"></img></a>
<a href="https://github.com/Frontesque/VueTube/blob/main/LICENSE" alt="License"><img src="https://img.shields.io/github/license/Frontesque/VueTube"></img></a> <a href="https://github.com/VueTubeApp/VueTube/blob/main/LICENSE" alt="License"><img src="https://img.shields.io/github/license/VueTubeApp/VueTube"></img></a>
<a><img src="https://img.shields.io/github/stars/Frontesque/VueTube" alt="Stars"></img></a> <a><img src="https://img.shields.io/github/stars/VueTubeApp/VueTube" alt="Stars"></img></a>
<a><img src="https://img.shields.io/snyk/vulnerabilities/github/FrontEsque/VueTube" alt="Vulnerabilities"></img></a> <a><img src="https://img.shields.io/snyk/vulnerabilities/github/VueTubeApp/VueTube" alt="Vulnerabilities"></img></a>
<a><img src="https://img.shields.io/librariesio/github/Frontesque/VueTube" alt="Dependencies"></img></a> <a><img src="https://img.shields.io/librariesio/github/VueTubeApp/VueTube" alt="Dependencies"></img></a>
<a><img src="https://img.shields.io/tokei/lines/github/Frontesque/VueTube" alt="Lines"></img></a> <a><img src="https://img.shields.io/tokei/lines/github/VueTubeApp/VueTube" alt="Lines"></img></a>
<a href="https://github.com/Frontesque/VueTube/actions/workflows/ci.yml" alt="CI"><img src="https://github.com/Frontesque/VueTube/actions/workflows/ci.yml/badge.svg"></img></a> <a href="https://github.com/VueTubeApp/VueTube/actions/workflows/ci.yml" alt="CI"><img src="https://github.com/VueTubeApp/VueTube/actions/workflows/ci.yml/badge.svg"></img></a>
<a href="https://vuetube.app" alt="Website"><img src="https://img.shields.io/website?down_message=offline&up_message=online&url=https%3A%2F%2Fvuetube.app"></img></a> <a href="https://vuetube.app" alt="Website"><img src="https://img.shields.io/website?down_message=offline&up_message=online&url=https%3A%2F%2Fvuetube.app"></img></a>
<a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a> <a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a>
<a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a> <a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a>
<a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a> <a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
<a href="https://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a> <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 ## Features
@ -50,11 +51,11 @@ To install please visit www.vuetube.app/install
## Screenshots ## Screenshots
View on our website: [https://vuetube.app/info/screenshots](https://vuetube.app/info/screenshots) View on our website: www.vuetube.app/info/screenshots
### Technologies used ### Technologies used
<a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368956760074/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a> <a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/973719873467342908/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
### Why am I doing this? ### Why am I doing this?
@ -62,12 +63,12 @@ Well this has been thrown around on the Return Youtube Dislike discord server fo
### Want to contribute? ### Want to contribute?
Please read our website on how to do so: https://vuetube.app/contributing Please read our website on how to do so: www.vuetube.app/contributing
## Contributors ## Contributors
<a href="https://github.com/Frontesque/VueTube/graphs/contributors"> <a href="https://github.com/VueTubeApp/VueTube/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Frontesque/VueTube" /> <img src="https://contrib.rocks/image?repo=VueTubeApp/VueTube" />
</a> </a>
<sub>Made with [contrib.rocks](https://contrib.rocks). </sub> <sub>Made with [contrib.rocks](https://contrib.rocks). </sub>
@ -78,6 +79,7 @@ Please read our website on how to do so: https://vuetube.app/contributing
- VueTube Logo by [@afnzmn](https://github.com/afnzmn) - VueTube Logo by [@afnzmn](https://github.com/afnzmn)
## Disclamer ## 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). 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. Any trademark, service mark, trade name, or other intellectual property rights used in the VueTube project are owned by the respective owners.