0
0
Fork 0
mirror of https://github.com/VueTubeApp/VueTube synced 2024-11-29 06:33:05 +00:00

files linted, sorry for the spam c:

This commit is contained in:
Nikita Krupin 2022-03-21 19:47:11 -04:00
parent 777a0da738
commit ad9d916627
29 changed files with 1023 additions and 866 deletions

View file

@ -1,18 +1,22 @@
<template> <template>
<v-bottom-navigation v-model="tabSelection" shift class="bottomNav py-4 accent2"> <v-bottom-navigation
v-model="tabSelection"
shift
class="bottomNav py-4 accent2"
>
<v-btn <v-btn
v-for="(item, i) in tabs" v-for="(item, i) in tabs"
:key="i" :key="i"
v-ripple="false"
class="navButton" class="navButton"
:to="item.link" :to="item.link"
plain plain
v-ripple="false"
> >
<span v-text="item.name" /> <span v-text="item.name" />
<v-icon <v-icon
v-text="item.icon"
:color="tabSelection == i ? 'primary' : 'grey'" :color="tabSelection == i ? 'primary' : 'grey'"
:class="tabSelection == i ? 'tab primaryAlt' : ''" :class="tabSelection == i ? 'tab primaryAlt' : ''"
v-text="item.icon"
/> />
</v-btn> </v-btn>
<!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()" <!-- <v-btn text class="navButton mr-2 fill-height" color="white" @click="searchBtn()"

View file

@ -12,12 +12,12 @@
<div style="position: relative"> <div style="position: relative">
<v-img :src="video.thumbnail" /> <v-img :src="video.thumbnail" />
<div <div
v-text="video.metadata.overlay[0]"
class="videoRuntimeFloat" class="videoRuntimeFloat"
style="color: #fff" style="color: #fff"
v-text="video.metadata.overlay[0]"
/> />
</div> </div>
<div v-text="video.title" style="margin-top: 0.5em" /> <div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="parseBottom(video)" /> <div v-text="parseBottom(video)" />
</v-card-text> </v-card-text>
</v-card> </v-card>

View file

@ -4,22 +4,27 @@
color="accent2" color="accent2"
class="topNav rounded-0 pa-3" class="topNav rounded-0 pa-3"
> >
<h3 class="my-auto ml-4" v-text="page" v-show="!search" /> <h3 v-show="!search" class="my-auto ml-4" v-text="page" />
<v-btn icon v-if="search" class="mr-3 my-auto" @click="$emit('close-search')"> <v-btn
v-if="search"
icon
class="mr-3 my-auto"
@click="$emit('close-search')"
>
<v-icon>mdi-close</v-icon> <v-icon>mdi-close</v-icon>
</v-btn> </v-btn>
<v-text-field <v-text-field
v-if="search"
v-model="text"
solo solo
dense dense
flat flat
label="Search" label="Search"
v-model="text"
@input="$emit('text-changed', text)"
class="searchBar" class="searchBar"
v-if="search" @input="$emit('text-changed', text)"
v-on:keyup.enter="$emit('search-btn', text)" @keyup.enter="$emit('search-btn', text)"
/> />
<v-spacer v-if="!search" /> <v-spacer v-if="!search" />
@ -33,11 +38,11 @@
><v-icon>mdi-magnify</v-icon></v-btn ><v-icon>mdi-magnify</v-icon></v-btn
> >
<v-btn <v-btn
v-show="!search"
icon icon
tile tile
class="ml-4 mr-2 my-auto fill-height" class="ml-4 mr-2 my-auto fill-height"
style="border-radius: 0.25rem !important" style="border-radius: 0.25rem !important"
v-show="!search"
to="/settings" to="/settings"
><v-icon>mdi-dots-vertical</v-icon></v-btn ><v-icon>mdi-dots-vertical</v-icon></v-btn
> >
@ -46,7 +51,16 @@
<script> <script>
export default { export default {
props: ["search", "page"], props: {
search: {
type: Boolean,
default: false,
},
page: {
type: String,
default: "Home",
},
},
events: ["searchBtn", "textChanged", "closeSearch"], events: ["searchBtn", "textChanged", "closeSearch"],
data: () => ({ data: () => ({
text: "", text: "",

View file

@ -1,10 +1,20 @@
<template> <template>
<div> <div>
<v-snackbar v-model="updateSnackbar" class="updateBar" :timeout="updateSnackbarTimeout"> <v-snackbar
v-model="updateSnackbar"
class="updateBar"
:timeout="updateSnackbarTimeout"
>
{{ updateSnackbarText }} {{ updateSnackbarText }}
<template v-slot:action="{ attrs }"> <template #action="{ attrs }">
<v-btn color="primary" text v-bind="attrs" @click="updateSnackbar = false">Close</v-btn> <v-btn
color="primary"
text
v-bind="attrs"
@click="updateSnackbar = false"
>Close</v-btn
>
</template> </template>
</v-snackbar> </v-snackbar>
</div> </div>
@ -22,16 +32,16 @@ export default {
return { return {
updateSnackbar: false, updateSnackbar: false,
updateSnackbarText: "An update is available", updateSnackbarText: "An update is available",
updateSnackbarTimeout: 5000 updateSnackbarTimeout: 5000,
} };
}, },
async mounted() { async mounted() {
const commits = await this.$vuetube.commits; const commits = await this.$vuetube.commits;
const appVersion = process.env.appVersion; const appVersion = process.env.appVersion;
if (appVersion !== commits[0].sha && appVersion !== 'dev-local') { if (appVersion !== commits[0].sha && appVersion !== "dev-local") {
this.updateSnackbar = true; this.updateSnackbar = true;
} }
}, },
} };
</script> </script>

View file

@ -1,11 +1,11 @@
<template> <template>
<v-app v-show="stateLoaded" style="background: black !important"> <v-app v-show="stateLoaded" style="background: black !important">
<topNavigation <topNavigation
:search="search"
:page="page"
@close-search="search = !search" @close-search="search = !search"
@search-btn="searchBtn" @search-btn="searchBtn"
@text-changed="textChanged" @text-changed="textChanged"
:search="search"
:page="page"
/> />
<div class="accent2" style="height: 100%; margin-top: 4rem"> <div class="accent2" style="height: 100%; margin-top: 4rem">
@ -41,7 +41,7 @@
" "
> >
<div class="scroll-y" style="height: 100%"> <div class="scroll-y" style="height: 100%">
<div style="min-width: 180px" v-if="search"> <div v-if="search" style="min-width: 180px">
<v-list-item v-for="(item, index) in response" :key="index"> <v-list-item v-for="(item, index) in response" :key="index">
<v-icon>mdi-magnify</v-icon> <v-icon>mdi-magnify</v-icon>
<v-btn <v-btn
@ -65,8 +65,8 @@
<style> <style>
* { * {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen,
Cantarell, "Open Sans", "Helvetica Neue", sans-serif; Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
} }
.scroll-y { .scroll-y {
overflow-y: scroll !important; /* has to be scroll, not auto */ overflow-y: scroll !important; /* has to be scroll, not auto */

View file

@ -1,31 +1,28 @@
<template> <template>
<v-app> <v-app>
<center> <center>
<v-icon size="100">mdi-alert-circle</v-icon> <v-icon size="100">mdi-alert-circle</v-icon>
<h1 class="grey--text">An error occured!</h1> <h1 class="grey--text">An error occured!</h1>
<v-btn to="/">Reload Application</v-btn> <v-btn to="/">Reload Application</v-btn>
<v-btn to="/logs">Show Logs</v-btn> <v-btn to="/logs">Show Logs</v-btn>
<div style="margin-top: 5em; color: #999; font-size: 0.75em">
<div style="margin-top: 5em; color: #999; font-size: 0.75em;"> <div style="font-size: 1.4em">Error Information</div>
<div style="font-size: 1.4em;">Error Information</div>
<div>Code: {{ error.statusCode }}</div> <div>Code: {{ error.statusCode }}</div>
<div>Path: {{ this.$route.fullPath }}</div> <div>Path: {{ $route.fullPath }}</div>
</div> </div>
</center> </center>
</v-app> </v-app>
</template> </template>
<script> <script>
export default { export default {
layout: 'empty', layout: "empty",
props: { props: {
error: { error: {
type: Object, type: Object,
default: null default: null,
} },
} },
} };
</script> </script>

View file

@ -1,59 +1,54 @@
import colors from 'vuetify/es5/util/colors' import colors from "vuetify/es5/util/colors";
/**** Front's Notes / Don't Remove **** /**** Front's Notes / Don't Remove ****
* Data Storage: * Data Storage:
* localStorage.setItem("key", data) * localStorage.setItem("key", data)
* localStorage.getItem('key') * localStorage.getItem('key')
*/ */
export default { export default {
//--- Bettertube Stuff ---// //--- Bettertube Stuff ---//
env: { env: {
appVersion: "dev-local", appVersion: "dev-local",
}, },
target: 'static', target: "static",
plugins: [ plugins: [
{ src: "~/plugins/youtube", mode: "client" }, { src: "~/plugins/youtube", mode: "client" },
{ src: "~/plugins/vuetube", mode: "client" }, { src: "~/plugins/vuetube", mode: "client" },
{ src: "~/plugins/ryd", mode: "client"} { src: "~/plugins/ryd", mode: "client" },
], ],
generate: { generate: {
dir: '../dist' dir: "../dist",
}, },
//--- Bettertube Debugging ---// //--- Bettertube Debugging ---//
server: { server: {
port: 80, // default: 3000 (Note: Running on ports below 1024 requires root privileges!) port: 80, // default: 3000 (Note: Running on ports below 1024 requires root privileges!)
host: '0.0.0.0', // default: localhost, host: "0.0.0.0", // default: localhost,
timing: false timing: false,
}, },
//--- Default NUXT Stuff ---// //--- Default NUXT Stuff ---//
head: { head: {
title: 'VueTube', title: "VueTube",
htmlAttrs: { htmlAttrs: {
lang: 'en' lang: "en",
}, },
meta: [ meta: [
{ charset: 'utf-8' }, { charset: "utf-8" },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' }, { name: "viewport", content: "width=device-width, initial-scale=1" },
{ name: 'format-detection', content: 'telephone=no' } { name: "format-detection", content: "telephone=no" },
] ],
}, },
css: [], css: [],
components: true, components: true,
buildModules: [ buildModules: ["@nuxtjs/vuetify"],
'@nuxtjs/vuetify',
],
modules: [], modules: [],
vuetify: { vuetify: {
customVariables: ['~/assets/variables.scss'], customVariables: ["~/assets/variables.scss"],
theme: { theme: {
dark: false, dark: false,
options: { customProperties: true }, options: { customProperties: true },
@ -64,7 +59,7 @@ export default {
accent: "#CD201F", accent: "#CD201F",
accent2: "#fff", accent2: "#fff",
background: "#fff", background: "#fff",
info: "#000" info: "#000",
}, },
dark: { dark: {
primary: colors.red.darken2, //colors.blue.darken2 primary: colors.red.darken2, //colors.blue.darken2
@ -73,8 +68,8 @@ export default {
accent2: "#222", accent2: "#222",
background: "#333", background: "#333",
info: "#fff", info: "#fff",
} },
} },
} },
} },
} };

View file

@ -1,6 +1,10 @@
<template> <template>
<center> <center>
<v-img contain style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" /> <v-img
contain
style="margin-top: 5em; max-width: 80%; max-height: 15em"
src="/dev.svg"
/>
<h1 class="grey--text">Page Under Construction</h1> <h1 class="grey--text">Page Under Construction</h1>
<p class="grey--text">Please read the VueTube FAQ for more information.</p> <p class="grey--text">Please read the VueTube FAQ for more information.</p>
</center> </center>

View file

@ -1,6 +1,5 @@
<template> <template>
<center style="padding: 1em;"> <center style="padding: 1em">
<div class="row pa-4" style="flex-direction: column"> <div class="row pa-4" style="flex-direction: column">
<div> <div>
<v-img src="/icon.svg" width="100px" /> <v-img src="/icon.svg" width="100px" />
@ -8,15 +7,19 @@
<v-spacer /> <v-spacer />
<div> <div>
<h1 class="pageTitle mb-3">VueTube</h1> <h1 class="pageTitle mb-3">VueTube</h1>
<v-btn @click="openExternal('https://github.com/Frontesque/VueTube')"><v-icon>mdi-github</v-icon></v-btn> <v-btn @click="openExternal('https://github.com/Frontesque/VueTube')"
<v-btn @click="openExternal('https://discord.gg/7P8KJrdd5W')"><v-icon>mdi-discord</v-icon></v-btn> ><v-icon>mdi-github</v-icon></v-btn
>
<v-btn @click="openExternal('https://discord.gg/7P8KJrdd5W')"
><v-icon>mdi-discord</v-icon></v-btn
>
</div> </div>
</div> </div>
<h3 style="margin-top: 2em;">App Information</h3> <h3 style="margin-top: 2em">App Information</h3>
<div>App Version: {{ version.substring(0, 7) }}</div> <div>App Version: {{ version.substring(0, 7) }}</div>
<h3 style="margin-top: 1em;">Device Information</h3> <h3 style="margin-top: 1em">Device Information</h3>
<div>Platform: {{ deviceInfo.platform }}</div> <div>Platform: {{ deviceInfo.platform }}</div>
<div>OS: {{ deviceInfo.operatingSystem }} ({{ deviceInfo.osVersion }})</div> <div>OS: {{ deviceInfo.operatingSystem }} ({{ deviceInfo.osVersion }})</div>
<div>Model: {{ deviceInfo.model }}</div> <div>Model: {{ deviceInfo.model }}</div>
@ -32,25 +35,25 @@
</style> </style>
<script> <script>
import { Browser } from '@capacitor/browser'; import { Browser } from "@capacitor/browser";
import { Device } from '@capacitor/device'; import { Device } from "@capacitor/device";
export default { export default {
data() { data() {
return { return {
version: process.env.appVersion, version: process.env.appVersion,
deviceInfo: "", deviceInfo: "",
} };
},
methods: {
async openExternal(url) {
await Browser.open({ url: url });
}
}, },
async mounted() { async mounted() {
const info = await Device.getInfo(); const info = await Device.getInfo();
this.deviceInfo = info this.deviceInfo = info;
} },
} methods: {
async openExternal(url) {
await Browser.open({ url: url });
},
},
};
</script> </script>

View file

@ -1,24 +1,26 @@
<template> <template>
<div> <div>
<v-list-item v-for="(item, index) in logs" :key="index"> <v-list-item v-for="(item, index) in logs" :key="index">
<v-card class="card"> <v-card class="card">
<v-card-title> <v-card-title>
<v-chip outlined class="errorChip" color="error" v-if="item.error">Error</v-chip> <v-chip v-if="item.error" outlined class="errorChip" color="error"
>Error</v-chip
>
{{ item.name }} {{ item.name }}
<span v-text="`• ${new Date(item.time).toLocaleString()}`" class="date" /> <span
class="date"
v-text="`• ${new Date(item.time).toLocaleString()}`"
/>
</v-card-title> </v-card-title>
<v-expansion-panels> <v-expansion-panels>
<v-expansion-panel> <v-expansion-panel>
<v-expansion-panel-header>More</v-expansion-panel-header> <v-expansion-panel-header>More</v-expansion-panel-header>
<v-expansion-panel-content v-text="item.data" class="logContent" /> <v-expansion-panel-content class="logContent" v-text="item.data" />
</v-expansion-panel> </v-expansion-panel>
</v-expansion-panels> </v-expansion-panels>
</v-card> </v-card>
</v-list-item> </v-list-item>
</div> </div>
</template> </template>
@ -46,12 +48,12 @@
export default { export default {
data() { data() {
return { return {
logs: new Array() logs: new Array(),
} };
}, },
mounted() { mounted() {
const logs = this.$youtube.logs const logs = this.$youtube.logs;
this.logs = logs; this.logs = logs;
} },
} };
</script> </script>

View file

@ -1,50 +1,38 @@
<template> <template>
<div class="mainContainer pt-1"> <div class="mainContainer pt-1">
<v-card class="pb-5"> <v-card class="pb-5">
<v-card-title>Default Page</v-card-title> <v-card-title>Default Page</v-card-title>
<v-card-text> <v-card-text>
<v-select <v-select
:items="pages"
v-model="page" v-model="page"
:items="pages"
label="Default Page" label="Default Page"
solo solo
></v-select> ></v-select>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
page: "home", page: "home",
pages: [ pages: ["home", "subscriptions", "library"],
"home", };
"subscriptions", },
"library"
] watch: {
} page: function (newVal) {
localStorage.setItem("startPage", newVal);
},
}, },
mounted() { mounted() {
this.page = localStorage.getItem("startPage") || "home"; this.page = localStorage.getItem("startPage") || "home";
}, },
};
watch: {
page: function (val, oldVal) {
localStorage.setItem("startPage", val);
}
}
}
</script> </script>
<style scoped> <style scoped>

View file

@ -1,6 +1,5 @@
<template> <template>
<div class="py-1"> <div class="py-1">
<v-card class="pb-5"> <v-card class="pb-5">
<v-card-title>Global Base Color</v-card-title> <v-card-title>Global Base Color</v-card-title>
<v-row class="ml-3 mr-6"> <v-row class="ml-3 mr-6">
@ -14,13 +13,25 @@
@click="saveTheme($vuetify.theme.dark)" @click="saveTheme($vuetify.theme.dark)"
/> />
</section> </section>
<v-btn v-if="$vuetify.theme.dark" text tile class="white--text black" @click="amoled" > <v-btn
v-if="$vuetify.theme.dark"
text
tile
class="white--text black"
@click="amoled"
>
{{ {{
this.$vuetify.theme.themes.dark.background === '#000' $vuetify.theme.themes.dark.background === "#000" ? "LCD" : "OLED"
? 'LCD'
: 'OLED'
}} }}
<v-icon :size="this.$vuetify.theme.themes.dark.background === '#000' ? '.5rem' : '.9rem'" class="ml-2">mdi-brightness-2</v-icon> <v-icon
:size="
$vuetify.theme.themes.dark.background === '#000'
? '.5rem'
: '.9rem'
"
class="ml-2"
>mdi-brightness-2</v-icon
>
</v-btn> </v-btn>
</v-row> </v-row>
</v-card> </v-card>
@ -28,64 +39,35 @@
<v-card class="pb-5"> <v-card class="pb-5">
<v-card-title>Accent Color</v-card-title> <v-card-title>Accent Color</v-card-title>
<v-card-text> <v-card-text>
<v-alert color="primary" dense outlined type="warning"
<v-alert color="primary" dense outlined type="warning">NOTE: This doesn't save after closing the app (yet)</v-alert> >NOTE: This doesn't save after closing the app (yet)</v-alert
<v-color-picker dot-size="5" hide-mode-switch mode="hexa" v-model="accentColor" /> >
<v-color-picker
v-model="accentColor"
dot-size="5"
hide-mode-switch
mode="hexa"
/>
</v-card-text> </v-card-text>
</v-card> </v-card>
</div> </div>
</template> </template>
<script> <script>
export default { export default {
data() { data() {
return { return {
accentColor: '#ffffff' accentColor: "#ffffff",
} };
},
mounted() {
this.accentColor = this.$vuetify.theme.themes.dark.primary;
this.roblox = localStorage.getItem('roblox') === 'true';
},
methods: {
amoled() {
this.$vuetify.theme.themes.dark.background === '#000' ? (
this.$vuetify.theme.themes.dark.accent = '#222',
this.$vuetify.theme.themes.dark.accent2 = '#222',
this.$vuetify.theme.themes.dark.background = '#333',
localStorage.setItem("isOled", false)
) : (
this.$vuetify.theme.themes.dark.accent = '#000',
this.$vuetify.theme.themes.dark.accent2 = '#000',
this.$vuetify.theme.themes.dark.background = '#000',
localStorage.setItem("isOled", true)
)
// doesn't work 😭
// console.log(document.getElementsByClassName('v-application--wrap')[0])
// console.log(document.getElementsByClassName('v-application--wrap')[0].style)
// document.getElementsByClassName('v-application--wrap')[0].style.backgroundColor = "#000000 !important"
},
saveTheme(isDark) {
this.$vuetube.statusBar.setBackground(this.$vuetify.theme.currentTheme.accent)
localStorage.setItem("darkTheme", isDark);
},
}, },
watch: { watch: {
accentColor: function (val, oldVal) { accentColor: function (val) {
this.$vuetify.theme.currentTheme.primary = val; this.$vuetify.theme.currentTheme.primary = val;
let primaryAlt = this.$vuetube.hexToRgb(val); let primaryAlt = this.$vuetube.hexToRgb(val);
let rgbEdit = 130; //Light Mode let rgbEdit = 130; //Light Mode
if (localStorage.getItem('darkTheme') === "true") rgbEdit = -80; //Dark Mode if (localStorage.getItem("darkTheme") === "true") rgbEdit = -80; //Dark Mode
for (const i in primaryAlt) { for (const i in primaryAlt) {
primaryAlt[i] = primaryAlt[i] + rgbEdit; //Amount To Lighten By primaryAlt[i] = primaryAlt[i] + rgbEdit; //Amount To Lighten By
@ -93,15 +75,45 @@ export default {
if (primaryAlt[i] < 0) primaryAlt[i] = 0; if (primaryAlt[i] < 0) primaryAlt[i] = 0;
} }
primaryAlt = this.$vuetube.rgbToHex(primaryAlt.r, primaryAlt.g, primaryAlt.b); primaryAlt = this.$vuetube.rgbToHex(
primaryAlt.r,
primaryAlt.g,
primaryAlt.b
);
this.$vuetify.theme.currentTheme.primaryAlt = primaryAlt; this.$vuetify.theme.currentTheme.primaryAlt = primaryAlt;
} },
} },
} mounted() {
this.accentColor = this.$vuetify.theme.themes.dark.primary;
this.roblox = localStorage.getItem("roblox") === "true";
},
methods: {
amoled() {
this.$vuetify.theme.themes.dark.background === "#000"
? ((this.$vuetify.theme.themes.dark.accent = "#222"),
(this.$vuetify.theme.themes.dark.accent2 = "#222"),
(this.$vuetify.theme.themes.dark.background = "#333"),
localStorage.setItem("isOled", false))
: ((this.$vuetify.theme.themes.dark.accent = "#000"),
(this.$vuetify.theme.themes.dark.accent2 = "#000"),
(this.$vuetify.theme.themes.dark.background = "#000"),
localStorage.setItem("isOled", true));
// doesn't work 😭
// console.log(document.getElementsByClassName('v-application--wrap')[0])
// console.log(document.getElementsByClassName('v-application--wrap')[0].style)
// document.getElementsByClassName('v-application--wrap')[0].style.backgroundColor = "#000000 !important"
},
saveTheme(isDark) {
this.$vuetube.statusBar.setBackground(
this.$vuetify.theme.currentTheme.accent
);
localStorage.setItem("darkTheme", isDark);
},
},
};
</script> </script>
<style scoped> <style scoped>

View file

@ -18,14 +18,14 @@
thumb-size="64" thumb-size="64"
></v-slider> ></v-slider>
<v-slider <v-slider
v-model="roundTweak"
class="mr-2" class="mr-2"
label="Inner" label="Inner"
v-model="roundTweak"
:max="4" :max="4"
step="1" step="1"
thumb-size="64" thumb-size="64"
> >
<template v-slot:thumb-label="{ value }"> <template #thumb-label="{ value }">
<div <div
class="pa-4 white text-red red-text red--text" class="pa-4 white text-red red-text red--text"
:style="{ borderRadius: value * 3 + 'px !important' }" :style="{ borderRadius: value * 3 + 'px !important' }"

View file

@ -1,30 +1,42 @@
<template> <template>
<div class="py-2"> <div class="py-2">
<v-list-item v-for="(item, index) in commits" :key="index" class="my-1"> <v-list-item v-for="(item, index) in commits" :key="index" class="my-1">
<v-card class="card my-2"> <v-card class="card my-2">
<v-card-title style="padding: 0 0.25em 0 0.75em;"> <v-card-title style="padding: 0 0.25em 0 0.75em">
{{ item.author ? item.author.login : item.commit.author.name }} {{ item.author ? item.author.login : item.commit.author.name }}
<span v-text="`• ${item.sha.substring(0, 7)}`" class="subtitle" /> <span class="subtitle" v-text="`• ${item.sha.substring(0, 7)}`" />
<v-spacer /> <v-spacer />
<v-chip outlined class="tags" color="orange" v-if="index == 0">Latest</v-chip> <v-chip v-if="index == 0" outlined class="tags" color="orange"
<v-chip outlined class="tags" color="green" v-if="item.sha == installedVersion">Installed</v-chip> >Latest</v-chip
>
<v-chip
v-if="item.sha == installedVersion"
outlined
class="tags"
color="green"
>Installed</v-chip
>
</v-card-title> </v-card-title>
<div style="margin-left: 1em;"> <div style="margin-left: 1em">
<div class="date" v-text="new Date(item.commit.committer.date).toLocaleString()" /> <div
class="date"
v-text="new Date(item.commit.committer.date).toLocaleString()"
/>
{{ item.commit.message }} {{ item.commit.message }}
</div> </div>
<v-card-actions> <v-card-actions>
<v-spacer /> <v-spacer />
<v-btn @click="openExternal(item)"><v-icon class="btn-icon">mdi-github</v-icon>View</v-btn> <v-btn @click="openExternal(item)"
<v-btn disabled @click="install(item)"><v-icon class="btn-icon">mdi-download</v-icon>Install</v-btn> ><v-icon class="btn-icon">mdi-github</v-icon>View</v-btn
>
<v-btn disabled @click="install(item)"
><v-icon class="btn-icon">mdi-download</v-icon>Install</v-btn
>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-list-item> </v-list-item>
</div> </div>
</template> </template>
@ -51,21 +63,22 @@
</style> </style>
<script> <script>
import { Browser } from '@capacitor/browser'; import { Browser } from "@capacitor/browser";
export default { export default {
data() { data() {
return { return {
commits: new Array(), commits: new Array(),
installedVersion: process.env.appVersion installedVersion: process.env.appVersion,
} };
}, },
async mounted() { async mounted() {
const commits = await this.$vuetube.commits; const commits = await this.$vuetube.commits;
if (commits[0].sha) { //If Commit Valid if (commits[0].sha) {
//If Commit Valid
this.commits = commits; this.commits = commits;
} else { } else {
console.log(commits) console.log(commits);
} }
}, },
methods: { methods: {
@ -75,10 +88,9 @@ export default {
install(item) { install(item) {
this.$vuetube.getRuns(item, (data) => { this.$vuetube.getRuns(item, (data) => {
console.log(data) console.log(data);
}); });
},
} },
} };
}
</script> </script>

View file

@ -10,9 +10,13 @@
<v-card-text> <v-card-text>
<div style="position: relative"> <div style="position: relative">
<v-img :src="video.thumbnails[video.thumbnails.length - 1].url" /> <v-img :src="video.thumbnails[video.thumbnails.length - 1].url" />
<div v-text="video.runtime" class="videoRuntimeFloat" style="color: #fff" /> <div
class="videoRuntimeFloat"
style="color: #fff"
v-text="video.runtime"
/>
</div> </div>
<div v-text="video.title" style="margin-top: 0.5em" /> <div style="margin-top: 0.5em" v-text="video.title" />
<div v-text="`${video.views} • ${video.uploaded}`" /> <div v-text="`${video.views} • ${video.uploaded}`" />
</v-card-text> </v-card-text>
</v-card> </v-card>

View file

@ -1,12 +1,15 @@
<template> <template>
<div style="padding-top: 1em;"> <div style="padding-top: 1em">
<v-list-item v-for="(item, index) in settingsItems" :key="index"> <v-list-item v-for="(item, index) in settingsItems" :key="index">
<v-btn
<v-btn text class="entry text-left text-capitalize" :to="item.to" :disabled="item.disabled"> text
<v-icon v-text="item.icon" size="30px" class="icon" /> class="entry text-left text-capitalize"
:to="item.to"
:disabled="item.disabled"
>
<v-icon size="30px" class="icon" v-text="item.icon" />
{{ item.name }} {{ item.name }}
</v-btn> </v-btn>
</v-list-item> </v-list-item>
</div> </div>
</template> </template>
@ -30,15 +33,28 @@ export default {
settingsItems: [ settingsItems: [
{ name: "General", icon: "mdi-cog", to: "", disabled: true }, { name: "General", icon: "mdi-cog", to: "", disabled: true },
{ name: "Theme", icon: "mdi-brush-variant", to: "/mods/theme" }, { name: "Theme", icon: "mdi-brush-variant", to: "/mods/theme" },
{ name: "Player", icon: "mdi-motion-play-outline", to: "", disabled: true }, {
{ name: "UI Tweaker", icon: "mdi-television-guide", to: "/mods/tweaks" }, name: "Player",
icon: "mdi-motion-play-outline",
to: "",
disabled: true,
},
{
name: "UI Tweaker",
icon: "mdi-television-guide",
to: "/mods/tweaks",
},
{ name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" }, { name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" },
{ name: "Plugins", icon: "mdi-puzzle", to: "", disabled: true }, { name: "Plugins", icon: "mdi-puzzle", to: "", disabled: true },
{ name: "Updates", icon: "mdi-cloud-download-outline", to: "/mods/updates" }, {
name: "Updates",
icon: "mdi-cloud-download-outline",
to: "/mods/updates",
},
{ name: "Logs", icon: "mdi-text-box-outline", to: "/mods/logs" }, { name: "Logs", icon: "mdi-text-box-outline", to: "/mods/logs" },
{ name: "About", icon: "mdi-information-outline", to: "/mods/about" }, { name: "About", icon: "mdi-information-outline", to: "/mods/about" },
] ],
} };
} },
} };
</script> </script>

View file

@ -1,6 +1,10 @@
<template> <template>
<center> <center>
<v-img contain style="margin-top: 5em; max-width: 80%; max-height: 15em;" src="/dev.svg" /> <v-img
contain
style="margin-top: 5em; max-width: 80%; max-height: 15em"
src="/dev.svg"
/>
<h1 class="grey--text">Page Under Construction</h1> <h1 class="grey--text">Page Under Construction</h1>
<p class="grey--text">Please read the VueTube FAQ for more information.</p> <p class="grey--text">Please read the VueTube FAQ for more information.</p>
</center> </center>

View file

@ -18,11 +18,11 @@
> >
<v-btn <v-btn
text text
@click="item.action"
class="vertical-button" class="vertical-button"
style="padding: 0; margin: 0" style="padding: 0; margin: 0"
elevation="0" elevation="0"
:disabled="item.disabled" :disabled="item.disabled"
@click="item.action"
> >
<v-icon v-text="item.icon" /> <v-icon v-text="item.icon" />
<div v-text="item.value || item.name" /> <div v-text="item.value || item.name" />
@ -40,7 +40,7 @@
<p>Channel Stuff</p> <p>Channel Stuff</p>
<hr /> <hr />
</v-card-text> </v-card-text>
<div class="scroll-y ml-2 mr-2" v-if="showMore"> <div v-if="showMore" class="scroll-y ml-2 mr-2">
{{ description }} {{ description }}
</div> </div>
@ -72,7 +72,6 @@
</div> </div>
</template> </template>
<style> <style>
.vertical-button span.v-btn__content { .vertical-button span.v-btn__content {
flex-direction: column; flex-direction: column;
@ -82,12 +81,6 @@
<script> <script>
export default { export default {
methods: {
dislike() {},
share() {
this.share = !this.share;
},
},
data() { data() {
return { return {
interactions: [ interactions: [
@ -151,5 +144,11 @@ export default {
this.interactions[1].value = data.dislikes.toLocaleString(); this.interactions[1].value = data.dislikes.toLocaleString();
}); });
}, },
methods: {
dislike() {},
share() {
this.share = !this.share;
},
},
}; };
</script> </script>

View file

@ -1,20 +1,20 @@
// To centralize certain values and URLs as for easier debugging and refactoring // To centralize certain values and URLs as for easier debugging and refactoring
const url = { const url = {
YT_URL: 'https://www.youtube.com', YT_URL: "https://www.youtube.com",
YT_MOBILE: "https://m.youtube.com", YT_MOBILE: "https://m.youtube.com",
YT_MUSIC_URL: 'https://music.youtube.com', YT_MUSIC_URL: "https://music.youtube.com",
YT_BASE_API: 'https://www.youtube.com/youtubei/v1', YT_BASE_API: "https://www.youtube.com/youtubei/v1",
YT_SUGGESTIONS: "https://suggestqueries.google.com/complete", YT_SUGGESTIONS: "https://suggestqueries.google.com/complete",
VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube", VT_GITHUB: "https://api.github.com/repos/Frontesque/VueTube",
} };
const ytApiVal = { const ytApiVal = {
VERSION: "16.25", VERSION: "16.25",
CLIENTNAME: "ANDROID", CLIENTNAME: "ANDROID",
VERSION_WEB: "2.20220318.00.00", VERSION_WEB: "2.20220318.00.00",
CLIENT_WEB: 2 CLIENT_WEB: 2,
} };
module.exports = { module.exports = {
URLS: url, URLS: url,
@ -26,42 +26,42 @@ module.exports = {
watch: "Watch", watch: "Watch",
recommendations: "Recommendations", recommendations: "Recommendations",
init: "Initialize", init: "Initialize",
innertube: "Innertube" innertube: "Innertube",
}, },
INNERTUBE_HEADER: (info) => { INNERTUBE_HEADER: (info) => {
let headers = { let headers = {
accept: '*/*', accept: "*/*",
'user-agent': info.userAgent, "user-agent": info.userAgent,
'accept-language': `${info.hl}-${info.gl},${info.hl};q=0.9`, "accept-language": `${info.hl}-${info.gl},${info.hl};q=0.9`,
'content-type': 'application/json', "content-type": "application/json",
'x-goog-authuser': 0, "x-goog-authuser": 0,
'x-goog-visitor-id': info.visitorData || "", "x-goog-visitor-id": info.visitorData || "",
'x-youtube-client-name': ytApiVal.CLIENTNAME, "x-youtube-client-name": ytApiVal.CLIENTNAME,
'x-youtube-client-version': ytApiVal.VERSION, "x-youtube-client-version": ytApiVal.VERSION,
'x-origin': info.originalUrl, "x-origin": info.originalUrl,
'origin': info.originalUrl, origin: info.originalUrl,
}; };
return headers return headers;
}, },
INNERTUBE_CLIENT: (info) => { INNERTUBE_CLIENT: (info) => {
let client = { let client = {
"gl": info.gl, gl: info.gl,
"hl": info.hl, hl: info.hl,
"deviceMake": info.deviceMake, deviceMake: info.deviceMake,
"deviceModel": info.deviceModel, deviceModel: info.deviceModel,
"userAgent": info.userAgent, userAgent: info.userAgent,
"clientName": ytApiVal.CLIENTNAME, clientName: ytApiVal.CLIENTNAME,
"clientVersion": ytApiVal.VERSION, clientVersion: ytApiVal.VERSION,
"osName": info.osName, osName: info.osName,
"osVersion": info.osVersion, osVersion: info.osVersion,
"platform": "MOBILE", platform: "MOBILE",
"originalUrl": info.originalUrl, originalUrl: info.originalUrl,
"configInfo": info.configInfo, configInfo: info.configInfo,
"remoteHost": info.remoteHost, remoteHost: info.remoteHost,
"visitorData": info.visitorData, visitorData: info.visitorData,
}; };
return client return client;
}, },
} };

View file

@ -1,47 +1,56 @@
// Code specific to working with the innertube API // Code specific to working with the innertube API
// https://www.youtube.com/youtubei/v1 // https://www.youtube.com/youtubei/v1
import { Http } from '@capacitor-community/http'; import { Http } from "@capacitor-community/http";
import { getBetweenStrings } from './utils'; import { getBetweenStrings } from "./utils";
import constants from './constants'; import constants from "./constants";
class Innertube { class Innertube {
//--- Initiation ---// //--- Initiation ---//
constructor(ErrorCallback) { constructor(ErrorCallback) {
this.ErrorCallback = ErrorCallback || undefined; this.ErrorCallback = ErrorCallback || undefined;
this.retry_count = 0 this.retry_count = 0;
} }
checkErrorCallback() { checkErrorCallback() {
return typeof this.ErrorCallback === "function" return typeof this.ErrorCallback === "function";
} }
async initAsync() { async initAsync() {
const html = await Http.get({ url: constants.URLS.YT_URL, params: { hl: "en" } }).catch((error) => error); const html = await Http.get({
url: constants.URLS.YT_URL,
params: { hl: "en" },
}).catch((error) => error);
try { try {
if (html instanceof Error && this.checkErrorCallback) this.ErrorCallback(html.message, true); if (html instanceof Error && this.checkErrorCallback)
this.ErrorCallback(html.message, true);
try { try {
const data = JSON.parse(getBetweenStrings(html.data, 'ytcfg.set(', ');')); const data = JSON.parse(
getBetweenStrings(html.data, "ytcfg.set(", ");")
);
if (data.INNERTUBE_CONTEXT) { if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY; this.key = data.INNERTUBE_API_KEY;
this.context = data.INNERTUBE_CONTEXT; this.context = data.INNERTUBE_CONTEXT;
this.logged_in = data.LOGGED_IN; this.logged_in = data.LOGGED_IN;
this.context.client = constants.INNERTUBE_CLIENT(this.context.client) this.context.client = constants.INNERTUBE_CLIENT(this.context.client);
this.header = constants.INNERTUBE_HEADER(this.context.client) this.header = constants.INNERTUBE_HEADER(this.context.client);
} }
} catch (err) { } catch (err) {
console.log(err) console.log(err);
if (this.checkErrorCallback) this.ErrorCallback(err, true) if (this.checkErrorCallback) this.ErrorCallback(err, true);
if (this.retry_count >= 10) { this.initAsync() } else { if (this.checkErrorCallback) this.ErrorCallback("Failed to retrieve Innertube session", true); } if (this.retry_count >= 10) {
this.initAsync();
} else {
if (this.checkErrorCallback)
this.ErrorCallback("Failed to retrieve Innertube session", true);
}
} }
} catch (error) { } catch (error) {
this.ErrorCallback(error, true) this.ErrorCallback(error, true);
}; }
}; }
static async createAsync(ErrorCallback) { static async createAsync(ErrorCallback) {
const created = new Innertube(ErrorCallback); const created = new Innertube(ErrorCallback);
@ -52,100 +61,122 @@ class Innertube {
//--- API Calls ---// //--- API Calls ---//
async browseAsync(action_type) { async browseAsync(action_type) {
let data = { context: this.context } let data = { context: this.context };
switch (action_type) { switch (action_type) {
case 'recommendations': case "recommendations":
data.browseId = 'FEwhat_to_watch' data.browseId = "FEwhat_to_watch";
break; break;
case 'playlist': case "playlist":
data.browseId = args.browse_id data.browseId = args.browse_id;
break; break;
default: default:
} }
console.log(data) console.log(data);
const response = await Http.post({ const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`, url: `${constants.URLS.YT_BASE_API}/browse?key=${this.key}`,
data: data, data: data,
headers: { "Content-Type": "application/json" } headers: { "Content-Type": "application/json" },
}).catch((error) => error); }).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.status, message: response.message }; if (response instanceof Error)
return {
success: false,
status_code: response.status,
message: response.message,
};
return { return {
success: true, success: true,
status_code: response.status, status_code: response.status,
data: response.data data: response.data,
}; };
} }
static getThumbnail(id, resolution) { static getThumbnail(id, resolution) {
if (resolution == "max") { if (resolution == "max") {
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg` const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
let img = new Image(); let img = new Image();
img.src = url img.src = url;
img.onload = function () { img.onload = function () {
if (img.height !== 120) return url if (img.height !== 120) return url;
}; };
} }
return `https://img.youtube.com/vi/${id}/mqdefault.jpg` return `https://img.youtube.com/vi/${id}/mqdefault.jpg`;
} }
async getVidAsync(id) { async getVidAsync(id) {
let data = { context: this.context, videoId: id } let data = { context: this.context, videoId: id };
const response = await Http.get({ const response = await Http.get({
url: `https://m.youtube.com/watch?v=${id}&pbj=1`, url: `https://m.youtube.com/watch?v=${id}&pbj=1`,
params: {}, params: {},
headers: Object.assign(this.header, { headers: Object.assign(this.header, {
referer: `https://m.youtube.com/watch?v=${id}`, referer: `https://m.youtube.com/watch?v=${id}`,
'x-youtube-client-name': constants.YT_API_VALUES.CLIENT_WEB, "x-youtube-client-name": constants.YT_API_VALUES.CLIENT_WEB,
'x-youtube-client-version': constants.YT_API_VALUES.VERSION_WEB}) "x-youtube-client-version": constants.YT_API_VALUES.VERSION_WEB,
}),
}).catch((error) => error); }).catch((error) => error);
const responseMobile = await Http.post({ const responseMobile = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`, url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data, data: data,
headers: constants.INNERTUBE_HEADER(this.context) headers: constants.INNERTUBE_HEADER(this.context),
}).catch((error) => error); }).catch((error) => error);
if (response instanceof Error) return { success: false, status_code: response.response.status, message: response.message }; if (response instanceof Error)
return {
success: false,
status_code: response.response.status,
message: response.message,
};
return { return {
success: true, success: true,
status_code: response.status, status_code: response.status,
data: {webOutput: response.data, appOutput: responseMobile.data} data: { webOutput: response.data, appOutput: responseMobile.data },
}; };
} }
// Simple Wrappers // Simple Wrappers
async getRecommendationsAsync() { async getRecommendationsAsync() {
const rec = await this.browseAsync("recommendations"); const rec = await this.browseAsync("recommendations");
console.log(rec.data) console.log(rec.data);
return rec; return rec;
} }
async VidInfoAsync(id) { async VidInfoAsync(id) {
let response = await this.getVidAsync(id) let response = await this.getVidAsync(id);
if (response.success && (response.data.webOutput[2].playerResponse?.playabilityStatus?.status == ("ERROR" || undefined))) if (
throw new Error(`Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`) response.success &&
const responseWeb = response.data.webOutput response.data.webOutput[2].playerResponse?.playabilityStatus?.status ==
const responseApp = response.data.appOutput ("ERROR" || undefined)
const details = responseWeb[2].playerResponse?.videoDetails )
const microformat = responseWeb[2].playerResponse?.microformat?.playerMicroformatRenderer throw new Error(
const renderedPanels = responseWeb[3].response?.engagementPanels `Could not get information for video: ${response[2].playerResponse?.playabilityStatus?.status} - ${response[2].playerResponse?.playabilityStatus?.reason}`
const columnUI = responseWeb[3].response?.contents.singleColumnWatchNextResults?.results?.results );
const resolutions = responseApp.streamingData const responseWeb = response.data.webOutput;
const responseApp = response.data.appOutput;
const details = responseWeb[2].playerResponse?.videoDetails;
const microformat =
responseWeb[2].playerResponse?.microformat?.playerMicroformatRenderer;
const renderedPanels = responseWeb[3].response?.engagementPanels;
const columnUI =
responseWeb[3].response?.contents.singleColumnWatchNextResults?.results
?.results;
const resolutions = responseApp.streamingData;
console.log((columnUI.contents).length) console.log(columnUI.contents.length);
return { return {
id: details.videoId, id: details.videoId,
title: details.title || microformat.title?.runs[0].text, title: details.title || microformat.title?.runs[0].text,
isLive: details.isLiveContent || microformat.liveBroadcastDetails?.isLiveNow || false, isLive:
details.isLiveContent ||
microformat.liveBroadcastDetails?.isLiveNow ||
false,
channelName: details.author || microformat.ownerChannelName, channelName: details.author || microformat.ownerChannelName,
channelUrl: microformat.ownerProfileUrl, channelUrl: microformat.ownerProfileUrl,
availableResolutions: resolutions?.formats, availableResolutions: resolutions?.formats,
@ -153,7 +184,8 @@ class Innertube {
metadata: { metadata: {
description: microformat.description?.runs[0].text, description: microformat.description?.runs[0].text,
descriptionShort: details.shortDescription, descriptionShort: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails || microformat.thumbnails?.thumbnails, thumbnails:
details.thumbnails?.thumbnails || microformat.thumbnails?.thumbnails,
isFamilySafe: microformat.isFamilySafe, isFamilySafe: microformat.isFamilySafe,
availableCountries: microformat.availableCountries, availableCountries: microformat.availableCountries,
liveBroadcastDetails: microformat.liveBroadcastDetails, liveBroadcastDetails: microformat.liveBroadcastDetails,
@ -162,19 +194,24 @@ class Innertube {
isPrivate: details.isPrivate, isPrivate: details.isPrivate,
viewCount: details.viewCount || microformat.viewCount, viewCount: details.viewCount || microformat.viewCount,
lengthSeconds: details.lengthSeconds || microformat.lengthSeconds, lengthSeconds: details.lengthSeconds || microformat.lengthSeconds,
likes: parseInt(columnUI?.contents[1] likes: parseInt(
.slimVideoMetadataSectionRenderer?.contents[1].slimVideoActionBarRenderer?.buttons[0] columnUI?.contents[1].slimVideoMetadataSectionRenderer?.contents[1].slimVideoActionBarRenderer?.buttons[0].slimMetadataToggleButtonRenderer?.button?.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(
.slimMetadataToggleButtonRenderer?.button?.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(/\D/g, '')) // Yes. I know. /\D/g,
""
)
), // Yes. I know.
}, },
renderedData: { renderedData: {
description: renderedPanels[0].engagementPanelSectionListRenderer?.content.structuredDescriptionContentRenderer?.items[1].expandableVideoDescriptionBodyRenderer?.descriptionBodyText.runs, description:
recommendations: columnUI?.contents[(columnUI.contents).length -1].itemSectionRenderer?.contents renderedPanels[0].engagementPanelSectionListRenderer?.content
.structuredDescriptionContentRenderer?.items[1]
.expandableVideoDescriptionBodyRenderer?.descriptionBodyText.runs,
recommendations:
columnUI?.contents[columnUI.contents.length - 1].itemSectionRenderer
?.contents,
},
};
} }
} }
} export default Innertube;
}
export default Innertube

View file

@ -5,13 +5,13 @@ import constants from "./constants";
function useRender(video, renderer) { function useRender(video, renderer) {
switch (renderer) { switch (renderer) {
case "videoWithContextRenderer": case "videoWithContextRenderer":
return videoWithContextRenderer(video) return videoWithContextRenderer(video);
case "gridVideoRenderer": case "gridVideoRenderer":
return gridVideoRenderer(video) return gridVideoRenderer(video);
case "compactAutoplayRenderer": case "compactAutoplayRenderer":
return compactAutoplayRenderer(video) return compactAutoplayRenderer(video);
default: default:
return undefined return undefined;
} }
} }
@ -20,16 +20,31 @@ function gridVideoRenderer(video) {
id: video.videoId, id: video.videoId,
title: video.title?.runs[0].text, title: video.title?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"), thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0].text : video.longBylineText?.runs[0].text, channel: video.shortBylineText?.runs[0]
channelId: (video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.browseId, ? video.shortBylineText.runs[0].text
channelURL: `${constants.YT_URL}/${(video.shortBylineText?.runs[0] ? video.shortBylineText.runs[0] : video.longBylineText?.runs[0]).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl}`, : video.longBylineText?.runs[0].text,
channelId: (video.shortBylineText?.runs[0]
? video.shortBylineText.runs[0]
: video.longBylineText?.runs[0]
).navigationEndpoint?.browseEndpoint?.browseId,
channelURL: `${constants.YT_URL}/${
(video.shortBylineText?.runs[0]
? video.shortBylineText.runs[0]
: video.longBylineText?.runs[0]
).navigationEndpoint?.browseEndpoint?.canonicalBaseUrl
}`,
channelThumbnail: video.channelThumbnail?.thumbnails[0], channelThumbnail: video.channelThumbnail?.thumbnails[0],
metadata: { metadata: {
published: video.publishedTimeText?.runs[0].text, published: video.publishedTimeText?.runs[0].text,
views: video.shortViewCountText?.runs[0].text, views: video.shortViewCountText?.runs[0].text,
length: video.lengthText?.runs[0].text, length: video.lengthText?.runs[0].text,
overlayStyle: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.style), overlayStyle: video.thumbnailOverlays?.map(
overlay: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text), (overlay) => overlay.thumbnailOverlayTimeStatusRenderer?.style
),
overlay: video.thumbnailOverlays?.map(
(overlay) =>
overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text
),
}, },
}; };
} }
@ -40,24 +55,35 @@ function videoWithContextRenderer(video) {
title: video.headline?.runs[0].text, title: video.headline?.runs[0].text,
thumbnail: Innertube.getThumbnail(video.videoId, "max"), thumbnail: Innertube.getThumbnail(video.videoId, "max"),
channel: video.shortBylineText?.runs[0].text, channel: video.shortBylineText?.runs[0].text,
channelURL: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl, channelURL:
channelId: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.navigationEndpoint?.browseEndpoint?.browseId, video.channelThumbnail?.channelThumbnailWithLinkRenderer
channelThumbnail: video.channelThumbnail?.channelThumbnailWithLinkRenderer?.thumbnail.thumbnails[0].url, ?.navigationEndpoint?.browseEndpoint?.canonicalBaseUrl,
channelId:
video.channelThumbnail?.channelThumbnailWithLinkRenderer
?.navigationEndpoint?.browseEndpoint?.browseId,
channelThumbnail:
video.channelThumbnail?.channelThumbnailWithLinkRenderer?.thumbnail
.thumbnails[0].url,
metadata: { metadata: {
views: video.shortViewCountText?.runs[0].text, views: video.shortViewCountText?.runs[0].text,
length: video.lengthText?.runs[0].text, length: video.lengthText?.runs[0].text,
overlayStyle: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.style), overlayStyle: video.thumbnailOverlays?.map(
overlay: video.thumbnailOverlays?.map(overlay => overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text), (overlay) => overlay.thumbnailOverlayTimeStatusRenderer?.style
),
overlay: video.thumbnailOverlays?.map(
(overlay) =>
overlay.thumbnailOverlayTimeStatusRenderer?.text.runs[0].text
),
isWatched: video.isWatched, isWatched: video.isWatched,
}, },
} };
} }
function compactAutoplayRenderer(video) { function compactAutoplayRenderer(video) {
video = video.contents video = video.contents;
let item; let item;
if (video) item = video[0] if (video) item = video[0];
if (item) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0]) if (item) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0]);
else return undefined else return undefined;
} }
export default useRender export default useRender;

View file

@ -1,6 +1,6 @@
//--- Modules/Imports ---// //--- Modules/Imports ---//
import { Http } from '@capacitor-community/http'; import { Http } from "@capacitor-community/http";
import constants from './constants'; import constants from "./constants";
function generateUserID(length = 36) { function generateUserID(length = 36) {
const charset = const charset =
@ -73,31 +73,27 @@ async function solvePuzzle(puzzle) {
} }
} }
const rydModule = { const rydModule = {
logs: new Array(), logs: new Array(),
//--- Get Dislikes ---// //--- Get Dislikes ---//
getDislikes(id, callback) { getDislikes(id, callback) {
console.log("fetching ryd") console.log("fetching ryd");
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `https://returnyoutubedislikeapi.com/votes`, url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id } params: { videoId: id },
}) })
.then((res) => { .then((res) => {
callback(res.data) callback(res.data);
}) })
.catch((err) => { .catch((err) => {
callback(err); callback(err);
}); });
} },
};
}
//--- Start ---// //--- Start ---//
export default ({ app }, inject) => { export default ({ app }, inject) => {
inject('ryd', {...rydModule}) inject("ryd", { ...rydModule });
} };

View file

@ -1,7 +1,10 @@
// Collection of functions that are useful but non-specific to any particular files // Collection of functions that are useful but non-specific to any particular files
function getBetweenStrings(data, start_string, end_string) { function getBetweenStrings(data, start_string, end_string) {
const regex = new RegExp(`${escapeRegExp(start_string)}(.*?)${escapeRegExp(end_string)}`, "s"); const regex = new RegExp(
`${escapeRegExp(start_string)}(.*?)${escapeRegExp(end_string)}`,
"s"
);
const match = data.match(regex); const match = data.match(regex);
return match ? match[1] : undefined; return match ? match[1] : undefined;
} }
@ -10,15 +13,15 @@ function escapeRegExp(string) {
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
} }
function hexToRgb(hex) { function hexToRgb(hex) {
var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex); var result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
return result ? { return result
? {
r: parseInt(result[1], 16), r: parseInt(result[1], 16),
g: parseInt(result[2], 16), g: parseInt(result[2], 16),
b: parseInt(result[3], 16) b: parseInt(result[3], 16),
} : null; }
: null;
} }
function rgbToHex(r, g, b) { function rgbToHex(r, g, b) {
@ -28,5 +31,5 @@ function rgbToHex(r, g, b) {
module.exports = { module.exports = {
getBetweenStrings, getBetweenStrings,
hexToRgb, hexToRgb,
rgbToHex rgbToHex,
}; };

View file

@ -1,44 +1,39 @@
//--- Modules/Imports ---// //--- Modules/Imports ---//
import { Http } from '@capacitor-community/http'; import { Http } from "@capacitor-community/http";
import { StatusBar, Style } from '@capacitor/status-bar'; import { StatusBar, Style } from "@capacitor/status-bar";
import constants from './constants'; import constants from "./constants";
import { hexToRgb, rgbToHex } from './utils'; import { hexToRgb, rgbToHex } from "./utils";
const module = { const module = {
//--- Get GitHub Commits ---// //--- Get GitHub Commits ---//
commits: new Promise((resolve, reject) => { commits: new Promise((resolve, reject) => {
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `${constants.URLS.VT_GITHUB}/commits`, url: `${constants.URLS.VT_GITHUB}/commits`,
params: {} params: {},
}) })
.then((res) => { .then((res) => {
resolve(res.data) resolve(res.data);
}) })
.catch((err) => { .catch((err) => {
reject(err) reject(err);
}); });
}), }),
getRuns(item, callback) { getRuns(item, callback) {
let url = `${constants.URLS.VT_GITHUB}/commits/${item.sha}/check-runs`; let url = `${constants.URLS.VT_GITHUB}/commits/${item.sha}/check-runs`;
Http.request({ Http.request({
method: 'GET', method: "GET",
url: url, url: url,
params: {} params: {},
}) })
.then((res) => { .then((res) => {
callback(res.data) callback(res.data);
}) })
.catch((err) => { .catch((err) => {
callback(err) callback(err);
}); });
}, },
statusBar: { statusBar: {
@ -59,15 +54,18 @@ const module = {
}, },
async setBackground(color) { async setBackground(color) {
return await StatusBar.setBackgroundColor({ color: color }); return await StatusBar.setBackgroundColor({ color: color });
} },
}, },
hexToRgb(hex) { return hexToRgb(hex); }, hexToRgb(hex) {
rgbToHex(r, g, b) { return rgbToHex(r, g, b); } return hexToRgb(hex);
},
} rgbToHex(r, g, b) {
return rgbToHex(r, g, b);
},
};
//--- Start ---// //--- Start ---//
export default ({ app }, inject) => { export default ({ app }, inject) => {
inject('vuetube', module) inject("vuetube", module);
} };

View file

@ -1,8 +1,8 @@
//--- Modules/Imports ---// //--- Modules/Imports ---//
import { Http } from '@capacitor-community/http'; import { Http } from "@capacitor-community/http";
import Innertube from './innertube' import Innertube from "./innertube";
import constants from './constants'; import constants from "./constants";
import useRender from './renderers'; import useRender from "./renderers";
//--- Logger Function ---// //--- Logger Function ---//
function logger(func, data, isError = false) { function logger(func, data, isError = false) {
@ -10,36 +10,58 @@ function logger(func, data, isError = false) {
name: func, name: func,
time: Date.now(), time: Date.now(),
data: data, data: data,
error: isError error: isError,
}) });
} }
//--- Youtube Base Parser ---// //--- Youtube Base Parser ---//
function youtubeParse(html, callback) { function youtubeParse(html, callback) {
//--- Replace Encoded Characters ---/// //--- Replace Encoded Characters ---///
html = html.replace(/\\x([0-9A-F]{2})/ig, (...items) => { return String.fromCharCode(parseInt(items[1], 16)); }); html = html.replace(/\\x([0-9A-F]{2})/gi, (...items) => {
return String.fromCharCode(parseInt(items[1], 16));
});
//--- Properly Format JSON ---// //--- Properly Format JSON ---//
html = html.replaceAll("\\\\\"", ""); html = html.replaceAll('\\\\"', "");
//--- Parse JSON ---// //--- Parse JSON ---//
html = JSON.parse(html); html = JSON.parse(html);
//--- Get Results ---// ( Thanks To appit-online On Github ) -> https://github.com/appit-online/youtube-search/blob/master/src/lib/search.ts //--- Get Results ---// ( Thanks To appit-online On Github ) -> https://github.com/appit-online/youtube-search/blob/master/src/lib/search.ts
let results; let results;
if (html && html.contents && html.contents.sectionListRenderer && html.contents.sectionListRenderer.contents && if (
html &&
html.contents &&
html.contents.sectionListRenderer &&
html.contents.sectionListRenderer.contents &&
html.contents.sectionListRenderer.contents.length > 0 && html.contents.sectionListRenderer.contents.length > 0 &&
html.contents.sectionListRenderer.contents[0].itemSectionRenderer && html.contents.sectionListRenderer.contents[0].itemSectionRenderer &&
html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents.length > 0) { html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents
results = html.contents.sectionListRenderer.contents[0].itemSectionRenderer.contents; .length > 0
) {
results =
html.contents.sectionListRenderer.contents[0].itemSectionRenderer
.contents;
logger(constants.LOGGER_NAMES.search, results); logger(constants.LOGGER_NAMES.search, results);
callback(results); callback(results);
} else { } else {
try { try {
results = JSON.parse(html.split('{"itemSectionRenderer":{"contents":')[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(',"continuations":[{')[0]); results = JSON.parse(
html
.split('{"itemSectionRenderer":{"contents":')
[html.split('{"itemSectionRenderer":{"contents":').length - 1].split(
',"continuations":[{'
)[0]
);
logger(constants.LOGGER_NAMES.search, results); logger(constants.LOGGER_NAMES.search, results);
callback(results); callback(results);
} catch (e) {} } catch (e) {}
try { try {
results = JSON.parse(html.split('{"itemSectionRenderer":')[html.split('{"itemSectionRenderer":').length - 1].split('},{"continuationItemRenderer":{')[0]).contents; results = JSON.parse(
html
.split('{"itemSectionRenderer":')
[html.split('{"itemSectionRenderer":').length - 1].split(
'},{"continuationItemRenderer":{'
)[0]
).contents;
logger(constants.LOGGER_NAMES.search, results); logger(constants.LOGGER_NAMES.search, results);
callback(results); callback(results);
} catch (e) {} } catch (e) {}
@ -49,9 +71,9 @@ function youtubeParse(html, callback) {
//--- Search Main Function ---// //--- Search Main Function ---//
function youtubeSearch(text, callback) { function youtubeSearch(text, callback) {
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `${constants.URLS.YT_URL}/results`, url: `${constants.URLS.YT_URL}/results`,
params: { q: text, hl: "en" } params: { q: text, hl: "en" },
}) })
.then((res) => { .then((res) => {
//--- Get HTML Only ---// //--- Get HTML Only ---//
@ -61,10 +83,7 @@ function youtubeSearch(text, callback) {
youtubeParse(html, (data) => { youtubeParse(html, (data) => {
callback(data); callback(data);
}) });
}) })
.catch((err) => { .catch((err) => {
logger(constants.LOGGER_NAMES.search, err, true); logger(constants.LOGGER_NAMES.search, err, true);
@ -78,9 +97,9 @@ const searchModule = {
//--- Get YouTube's Search Auto Complete ---// //--- Get YouTube's Search Auto Complete ---//
autoComplete(text, callback) { autoComplete(text, callback) {
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `${constants.URLS.YT_SUGGESTIONS}/search`, url: `${constants.URLS.YT_SUGGESTIONS}/search`,
params: { client: 'youtube', q: text } params: { client: "youtube", q: text },
}) })
.then((res) => { .then((res) => {
logger(constants.LOGGER_NAMES.autoComplete, res); logger(constants.LOGGER_NAMES.autoComplete, res);
@ -93,7 +112,6 @@ const searchModule = {
}, },
search(text, callback) { search(text, callback) {
let results = new Array(); let results = new Array();
youtubeSearch(text, (videos) => { youtubeSearch(text, (videos) => {
for (const i in videos) { for (const i in videos) {
@ -107,18 +125,15 @@ const searchModule = {
runtime: video.compactVideoRenderer.lengthText.runs[0].text, runtime: video.compactVideoRenderer.lengthText.runs[0].text,
uploaded: video.compactVideoRenderer.publishedTimeText.runs[0].text, uploaded: video.compactVideoRenderer.publishedTimeText.runs[0].text,
views: video.compactVideoRenderer.viewCountText.runs[0].text, views: video.compactVideoRenderer.viewCountText.runs[0].text,
thumbnails: video.compactVideoRenderer.thumbnail.thumbnails thumbnails: video.compactVideoRenderer.thumbnail.thumbnails,
}) });
} else { } else {
//--- If Entry Is Not A Video ---// //--- If Entry Is Not A Video ---//
//logger(constants.LOGGER_NAMES.search, { type: "Error Caught Successfully", error: video }, true); //logger(constants.LOGGER_NAMES.search, { type: "Error Caught Successfully", error: video }, true);
} }
} }
}) });
callback(results); callback(results);
}, },
getRemainingVideoInfo(id, callback) { getRemainingVideoInfo(id, callback) {
@ -128,45 +143,50 @@ const searchModule = {
}); });
}; };
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `${constants.URLS.YT_URL}/watch`, url: `${constants.URLS.YT_URL}/watch`,
params: { v: id } params: { v: id },
}) })
.then((res) => { .then((res) => {
let dataUpdated = res.data.decodeEscapeSequence() let dataUpdated = res.data.decodeEscapeSequence();
let likes = dataUpdated.split(`"defaultIcon":{"iconType":"LIKE"},"defaultText":{"runs":[{"text":"`)[1].split(`"}],"accessibility":`)[0] let likes = dataUpdated
let uploadDate = dataUpdated.split(`"uploadDate":"`)[1].split(`}},"trackingParams":"`)[0].slice(0, -2); .split(
`"defaultIcon":{"iconType":"LIKE"},"defaultText":{"runs":[{"text":"`
)[1]
.split(`"}],"accessibility":`)[0];
let uploadDate = dataUpdated
.split(`"uploadDate":"`)[1]
.split(`}},"trackingParams":"`)[0]
.slice(0, -2);
let data = { let data = {
"likes": likes, likes: likes,
"uploadDate": uploadDate uploadDate: uploadDate,
} };
logger("vidData", data) logger("vidData", data);
callback(data) callback(data);
}) })
.catch((err) => { .catch((err) => {
logger("codeRun", err, true); logger("codeRun", err, true);
callback(err); callback(err);
}); });
}, },
getReturnYoutubeDislike(id, callback) { getReturnYoutubeDislike(id, callback) {
Http.request({ Http.request({
method: 'GET', method: "GET",
url: `https://returnyoutubedislikeapi.com/votes`, url: `https://returnyoutubedislikeapi.com/votes`,
params: { videoId: id } params: { videoId: id },
}) })
.then((res) => { .then((res) => {
logger("rydData", res.data) logger("rydData", res.data);
callback(res.data) callback(res.data);
}) })
.catch((err) => { .catch((err) => {
logger("codeRun", err, true); logger("codeRun", err, true);
callback(err); callback(err);
}); });
} },
};
}
//--- Recommendations ---// //--- Recommendations ---//
@ -175,19 +195,20 @@ let InnertubeAPI;
// Loads Innertube object. This will be the object used in all future Innertube API calls. getAPI Code provided by Lightfire228 (https://github.com/Lightfire228) // Loads Innertube object. This will be the object used in all future Innertube API calls. getAPI Code provided by Lightfire228 (https://github.com/Lightfire228)
// These are just a way for the backend Javascript to communicate with the front end Vue scripts. Essentially a wrapper inside a wrapper // These are just a way for the backend Javascript to communicate with the front end Vue scripts. Essentially a wrapper inside a wrapper
const innertubeModule = { const innertubeModule = {
async getAPI() { async getAPI() {
if (!InnertubeAPI) { if (!InnertubeAPI) {
InnertubeAPI = await Innertube.createAsync((message, isError) => { logger(constants.LOGGER_NAMES.innertube, message, isError); }) InnertubeAPI = await Innertube.createAsync((message, isError) => {
logger(constants.LOGGER_NAMES.innertube, message, isError);
});
} }
return InnertubeAPI; return InnertubeAPI;
}, },
async getVid(id) { async getVid(id) {
try { try {
return await InnertubeAPI.VidInfoAsync(id) return await InnertubeAPI.VidInfoAsync(id);
} catch (error) { } catch (error) {
logger(constants.LOGGER_NAMES.watch, error, true) logger(constants.LOGGER_NAMES.watch, error, true);
} }
}, },
@ -195,38 +216,50 @@ const innertubeModule = {
// Front page recommendation // Front page recommendation
async recommend() { async recommend() {
const response = await InnertubeAPI.getRecommendationsAsync(); const response = await InnertubeAPI.getRecommendationsAsync();
if (!response.success) throw new Error("An error occurred and innertube failed to respond") if (!response.success)
throw new Error("An error occurred and innertube failed to respond");
const contents = response.data.contents.singleColumnBrowseResultsRenderer.tabs[0].tabRenderer.content.sectionListRenderer.contents const contents =
response.data.contents.singleColumnBrowseResultsRenderer.tabs[0]
.tabRenderer.content.sectionListRenderer.contents;
const final = contents.map((shelves) => { const final = contents.map((shelves) => {
const video = shelves.shelfRenderer?.content?.horizontalListRenderer?.items const video =
shelves.shelfRenderer?.content?.horizontalListRenderer?.items;
if (video) return video.map((item) => { if (video)
return video.map((item) => {
if (item) { if (item) {
const renderedItem = useRender(item[Object.keys(item)[0]], Object.keys(item)[0]) const renderedItem = useRender(
console.log(renderedItem) item[Object.keys(item)[0]],
return renderedItem Object.keys(item)[0]
} else {return undefined} );
}) console.log(renderedItem);
return renderedItem;
}) } else {
console.log(final) return undefined;
return final }
});
});
console.log(final);
return final;
}, },
// This is the recommendations that exist under videos // This is the recommendations that exist under videos
viewRecommends(recommendList) { viewRecommends(recommendList) {
if (recommendList) return recommendList.map((item) => { if (recommendList)
return recommendList.map((item) => {
if (item) { if (item) {
return useRender(item[Object.keys(item)[0]], Object.keys(item)[0]) return useRender(item[Object.keys(item)[0]], Object.keys(item)[0]);
} else {return undefined} } else {
}) return undefined;
},
} }
});
},
};
//--- Start ---// //--- Start ---//
export default ({ app }, inject) => { export default ({ app }, inject) => {
inject('youtube', {...searchModule, ...innertubeModule }) inject("youtube", { ...searchModule, ...innertubeModule });
inject("logger", logger) inject("logger", logger);
} };
logger(constants.LOGGER_NAMES.init, "Program Started"); logger(constants.LOGGER_NAMES.init, "Program Started");

View file

@ -1,18 +1,18 @@
export const state = () => ({ export const state = () => ({
roundTweak: 0 roundTweak: 0,
}) });
export const mutations = { export const mutations = {
initTweaks(state) { initTweaks(state) {
// NOTE: localStorage is not reactive, so it will only be used on first load // NOTE: localStorage is not reactive, so it will only be used on first load
// currently called beforeCreate() in pages/default.vue // currently called beforeCreate() in pages/default.vue
if (process.client) { if (process.client) {
state.roundTweak = localStorage.getItem("roundTweak") || 0 state.roundTweak = localStorage.getItem("roundTweak") || 0;
} }
}, },
setRoundTweak(state, payload) { setRoundTweak(state, payload) {
if (!isNaN(payload)) { if (!isNaN(payload)) {
state.roundTweak = payload state.roundTweak = payload;
localStorage.setItem("roundTweak", payload) localStorage.setItem("roundTweak", payload);
}
}
} }
},
};