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

View File

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

View File

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

View File

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

View File

@ -1,15 +1,28 @@
<template>
<v-card
class="entry gridVideoRenderer background"
:to="`/watch?v=${video.videoId}`"
flat
to="/channel"
class="entry gridVideoRenderer background"
:class="
roundThumb && roundTweak > 0
? $vuetify.theme.dark
? 'lighten-1'
: 'darken-1'
: ''
"
:style="{
borderRadius: roundThumb ? `${roundTweak / 2}rem` : '0',
margin:
roundThumb && roundTweak > 0 ? '0 1rem 1rem 1rem' : '0 0 0.25rem 0',
}"
@click="$store.dispatch('channel/fetchChannel', video.channelId)"
>
<div id="details">
<div id="details" class="pa-4">
<a
:href="
this.$rendererUtils.getNavigationEndpoints(video.navigationEndpoint)
"
class="avatar-link pt-2"
class="avatar-link"
>
<v-img
class="avatar-thumbnail"
@ -19,12 +32,11 @@
"
/>
</a>
<v-card-text class="video-info pt-2" v-emoji>
<v-card-text class="video-info py-0" v-emoji>
<div
v-for="title in video.title.runs"
:key="title.text"
style="margin-top: 0.5em"
class="vid-title"
class="vid-title mt-1"
>
{{ title.text }}
</div>
@ -35,10 +47,43 @@
v-text="parseBottom(video)"
/>
</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>
</v-card>
</template>
<script>
export default {
props: ["video"],
computed: {
roundTweak() {
return this.$store.state.tweaks.roundTweak;
},
roundThumb() {
return this.$store.state.tweaks.roundThumb;
},
},
methods: {
parseBottom(video) {
const bottomText = [
video.subscriberCountText?.runs[0].text,
video.videoCountText?.runs.map((run) => run.text).join(" "),
];
return bottomText.join(" · ");
},
},
};
</script>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
@ -53,8 +98,6 @@
}
.avatar-thumbnail {
margin-top: 0.5rem;
margin-left: 0.5rem;
border-radius: 50%;
width: 50px;
height: 50px;
@ -64,7 +107,6 @@
display: flex;
flex-direction: row;
flex-basis: auto;
padding: 10px;
}
@media screen and (orientation: landscape) {
@ -76,19 +118,3 @@
}
}
</style>
<script>
export default {
props: ["video"],
methods: {
parseBottom(video) {
const bottomText = [
video.subscriberCountText?.runs[0].text,
video.videoCountText?.runs.map((run) => run.text).join(" "),
];
return bottomText.join(" · ");
},
},
};
</script>

View File

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

View File

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

View File

@ -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>
<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
ref="player"
autoplay
:src="vidSrc"
width="100%"
style="max-height: 50vh; display: block"
@webkitfullscreenchange="handleFullscreenChange"
:height="isFullscreen ? '100%' : 'auto'"
: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" />
<controls v-if="$refs.player" :video="$refs.player" />
<!-- // TODO: merge the bottom 2 into 1 reusable component -->
<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>
</template>
<script>
import seekbar from '~/components/Player/seekbar.vue';
import controls from '~/components/Player/controls.vue';
import loop from "~/components/Player/loop.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 {
props: ["sources"],
components: {
fullscreen,
watchtime,
playpause,
captions,
minimize,
quality,
seekbar,
controls
speed,
close,
loop,
},
props: {
sources: {
type: Array,
required: true,
},
video: {
type: Object,
required: true,
},
},
data() {
return {
isFullscreen: false,
controls: false,
seeking: false,
contain: true,
vidSrc: "",
};
},
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: {
handleFullscreenChange() {
if (document.fullscreenElement === this.$refs.player) {
this.$vuetube.statusBar.hide();
this.$vuetube.navigationBar.hide();
if (document?.fullscreenElement === this.$refs.vidcontainer) {
this.exitFullscreen();
} else {
this.$vuetube.statusBar.show();
this.$vuetube.navigationBar.show();
this.openFullscreen();
}
},
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() {
return this.$refs.player;
},
},
};
</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>
<div>
<video
ref="playerfake"
muted
autoplay
style="display: none"
:src="vidWrs"
/>
<v-progress-linear
query
active
background-color="primary"
style="width: 100%; background: #ffffff22"
background-opacity="0.5"
background-color="primary"
:buffer-value="buffered"
:value="percent"
color="primary"
height="3"
query
:value="percentage"
:style="
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>
</template>
<script>
export default {
props: ["video"],
props: ["sources", "video", "controls", "fullscreen"],
data() {
return {
percentage: 0,
buffered: 0
}
},
mounted() {
this.video.ontimeupdate = () => {
this.percentage = (this.video.currentTime / this.video.duration) * 100;
scrubbing: false,
percent: 0,
progress: 0,
buffered: 0,
duration: 0,
vidSrc: "",
vidWrs: "",
};
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>

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>
<div
v-if="
render.separatorDetails && render.separatorDetails.hasBottomSeparator
render.separatorDetails &&
render.separatorDetails.hasBottomSeparator &&
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0)
"
class="separator-bottom background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"

View File

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

View File

@ -1,13 +1,11 @@
<template>
<v-card
class="entry videoRenderer background"
class="entry videoRenderer background overflow-hidden"
:to="`/watch?v=${vidId}`"
:style="{
borderRadius: `${roundTweak / 2.5}rem`,
borderRadius: roundThumb ? `${roundTweak / 2}rem` : '0',
margin:
$store.state.tweaks.roundTweak > 0
? '0 1rem 1rem 1rem'
: '0 0 0.25rem 0',
roundThumb && roundTweak > 0 ? '0 1rem 1rem 1rem' : '0 0 0.25rem 0',
}"
flat
>
@ -16,7 +14,7 @@
:aspect-ratio="16 / 9"
:src="$youtube.getThumbnail(vidId, 'max', thumbnails)"
:style="{
borderRadius: `${roundTweak / 2.5}rem`,
borderRadius: roundThumb ? `${roundTweak / 12}rem` : '0',
}"
/>
<div
@ -27,8 +25,27 @@
v-text="thumbnailOverlayText"
/>
</div>
<div id="details">
<a :href="channelUrl" class="avatar-link pl-2 pt-2">
<div
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" />
</a>
<v-card-text class="video-info pt-2" v-emoji>
@ -42,8 +59,8 @@
</span>
<div
class="background--text text--lighten-5 caption"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
class="background--text caption"
:class="$vuetify.theme.dark ? 'text--lighten-5' : 'text--darken-4'"
v-text="bottomText"
/>
</v-card-text>
@ -51,6 +68,52 @@
</v-card>
</template>
<script>
export default {
props: {
vidId: {
type: String,
required: true,
},
thumbnails: {
type: Array,
required: true,
},
channelUrl: {
type: String,
required: true,
},
channelIcon: {
type: String,
required: true,
},
titles: {
type: Array,
required: true,
},
bottomText: {
type: String,
required: true,
},
thumbnailOverlayText: {
type: String,
},
thumbnailOverlayStyle: {
type: String,
},
},
computed: {
roundTweak() {
return this.$store.state.tweaks.roundTweak;
},
roundThumb() {
return this.$store.state.tweaks.roundThumb;
},
},
};
</script>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
@ -59,8 +122,9 @@
position: absolute;
bottom: 10px;
right: 10px;
border-radius: 5px;
border-radius: 4px;
padding: 0px 4px 0px 4px;
font-size: 0.66rem;
}
.videoRuntimeFloat.style-DEFAULT {
@ -119,46 +183,3 @@
}
}
</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>
<!-- hide-on-scroll -->
<v-bottom-navigation
v-model="tabSelection"
shift
class="bottomNav py-4 background"
:style="
$vuetify.theme.dark
? 'border-top: 1px solid var(--v-background-lighten1) !important;'
: 'border-top: 1px solid var(--v-background-darken1) !important;'
"
>
<v-btn
v-for="(item, i) in tabs"
:key="i"
v-ripple="false"
class="navButton"
:to="item.link"
plain
<div class="bottomNav background">
<v-divider v-if="!$store.state.tweaks.roundTweak" />
<v-bottom-navigation
v-model="tabSelection"
style="padding: 0 !important; box-shadow: none !important"
class="transparent"
shift
>
<v-btn
v-for="(item, i) in tabs"
:key="i"
v-ripple="false"
class="navButton"
:to="item.link"
plain
>
<span v-text="item.name" />
<v-icon
:color="
tabSelection == i
? 'primary'
: $vuetify.theme.dark
? 'background lighten-4'
: 'background darken-4'
"
:class="
tabSelection == i
? $vuetify.theme.dark
? 'tab primary darken-4'
: 'tab primary lighten-4'
: ''
"
v-text="item.icon"
/>
<!--
<span v-text="item.name" />
<v-icon
:color="
@ -35,18 +51,20 @@
: ''
"
v-text="item.icon"
/>
<!--
Add the following to 'v-text- above to make the icons outlined unless active
/> -->
<!-- Add the following to 'v-text- above to make the icons outlined unless active
+ (tabSelection == i ? '' : '-outline')
-->
</v-btn>
<!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()"
><v-icon>mdi-magnify</v-icon></v-btn
> -->
</v-bottom-navigation>
</v-btn>
<!-- <v-btn
text
class="navButton mr-2 fill-height"
color="white"
@click="searchBtn()"
><v-icon>mdi-magnify</v-icon></v-btn
> -->
</v-bottom-navigation>
</div>
</template>
<script>
@ -56,31 +74,37 @@ export default {
tabSelection: 0,
tabs: [
// 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: "Upload", icon: "mdi-plus", link: "/upload" },
{
name: "Subscriptions",
name: "...",
icon: "mdi-youtube-subscription",
link: "/subscriptions",
},
{ name: "Library", icon: "mdi-view-list", link: "/library" },
{ name: "...", icon: "mdi-view-list", link: "/library" },
// { 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>
<style scoped>
.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;
/* ios gesture nav */
bottom: env(safe-area-inset-bottom) !important;
height: 4rem !important;
padding: 0 !important;
position: fixed;
width: 100%;
bottom: 0;
}
.navButton {
width: 25vw !important;

View File

@ -0,0 +1,71 @@
<template>
<v-card
v-ripple
flat
class="background"
:class="
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
? $vuetify.theme.dark
? 'lighten-1'
: 'darken-1'
: ''
"
:style="{
borderRadius: $store.state.tweaks.roundThumb
? `${$store.state.tweaks.roundTweak / 2}rem`
: '0',
margin:
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
? '0 1rem 1rem 1rem'
: '0 0 0.25rem 0',
}"
>
<div class="d-flex flex-row pa-4">
<v-avatar size="50" color="primary" />
<div class="d-flex flex-column my-auto pl-4">
<b>Work in progress</b>
<div
class="background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
69 years ago
</div>
</div>
<v-spacer />
<v-btn
fab
text
elevation="0"
style="width: 50px !important; height: 50px !important"
>
<v-icon>mdi-share-outline</v-icon>
</v-btn>
</div>
<p class="px-4" v-emoji>Blurb Blurb Text Goes Here ...</p>
<v-img
contain
class="background my-4"
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
style="max-height: 15rem"
:style="{
borderRadius: `${$store.state.tweaks.roundTweak / 4}rem`,
marginLeft:
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak) >
0
? '1rem'
: '0',
marginRight:
!($store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak) >
0
? '1rem'
: '0',
}"
src="/dev.svg"
/>
<div class="d-flex flex-row px-4 pb-4">
<v-icon>mdi-thumb-up-outline</v-icon>
<b class="mx-2">123</b>
<v-icon class="ml-2">mdi-thumb-down-outline</v-icon>
</div>
</v-card>
</template>

View File

@ -1,9 +1,9 @@
<template>
<v-card class="dialog-base">
<v-expand-transition>
<slot name="reveal"></slot>
</v-expand-transition>
<div class="toolbar-container">
<v-card flat class="dialog-base background">
<div
class="toolbar-container d-flex flex-column background"
style="flex-direction: column !important"
>
<v-toolbar color="background" flat>
<slot name="header"></slot>
</v-toolbar>
@ -12,6 +12,9 @@
<div class="dialog-body background">
<slot></slot>
</div>
<v-expand-transition>
<slot name="reveal"></slot>
</v-expand-transition>
</v-card>
</template>

View File

@ -0,0 +1,80 @@
<template>
<v-card
v-ripple
class="background d-flex flex-row overflow-hidden mb-4 mx-4"
style="height: 6rem !important"
:class="
$store.state.tweaks.roundThumb && $store.state.tweaks.roundTweak > 0
? $vuetify.theme.dark
? 'lighten-1'
: 'darken-1'
: ''
"
:style="{
borderRadius: $store.state.tweaks.roundThumb
? `${$store.state.tweaks.roundTweak / 2}rem`
: '0',
}"
flat
>
<v-img
contain
src="/dev.svg"
class="background"
style="position: relative; max-width: 8rem !important"
:class="$vuetify.theme.dark ? 'lighten-3' : 'darken-3'"
:style="{
borderRadius: $store.state.tweaks.roundThumb
? `${$store.state.tweaks.roundTweak / 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>
<v-card
scroll-off-screen
style="height: 4rem !important; display: flex"
class="rounded-0 pa-3 topNav background"
>
<!-- :style="
$vuetify.theme.dark
? 'border-bottom: 1px solid var(--v-background-lighten1) !important;'
: 'border-bottom: 1px solid var(--v-background-darken1) !important;'
" -->
<h3 v-show="!search" class="my-auto ml-4" v-text="page" />
<v-card style="display: flex" class="rounded-0 pa-3 topNav background">
<!-- opacity with vue 😉 -->
<!-- :style="{ background: $vuetify.theme.currentTheme.primary + '55' }" -->
<h3
v-show="!search"
class="my-auto ml-4"
v-text="
$route.path.includes('channel') ? $store.state.channel.title : page
"
/>
<v-btn
v-if="search"
@ -26,8 +25,9 @@
solo
dense
flat
autofocus
label="Search"
style="margin-top: 1px"
style="margin-top: 7px"
:background-color="
$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'
"
@ -49,6 +49,7 @@
<v-icon>mdi-refresh</v-icon>
</v-btn>
<v-btn
v-if="$route.name !== 'settings' && !$route.path.includes('/mods')"
icon
tile
class="ml-3 my-auto fill-height"
@ -82,7 +83,7 @@ export default {
default: "Home",
},
},
events: ["searchBtn", "textChanged", "closeSearch"],
events: ["searchBtn", "textChanged", "closeSearch", "scrollToTop"],
data: () => ({
text: "",
}),
@ -113,12 +114,15 @@ export default {
<style scoped>
.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;
/* ios notch */
top: env(safe-area-inset-top) !important;
position: fixed;
width: 100%;
top: 0;
}
.topNavSearch {

View File

@ -4,13 +4,15 @@
:search="search"
:page="page"
style="z-index: 696969"
@close-search="search = !search"
@search-btn="searchBtn"
@text-changed="textChanged"
@close-search="search = !search"
@scroll-to-top="$refs.pgscroll.scrollTop = 0"
/>
<!-- channel-tabs -->
<v-tabs
v-if="$route.path.includes('/channel')"
v-if="$route.path.includes('/channel') && !search"
mobile-breakpoint="0"
style="
position: fixed;
@ -24,6 +26,7 @@
v-for="tab in channelTabs"
:key="tab.name"
:to="tab.to"
exact
:v-ripple="false"
>
{{ tab.name }}
@ -31,9 +34,15 @@
</v-tabs>
<div
style="height: 100%; padding-bottom: 4rem"
style="
height: 100%;
padding-bottom: calc(4rem + env(safe-area-inset-bottom));
"
: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">
@ -53,21 +62,17 @@
>
<div class="scroll-y" style="height: 100%">
<div v-if="search" style="min-width: 180px">
<v-list-item
v-for="(item, index) in response"
:key="index"
class="px-0"
>
<v-list-item v-for="item in response" :key="item[0]" class="px-0">
<v-btn
v-emoji
text
tile
dense
class="searchButton text-left text-none"
@click="youtubeSearch(item)"
v-emoji
>
<v-icon class="mr-5">mdi-magnify</v-icon>
{{ item[0] || item.text }}
{{ item[0] }}
</v-btn>
</v-list-item>
</div>
@ -154,28 +159,28 @@ export default {
return;
} // No text found, no point in calling API
//--- User Pastes Link, Direct Them To Video ---//
const isLink = linkParser(text);
if (isLink) {
this.response = [
{
text: `Watch Video from ID: ${isLink.searchParams.get("v")}`,
id: isLink.searchParams.get("v"),
},
];
return;
}
//--- End User Pastes Link, Direct Them To Video ---//
//--- Auto Suggest ---//
this.$youtube.autoComplete(text, (res) => {
const data = res.replace(/^.*?\(/, "").replace(/\)$/, ""); //Format Response
this.response = JSON.parse(data)[1];
});
//--- User Pastes Link, Direct Them To Video ---//
const isLink = linkParser(text);
if (isLink) {
this.response = [
`Watch Video from ID: ${isLink.searchParams.get("v")}`,
{ id: isLink.searchParams.get("v") },
];
return;
}
//--- End User Pastes Link, Direct Them To Video ---//
},
youtubeSearch(item) {
const link = item.id ? `/watch?v=${item.id}` : `/search?q=${item[0]}`;
const link = item[1].id
? `/watch?v=${item[1].id}` // link pasted
: `/search?q=${item[0]}`; // regular suggestion
this.$router.push(link);
this.search = false;
},
@ -226,11 +231,39 @@ export default {
.v-slide-group__next {
display: none !important;
}
.v-input--selection-controls__input {
margin-right: 0 !important;
}
.v-input__slot {
margin: 0 !important;
}
.v-slider {
margin: 0 !important;
}
.border-primary {
border: 2px solid var(--v-primary-base) !important;
}
.glassy {
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
}
.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 {
overflow: hidden;
@ -248,7 +281,9 @@ export default {
html,
body {
background: var(--v-background-base);
/* overflow-x: hidden; */
-webkit-overflow-scrolling: touch !important;
overflow-y: scroll !important;
overflow-x: hidden !important;
}
p,

View File

@ -17,6 +17,7 @@ export default {
{ src: "~/plugins/vuetube", mode: "client" },
{ src: "~/plugins/ryd", mode: "client" },
{ src: "~/plugins/thirdPartyPluginLoader", mode: "client" },
{ src: "~/plugins/language", mode: "client" },
],
generate: {
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>
<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>

View File

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

View File

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

View File

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

View File

@ -18,9 +18,11 @@ export default {
layout: "empty",
data: () => ({
progressMsg: "Connecting",
progressMsg: "...",
}),
async mounted() {
this.progressMsg = this.$lang("index").connecting;
this.$store.commit("tweaks/initTweaks");
const theming = new Promise((resolve) =>
// 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.dark
);
// this.$vuetube.statusBar.setTransparent();
// this.$vuetube.navigationBar.setTransparent();
// this.$vuetube.statusBar.setTransparent();
resolve();
}, 0)
);
await theming;
await this.$youtube.getAPI();
this.progressMsg = "Launching";
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
},

View File

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

View File

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

View File

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

View File

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

View File

@ -1,10 +1,13 @@
<template>
<client-only>
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_()_/¯ -->
<div
class="d-flex flex-column justify-end"
style="min-height: calc(100vh - 8rem)"
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">
<div
class="d-flex flex-row px-6 no-wrap"
@ -43,16 +46,28 @@
? '2px solid var(--v-primary-darken4)'
: '2px solid var(--v-primary-lighten4)',
}"
class="py-4 px-4 ma-2 rounded-lg"
:value="
$vuetify.theme.dark ? experimentalDark : experimentalLight
"
class="pa-4 ma-2 rounded-lg"
:value="$vuetify.theme.dark ? adaptiveDark : adaptiveLight"
/>
Adaptive
</div>
<div class="text-center">
<!-- Custom Background -->
<v-btn
icon
class="ma-2 rounded-lg background border-primary"
style="height: 3.75rem; width: 3.75rem"
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
@click="(pickerState = true), (pickerMode = 'background')"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
<br />
Custom
</div>
</div>
</v-radio-group>
<!-- ----------------------------------------------Primary Colors------------------------ -->
<!-- ----Primary Colors---- -->
<v-radio-group v-model="$vuetify.theme.currentTheme.primary" class="mx-2">
<div
class="d-flex flex-row px-6 py-2 no-wrap align-center"
@ -74,40 +89,53 @@
class="mr-2 my-auto rounded-xl"
:value="color"
/>
<v-dialog
v-model="dialog"
width="300"
content-class="background rounded-lg"
<!-- Custom Primary -->
<v-btn
icon
class="background"
style="height: 1.75rem; width: 1.75rem"
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
v-bind="attrs"
v-on="on"
@click="(pickerState = true), (pickerMode = 'primary')"
>
<template #activator="{ on, attrs }">
<v-btn
icon
class="background"
style="height: 1.75rem; width: 1.75rem"
:class="$vuetify.theme.dark ? 'lighten-2' : 'darken-2'"
v-bind="attrs"
v-on="on"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</template>
<v-color-picker
v-model="$vuetify.theme.currentTheme.primary"
style="min-width: 100%"
class="background"
hide-mode-switch
dot-size="50"
mode="hexa"
flat
/>
</v-dialog>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
</v-radio-group>
<!-- ----------------------------------------------Mode Switch------------------------ -->
<!-- ----Color Picker---- -->
<v-dialog
v-model="pickerState"
width="300"
content-class="background rounded-lg"
>
<v-color-picker
v-model="$vuetify.theme.currentTheme[pickerMode]"
style="min-width: 100%"
class="background"
hide-mode-switch
dot-size="50"
mode="hexa"
flat
/>
</v-dialog>
<!-- ----Mode Switch---- -->
<v-divider v-if="!$store.state.tweaks.roundTweak" />
<v-card
flat
class="d-flex flex-row justify-between mx-8 mb-8 px-4 background rounded-lg"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
class="d-flex flex-row justify-between mx-8 mb-8 px-4 py-3 background"
:class="
$store.state.tweaks.roundTweak > 0
? $vuetify.theme.dark
? 'lighten-1'
: 'darken-1'
: ''
"
:style="{
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
padding: !$store.state.tweaks.roundTweak ? '2rem !important' : '',
margin: !$store.state.tweaks.roundTweak ? '0 !important' : '',
}"
@click="
($vuetify.theme.dark = !$vuetify.theme.dark),
$vuetube.haptics.hapticsImpactLight(1)
@ -127,6 +155,7 @@
<v-switch
v-model="$vuetify.theme.dark"
style="pointer-events: none"
class="mt-2"
persistent-hint
inset
/>
@ -153,9 +182,10 @@ export default {
{ name: "Black", color: "#000000" },
],
backgroundsLight: [{ name: "Normal", color: "#ffffff" }],
experimentalLight: "",
experimentalDark: "",
dialog: false,
adaptiveLight: "",
adaptiveDark: "",
pickerState: false,
pickerMode: "bg",
};
},
watch: {
@ -169,19 +199,30 @@ export default {
: localStorage.setItem("backgroundLight", value);
this.$vuetube.statusBar.setTheme(value, this.$vuetify.theme.dark);
this.$vuetube.navigationBar.setTheme(value, !this.$vuetify.theme.dark);
// WIP: luma-based light-dark auto-switching
// let bg = this.$vuetify.theme.currentTheme.background;
// console.log(this.$vuetube.hexToRgb(bg));
// let luma =
// 0.2126 * this.$vuetube.hexToRgb(bg).r +
// 0.7152 * this.$vuetube.hexToRgb(bg).g +
// 0.0722 * this.$vuetube.hexToRgb(bg).b;
// if (luma < 40) {
// this.$vuetify.theme.dark = true;
// this.vuetify.theme.currentTheme.background = bg;
// }
},
"$vuetify.theme.currentTheme.primary"(value) {
if (value != undefined) {
this.$vuetify.theme.dark
? localStorage.setItem("primaryDark", value)
: localStorage.setItem("primaryLight", value);
let tempD = this.experimentalDark;
let tempL = this.experimentalLight;
let tempD = this.adaptiveDark;
let tempL = this.adaptiveLight;
this.adapt();
if (this.$vuetify.theme.currentTheme.background === tempD)
this.$vuetify.theme.currentTheme.background = this.experimentalDark;
this.$vuetify.theme.currentTheme.background = this.adaptiveDark;
if (this.$vuetify.theme.currentTheme.background === tempL)
this.$vuetify.theme.currentTheme.background = this.experimentalLight;
this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
}
},
},
@ -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 SPACE " " is stored as part of the CSS variable itself to be used for chaining
this.experimentalDark = hexD.substring(1).toUpperCase();
this.experimentalLight = hexL.substring(1).toUpperCase();
this.adaptiveDark = hexD.substring(1).toUpperCase();
this.adaptiveLight = hexL.substring(1).toUpperCase();
setTimeout(() => {
if (
this.$vuetify.theme.currentTheme.background ==
hexD.substring(1).toUpperCase()
)
this.$vuetify.theme.currentTheme.background = this.experimentalDark;
this.$vuetify.theme.currentTheme.background = this.adaptiveDark;
if (
this.$vuetify.theme.currentTheme.background ==
hexL.substring(1).toUpperCase()
)
this.$vuetify.theme.currentTheme.background = this.experimentalLight;
this.$vuetify.theme.currentTheme.background = this.adaptiveLight;
}, 0);
},
},
};
</script>
<style>
.border-primary {
border: 2px solid var(--v-primary-base) !important;
}
.v-input--selection-controls__input {
margin-right: 0 !important;
}
</style>

View File

@ -1,67 +1,157 @@
<template>
<!-- !IMPORTANT: don't let autoformatter format this style to multiline or else it breaks ¯\_()_/¯ -->
<div
class="d-flex flex-column justify-end"
style="min-height: calc(100vh - 8rem)"
style="
min-height: calc(100vh - 8rem - env(safe-area-inset-top) - env(safe-area-inset-bottom)) !important;
"
>
<!-- TODO: outer radius -->
<!-- TODO: Dense Navbar -->
<!-- TODO: Disable Top Bar -->
<!-- TODO: Top and Bottom bar color selection -->
<v-card
flat
class="mb-4 background"
class="mx-4 my-2 px-4 py-2 d-flex flex-row justify-between background"
style="transition-duration: 0.3s; transition-property: border-radius"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 2}rem`,
}"
>
<h3
class="my-auto background--text"
:class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
>
Fullscreen (Soon)
</h3>
<v-spacer />
<v-switch
disabled
class="mt-2"
style="pointer-events: none"
persistent-hint
inset
/>
</v-card>
<v-divider v-if="!roundTweak" />
<v-card
flat
class="mx-4 my-2 px-4 py-2 d-flex flex-row justify-between background"
style="transition-duration: 0.3s; transition-property: border-radius"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 2}rem`,
}"
>
<h3
class="my-auto background--text"
:class="$vuetify.theme.dark ? 'text--lighten-3' : 'text--darken-3'"
>
Navbar Blur (Soon)
</h3>
<v-spacer />
<v-switch
disabled
class="mt-2"
style="pointer-events: none"
persistent-hint
inset
/>
</v-card>
<v-divider v-if="!roundTweak" />
<h3 class="ml-8 mt-8">Rounded Corners</h3>
<v-card
flat
class="mx-4 my-2 background"
style="
transition-duration: 0.3s;
transition-property: border-radius;
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="{
borderRadius: `${roundTweak / 2}rem`,
}"
>
<!-- margin: $store.state.tweaks.roundTweak > 0 ? '0 1rem' : '0', -->
<h3 class="mt-5">Rounded Corners</h3>
<div
class="background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
<v-card
flat
class="mb-1 px-4 py-2 d-flex flex-row justify-between background"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 12}rem`,
}"
@click="
(roundThumb = !roundThumb), $vuetube.haptics.hapticsImpactLight(1)
"
>
applies to only a few elements for now
</div>
<!-- TODO: outer radius -->
<!-- TODO: Dense Navbar -->
<!-- TODO: Disable Top Bar -->
<!-- TODO: Top and Bottom bar color selection -->
<div
class="my-auto background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
Round Thumbnails
</div>
<v-spacer />
<v-switch
v-model="roundThumb"
style="pointer-events: none"
persistent-hint
class="mt-2"
inset
/>
</v-card>
<v-card
flat
class="mb-1 px-4 py-2 d-flex flex-row justify-between background"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 12}rem`,
}"
@click="
(roundWatch = !roundWatch), $vuetube.haptics.hapticsImpactLight(1)
"
>
<div
class="my-auto background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
>
Round Watch Page Components
</div>
<v-spacer />
<v-switch
v-model="roundWatch"
style="pointer-events: none"
persistent-hint
class="mt-2"
inset
/>
</v-card>
<v-slider
v-model="roundTweak"
class="mr-2 mt-5"
label="Inner"
class="pr-4 pl-4 pt-4 pb-1 background"
:max="4"
step="1"
label="Radius"
step=".25"
thumb-size="64"
:class="
roundTweak > 0 ? ($vuetify.theme.dark ? 'lighten-1' : 'darken-1') : ''
"
:style="{
borderRadius: `${roundTweak / 12}rem`,
}"
@input="$vuetube.haptics.hapticsImpactLight(0)"
>
<template #thumb-label="{ value }">
@ -77,9 +167,6 @@
<script>
export default {
data: () => ({
list: ["x", "x"],
}),
computed: {
roundTweak: {
get() {
@ -89,6 +176,22 @@ export default {
this.$store.commit("tweaks/setRoundTweak", value);
},
},
roundThumb: {
get() {
return this.$store.state.tweaks.roundThumb;
},
set(value) {
this.$store.commit("tweaks/setRoundThumb", value);
},
},
roundWatch: {
get() {
return this.$store.state.tweaks.roundWatch;
},
set(value) {
this.$store.commit("tweaks/setRoundWatch", value);
},
},
},
};
</script>

View File

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

View File

@ -1,19 +1,67 @@
<template>
<div style="padding-top: 1em">
<v-list-item v-for="(item, index) in settingsItems" :key="index">
<div>
<v-list-item
v-for="(item, index) in settingsItems"
:key="index"
:style="{
padding:
$store.state.tweaks.roundTweak > 0
? '0 1rem !important'
: '0rem !important',
}"
>
<v-btn
text
class="entry text-left text-capitalize"
:to="item.to"
:disabled="item.disabled"
:style="{
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
paddingLeft:
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
}"
>
<v-icon size="30px" class="icon" v-text="item.icon" />
{{ item.name }}
</v-btn>
</v-list-item>
<v-list-item
:style="{
padding:
$store.state.tweaks.roundTweak > 0
? '0 1rem !important'
: '0rem !important',
}"
>
<!-- Dev Mode Open -->
<v-btn
v-if="!devmode"
text
class="entry"
:style="{
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
paddingLeft:
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
}"
@click="dev()"
/>
<!-- Dev Mode Open -->
<v-btn text class="entry" @click="dev()" />
<v-btn
v-if="devmode"
text
class="entry text-left text-capitalize"
to="/mods/developer"
:style="{
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
paddingLeft:
$store.state.tweaks.roundTweak > 0 ? '' : '1.5rem !important',
}"
>
<v-icon size="30px" class="icon">mdi-database-edit</v-icon>
{{ devmodebuttonname }}
</v-btn>
</v-list-item>
<!-- End Dev Mode -->
</div>
</template>
@ -22,10 +70,22 @@ export default {
data() {
return {
devClicks: 0,
devmode: false,
devmodebuttonname: "Developer Mode",
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",
icon: "mdi-motion-play-outline",
@ -37,10 +97,15 @@ export default {
icon: "mdi-television-guide",
to: "/mods/tweaks",
},
{ name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" },
{
name: "Startup Options",
icon: "mdi-restart",
to: "/mods/startup",
},
{
name: "Plugins",
icon: "mdi-puzzle",
to: "",
to: "/mods/plugins",
disabled: true,
},
@ -49,16 +114,41 @@ export default {
icon: "mdi-cloud-download-outline",
to: "/mods/updates",
},
{ name: "Logs", icon: "mdi-text-box-outline", to: "/mods/logs" },
{ name: "About", icon: "mdi-information-outline", to: "/mods/about" },
{
name: "Logs",
icon: "mdi-text-box-outline",
to: "/mods/logs",
},
{
name: "About",
icon: "mdi-information-outline",
to: "/mods/about",
},
],
};
},
mounted() {
this.settingsItems[0].name = this.$lang("settings").general;
this.settingsItems[1].name = this.$lang("settings").theme;
this.settingsItems[2].name = this.$lang("settings").player;
this.settingsItems[3].name = this.$lang("settings").uitweaker;
this.settingsItems[4].name = this.$lang("settings").startupoptions;
this.settingsItems[5].name = this.$lang("settings").plugins;
this.settingsItems[6].name = this.$lang("settings").updates;
this.settingsItems[7].name = this.$lang("settings").logs;
this.settingsItems[8].name = this.$lang("settings").about;
this.devmodebuttonname = this.$lang("settings").devmode;
this.devmode = localStorage.getItem("devmode");
},
methods: {
dev() {
this.devClicks++;
if (this.devClicks >= 6) {
this.$router.push("/mods/developer");
localStorage.setItem("devmode", "true");
this.devmode = true;
}
},
},

View File

@ -1,23 +1,13 @@
<template>
<div class="background" id="watch-body">
<div id="player-container">
<v-btn text style="position: fixed; z-index: 69420" to="home">
<v-icon>mdi-chevron-down</v-icon>
</v-btn>
<!-- VueTube Player V1 -->
<vuetubePlayer
:sources="sources"
v-if="useBetaPlayer === 'true' && sources.length > 0"
/>
<!-- Stock Player -->
<legacyPlayer
id="player"
<!-- // TODO: move component to default.vue -->
<!-- // TODO: pass sources through vuex instead of props -->
<player
v-if="sources.length > 0 && video.title && video.channelName"
ref="player"
v-touch="{ down: () => $router.push('/home') }"
class="background"
:vid-src="vidSrc"
v-if="useBetaPlayer !== 'true'"
:video="video"
:sources="sources"
/>
</div>
@ -69,14 +59,18 @@
fab
class="vertical-button mx-1"
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"
@click="callMethodByName(item.actionName)"
>
<v-icon v-text="item.icon" />
<div
class="mt-2"
style="font-size: 0.66rem"
class="mt-1"
style="font-size: 0.6rem"
v-text="item.value || item.name"
/>
</v-btn>
@ -108,14 +102,37 @@
</v-sheet>
</v-bottom-sheet> -->
</v-card>
<v-divider />
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<!-- Channel Bar -->
<div v-if="loaded">
<v-card
flat
class="channel-section background py-2 px-3 rounded-0"
:to="video.channelUrl"
class="channel-section py-2 px-3 background"
:class="
$store.state.tweaks.roundWatch && $store.state.tweaks.roundTweak > 0
? $vuetify.theme.dark
? 'background lighten-1'
: 'background darken-1'
: ''
"
to="/channel"
:style="{
borderRadius: $store.state.tweaks.roundWatch
? `${$store.state.tweaks.roundTweak / 2}rem`
: '0',
margin:
$store.state.tweaks.roundWatch &&
$store.state.tweaks.roundTweak > 0
? '1rem'
: '0',
}"
@click="$store.dispatch('channel/fetchChannel', video.channelUrl)"
>
<div id="details">
<div class="avatar-link mr-3">
@ -136,9 +153,14 @@
subscribe
</div>
</v-card>
<v-divider />
</div>
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<!-- Description -->
<div v-if="showMore">
<div class="scroll-y ma-4">
@ -146,12 +168,40 @@
:render="video.renderedData.description"
/>
</div>
<v-divider />
</div>
<v-divider
v-if="
showMore &&
(!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch)
"
/>
<!-- Comments -->
<div v-if="loaded && video.commentData" @click="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">
<template v-for="text in video.commentData.headerText.runs">
<template v-if="text.bold">
@ -163,21 +213,26 @@
<v-icon v-if="showComments" dense>mdi-unfold-less-horizontal</v-icon>
<v-icon v-else dense>mdi-unfold-more-horizontal</v-icon>
</v-card>
<v-divider />
</div>
<v-divider
v-if="
!$store.state.tweaks.roundTweak || !$store.state.tweaks.roundWatch
"
/>
<swipeable-bottom-sheet
v-if="loaded && video.commentData"
v-model="showComments"
hide-overlay
persistent
no-click-animation
attach="#content-container"
v-if="loaded && video.commentData"
>
<mainCommentRenderer
:defaultContinuation="video.commentContinuation"
:commentData="video.commentData"
v-model="showComments"
:comment-data="video.commentData"
:default-continuation="video.commentContinuation"
></mainCommentRenderer>
</swipeable-bottom-sheet>
@ -205,29 +260,27 @@
</template>
<script>
import player from "~/components/Player/index.vue";
import { Share } from "@capacitor/share";
import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
import { getCpn } from "~/plugins/utils";
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
import legacyPlayer from "~/components/Player/legacy.vue";
import vuetubePlayer from "~/components/Player/index.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 SwipeableBottomSheet from "~/components/ExtendedComponents/swipeableBottomSheet";
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
import backType from "~/plugins/classes/backType";
export default {
components: {
player,
ShelfRenderer,
VidLoadRenderer,
SlimVideoDescriptionRenderer,
vuetubePlayer,
legacyPlayer,
ItemSectionRenderer,
SwipeableBottomSheet,
mainCommentRenderer,
SwipeableBottomSheet,
SlimVideoDescriptionRenderer,
},
layout: "empty",
// transition(to) { // TODO: fix layout switching
@ -247,7 +300,6 @@ export default {
// Exit fullscreen if currently in fullscreen
// if (this.$refs.player) this.$refs.player.webkitExitFullscreen();
// Reset player and run getVideo function again
// this.vidSrc = "";
// this.startTime = Math.floor(Date.now() / 1000);
// this.getVideo();
clearInterval(this.interval);
@ -272,18 +324,10 @@ export default {
this.loaded = false;
this.$youtube.getVid(this.$route.query.v).then((result) => {
this.video = result;
console.log("Video info data", result);
console.log(result.availableResolutions);
//--- VueTube Player v1 ---//
// TODO: add other resolutions as well
this.sources = result.availableResolutions;
//--- Legacy Player ---//
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
console.log("Video info data", result);
this.video = result;
//--- Content Stuff ---//
this.likes = result.metadata.likes.toLocaleString();
@ -313,7 +357,6 @@ export default {
// using item.action in the v-for loop
this[name]();
},
dislike() {},
async share() {
// this.share = !this.share;
await Share.share({
@ -365,7 +408,8 @@ export default {
{
name: "Likes",
icon: "mdi-thumb-up-outline",
// action: null,
// action: this.like(),
actionName: "like",
value: this.likes,
disabled: true,
},
@ -384,17 +428,33 @@ export default {
actionName: "share",
disabled: false,
},
{
name: "Save",
icon: "mdi-plus-box-multiple-outline",
actionName: "enqueue",
disabled: true,
},
// {
// name: "Quality",
// icon: "mdi-high-definition",
// actionName: "quality",
// disabled: false,
// },
// {
// name: "Speed",
// icon: "mdi-speedometer",
// actionName: "speed",
// disabled: false,
// },
],
showMore: false,
showComments: false,
// share: false,
vidSrc: null,
sources: [],
recommends: null,
loaded: false,
interval: null,
video: null,
useBetaPlayer: false,
backHierarchy: [],
};
},
@ -402,7 +462,6 @@ export default {
mountedInit() {
this.startTime = Math.floor(Date.now() / 1000);
this.getVideo();
this.useBetaPlayer = localStorage.getItem("debug.BetaPlayer");
// Reset vertical scrolling
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,
linkParser,
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 = () => ({
roundTweak: 0,
roundThumb: null,
roundWatch: null,
});
export const mutations = {
initTweaks(state) {
// NOTE: localStorage is not reactive, so it will only be used on first load
// currently called beforeCreate() in pages/default.vue
// currently called on mounted() in pages/index.vue
if (process.client) {
state.roundTweak = localStorage.getItem("roundTweak") || 0;
state.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) {
@ -15,4 +21,12 @@ export const mutations = {
localStorage.setItem("roundTweak", payload);
}
},
setRoundThumb(state, payload) {
state.roundThumb = payload;
localStorage.setItem("roundThumb", payload);
},
setRoundWatch(state, payload) {
state.roundWatch = payload;
localStorage.setItem("roundWatch", payload);
},
};

View File

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

View File

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

View File

@ -4,7 +4,7 @@
"type": "APK",
"kind": "Directory"
},
"applicationId": "com.Frontesque.youtube",
"applicationId": "com.Frontesque.vuetube",
"variantName": "release",
"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.0 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.0 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.0 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">
<access origin="*" />
<feature name="CDVOrientation">
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation"/>
</feature>
</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">
<access origin="*" />
<feature name="CDVOrientation">
<param name="ios-package" value="CDVOrientation"/>
</feature>
</widget>

View File

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

View File

@ -14,6 +14,8 @@
"@capacitor/status-bar": "^1.0.8",
"@capacitor/toast": "^1.0.8",
"@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"
}
}

View File

@ -12,20 +12,21 @@ Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
</p>
<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/Frontesque/VueTube/issues" alt="Issues"><img src="https://img.shields.io/github/issues/Frontesque/VueTube"></img></a>
<a><img src="https://img.shields.io/github/languages/count/Frontesque/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><img src="https://img.shields.io/github/stars/Frontesque/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/librariesio/github/Frontesque/VueTube" alt="Dependencies"></img></a>
<a><img src="https://img.shields.io/tokei/lines/github/Frontesque/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/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/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/VueTubeApp/VueTube" alt="Languages"></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/VueTubeApp/VueTube" alt="Stars"></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/VueTubeApp/VueTube" alt="Dependencies"></img></a>
<a><img src="https://img.shields.io/tokei/lines/github/VueTubeApp/VueTube" alt="Lines"></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://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://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
<a href="https://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a>
</p>
## Features
@ -50,11 +51,11 @@ To install please visit www.vuetube.app/install
## 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
<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?
@ -62,12 +63,12 @@ Well this has been thrown around on the Return Youtube Dislike discord server fo
### 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
<a href="https://github.com/Frontesque/VueTube/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Frontesque/VueTube" />
<a href="https://github.com/VueTubeApp/VueTube/graphs/contributors">
<img src="https://contrib.rocks/image?repo=VueTubeApp/VueTube" />
</a>
<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)
## Disclamer
The VueTube project and its contents are not affiliated with, funded, authorized, endorsed by, or in any way accociated with YouTube, Google LLC or any of its affiliates and subsidaries. The official YouTube website can be found at [www.youtube.com](https://www.youtube.com).
Any trademark, service mark, trade name, or other intellectual property rights used in the VueTube project are owned by the respective owners.