Merge pull request #239 from 404-Program-not-found/main

Display Channel info, Comments, fixed bug with descriptions.
This commit is contained in:
Kenny 2022-04-11 08:24:10 -04:00 committed by GitHub
commit ca8151f21d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 731 additions and 262 deletions

View File

@ -0,0 +1,87 @@
<template>
<div id="comment">
<a
:href="this.$rendererUtils.getNavigationEndpoints(comment.authorEndpoint)"
class="avatar-link pt-2"
>
<v-img
class="avatar-thumbnail"
:src="
comment.authorThumbnail.thumbnails[
comment.authorThumbnail.thumbnails.length - 1
].url
"
/>
</a>
<v-card-text class="comment-info pt-2">
<div
v-for="title in comment.title.runs"
:key="title.text"
style="margin-top: 0.5em"
class="vid-title"
>
{{ title.text }}
</div>
<div
class="caption background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
v-text="parseBottom(comment)"
/>
</v-card-text>
</div>
</template>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
}
.vid-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
.avatar-thumbnail {
margin-top: 0.5rem;
margin-left: 0.5rem;
border-radius: 50%;
width: 50px;
height: 50px;
}
#details {
display: flex;
flex-direction: row;
flex-basis: auto;
padding: 10px;
}
@media screen and (orientation: landscape) {
.entry {
margin-bottom: 8px;
}
#details {
flex-direction: column-reverse;
}
}
</style>
<script>
export default {
props: ["comment"],
methods: {
parseBottom(comment) {
const bottomText = [
comment.subscriberCountText?.runs[0].text,
comment.videoCountText?.runs.map((run) => run.text).join(" "),
];
return bottomText.join(" · ");
},
},
};
</script>

View File

@ -32,7 +32,7 @@
<a <a
:href=" :href="
this.$rendererUtils.getNavigationEndpoints( this.$rendererUtils.getNavigationEndpoints(
video.shortBylineText.runs[0].navigationEndpoint video.shortBylineText.runs[0]
) )
" "
class="avatar-link pt-2" class="avatar-link pt-2"

View File

@ -38,12 +38,14 @@
import compactVideoRenderer from "~/components/CompactRenderers/compactVideoRenderer.vue"; import compactVideoRenderer from "~/components/CompactRenderers/compactVideoRenderer.vue";
import compactChannelRenderer from "~/components/CompactRenderers/compactChannelRenderer.vue"; import compactChannelRenderer from "~/components/CompactRenderers/compactChannelRenderer.vue";
import gridVideoRenderer from "~/components/gridRenderers/gridVideoRenderer.vue"; import gridVideoRenderer from "~/components/gridRenderers/gridVideoRenderer.vue";
import videoWithContextRenderer from "~/components/gridRenderers/videoWithContextRenderer.vue";
export default { export default {
components: { components: {
gridVideoRenderer, gridVideoRenderer,
compactVideoRenderer, compactVideoRenderer,
compactChannelRenderer, compactChannelRenderer,
videoWithContextRenderer,
}, },
props: ["render"], props: ["render"],

View File

@ -1,20 +1,20 @@
<template> <template>
<div class="description"> <div class="description" v-if="render.descriptionBodyText">
<template v-for="(text, index) in render.description.runs"> <template v-for="(text, index) in render.descriptionBodyText.runs">
<template <template v-if="$rendererUtils.checkInternal(text)">
v-if="
text.navigationEndpoint && text.navigationEndpoint.webviewEndpoint
"
>
<a <a
@click="openExternal($rendererUtils.getNavigationEndpoints(text))" @click="openInternal($rendererUtils.getNavigationEndpoints(text))"
:key="index" :key="index"
>{{ text.text }}</a >{{ text.text }}</a
> >
</template> </template>
<template v-else-if="$rendererUtils.checkInternal(text)"> <template
v-else-if="
text.navigationEndpoint && text.navigationEndpoint.urlEndpoint
"
>
<a <a
@click="openInternal($rendererUtils.getNavigationEndpoints(text))" @click="openExternal($rendererUtils.getNavigationEndpoints(text))"
:key="index" :key="index"
>{{ text.text }}</a >{{ text.text }}</a
> >
@ -27,7 +27,6 @@
<style scoped> <style scoped>
.description { .description {
white-space: pre-line; white-space: pre-line;
margin-bottom: 16px;
} }
</style> </style>

View File

@ -0,0 +1,151 @@
<template>
<v-card
class="entry videoWithContextRenderer background"
:to="`/watch?v=${video.videoId}`"
flat
>
<div style="position: relative" class="thumbnail-container">
<v-img
v-if="video.thumbnail"
:aspect-ratio="16 / 9"
:src="
$youtube.getThumbnail(
video.videoId,
'max',
video.thumbnail.thumbnails
)
"
/>
<div
v-if="video.thumbnailOverlays"
class="videoRuntimeFloat"
:class="
'style-' +
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.style
"
style="color: #fff"
v-text="
video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer.text
.runs[0].text
"
/>
</div>
<div id="details">
<a
:href="
$rendererUtils.getNavigationEndpoints(
video.shortBylineText.runs[0].navigationEndpoint
)
"
class="avatar-link pt-2"
>
<v-img
class="avatar-thumbnail"
:src="
video.channelThumbnail.channelThumbnailWithLinkRenderer.thumbnail
.thumbnails[0].url
"
/>
</a>
<v-card-text class="video-info pt-2">
<div
v-for="title in video.headline.runs"
:key="title.text"
style="margin-top: 0.5em"
class="font-weight-medium vid-title"
>
{{ title.text }}
</div>
<div
class="background--text text--lighten-5 caption"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
v-text="parseBottom(video)"
/>
</v-card-text>
</div>
</v-card>
</template>
<style scoped>
.entry {
width: 100%; /* Prevent Loading Weirdness */
}
.videoRuntimeFloat {
position: absolute;
bottom: 10px;
right: 10px;
border-radius: 5px;
padding: 0px 4px 0px 4px;
}
.videoRuntimeFloat.style-DEFAULT {
background: rgba(0, 0, 0, 0.5);
}
.videoRuntimeFloat.style-LIVE {
background: rgba(255, 0, 0, 0.5);
}
.vid-title {
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}
.avatar-thumbnail {
margin-top: 0.5rem;
margin-left: 0.5rem;
border-radius: 50%;
width: 35px;
height: 35px;
}
#details {
display: flex;
flex-direction: row;
flex-basis: auto;
}
@media screen and (orientation: landscape) {
.entry {
margin-bottom: 8px;
}
.thumbnail-container {
width: 50vh;
float: left;
}
#details {
flex-direction: column-reverse;
}
.avatar-thumbnail {
margin-top: 0;
margin-left: 16px;
}
.video-info {
padding-top: 0 !important;
padding-bottom: 0;
margin-top: 0;
}
}
</style>
<script>
export default {
props: ["video"],
methods: {
parseBottom(video) {
const bottomText = [
video.shortBylineText?.runs[0].text,
video.shortViewCountText?.runs[0].text,
];
if (video.publishedTimeText?.runs[0].text)
bottomText.push(video.publishedTimeText?.runs[0].text);
return bottomText.join(" · ");
},
},
};
</script>

View File

@ -60,7 +60,7 @@
import { App as CapacitorApp } from "@capacitor/app"; import { App as CapacitorApp } from "@capacitor/app";
import { mapState } from "vuex"; import { mapState } from "vuex";
import constants from "~/plugins/constants"; import constants from "~/plugins/constants";
import { linkParser } from "~/plugins/utils" import { linkParser } from "~/plugins/utils";
export default { export default {
data: () => ({ data: () => ({
@ -114,19 +114,30 @@ export default {
this.$router.push(slug.pathname + slug.search); this.$router.push(slug.pathname + slug.search);
} }
}); });
// --- Import Twemoji ---///
const plugin = document.createElement("script");
plugin.setAttribute("src", "//twemoji.maxcdn.com/v/latest/twemoji.min.js");
plugin.setAttribute("crossorigin", "anonymous");
document.head.appendChild(plugin);
}, },
methods: { methods: {
textChanged(text) { textChanged(text) {
if (text.length <= 0) this.response = []; // No text found, no point in calling API if (text.length <= 0) {
this.response = [];
return;
} // No text found, no point in calling API
//--- User Pastes Link, Direct Them To Video ---// //--- User Pastes Link, Direct Them To Video ---//
const isLink = linkParser(text); const isLink = linkParser(text);
if (isLink) { if (isLink) {
this.response = [{ this.response = [
text: `Watch video from ID: ${isLink}`, {
id: isLink text: `Watch video from ID: ${isLink}`,
}]; id: isLink,
},
];
return; return;
} }
//--- End User Pastes Link, Direct Them To Video ---// //--- End User Pastes Link, Direct Them To Video ---//
@ -139,9 +150,7 @@ export default {
}, },
youtubeSearch(item) { youtubeSearch(item) {
const link = item.id const link = item.id ? `/watch?v=${item.id}` : `/search?q=${item[0]}`;
? `/watch?v=${item.id}`
: `/search?q=${item[0]}`
this.$router.push(link); this.$router.push(link);
this.search = false; this.search = false;
}, },

View File

@ -25,15 +25,20 @@ export default {
const theming = new Promise((resolve) => const theming = new Promise((resolve) =>
// Set timeout is required for $vuetify.theme... dont ask me why -Front // Set timeout is required for $vuetify.theme... dont ask me why -Front
setTimeout(() => { setTimeout(() => {
this.$vuetify.theme.dark = JSON.parse(localStorage.getItem("darkTheme")) === true; this.$vuetify.theme.dark =
JSON.parse(localStorage.getItem("darkTheme")) === true;
if (localStorage.getItem("primaryDark") != null) if (localStorage.getItem("primaryDark") != null)
this.$vuetify.theme.themes.dark.primary = localStorage.getItem("primaryDark"); this.$vuetify.theme.themes.dark.primary =
localStorage.getItem("primaryDark");
if (localStorage.getItem("primaryLight") != null) if (localStorage.getItem("primaryLight") != null)
this.$vuetify.theme.themes.light.primary = localStorage.getItem("primaryLight"); this.$vuetify.theme.themes.light.primary =
localStorage.getItem("primaryLight");
if (localStorage.getItem("backgroundDark") != null) if (localStorage.getItem("backgroundDark") != null)
this.$vuetify.theme.themes.dark.background = localStorage.getItem("backgroundDark"); this.$vuetify.theme.themes.dark.background =
localStorage.getItem("backgroundDark");
if (localStorage.getItem("backgroundLight") != null) if (localStorage.getItem("backgroundLight") != null)
this.$vuetify.theme.themes.light.background = localStorage.getItem("backgroundLight"); this.$vuetify.theme.themes.light.background =
localStorage.getItem("backgroundLight");
this.$vuetube.navigationBar.setTheme( this.$vuetube.navigationBar.setTheme(
this.$vuetify.theme.currentTheme.background, this.$vuetify.theme.currentTheme.background,
!this.$vuetify.theme.dark !this.$vuetify.theme.dark

View File

@ -1,29 +1,44 @@
<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 flat class="card my-2 background" :class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'" :style="{borderRadius: `${roundTweak / 2}rem`}"> <v-card
flat
class="card my-2 background"
:class="$vuetify.theme.dark ? 'lighten-1' : 'darken-1'"
:style="{ borderRadius: `${roundTweak / 2}rem` }"
>
<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 class="subtitle background--text" :class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'" <span
v-text="`• ${item.sha.substring(0, 7)}`" /> class="subtitle background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
v-text="`• ${item.sha.substring(0, 7)}`"
/>
<v-spacer /> <v-spacer />
<v-chip v-if="index == 0" class="tags" color="orange" style=" <v-chip
border-radius: 0.5rem; v-if="index == 0"
border: 2px var(--v-orange-base); class="tags"
"> color="orange"
style="border-radius: 0.5rem; border: 2px var(--v-orange-base)"
>
Latest Latest
</v-chip> </v-chip>
<v-chip v-if="item.sha == installedVersion" class="tags" color="green" style=" <v-chip
border-radius: 0.5rem; v-if="item.sha == installedVersion"
border: 2px var(--v-green-base); class="tags"
"> color="green"
style="border-radius: 0.5rem; border: 2px var(--v-green-base)"
>
Installed Installed
</v-chip> </v-chip>
</v-card-title> </v-card-title>
<div style="margin-left: 1em"> <div style="margin-left: 1em">
<div class="date background--text" :class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'" <div
v-text="new Date(item.commit.committer.date).toLocaleString()" /> class="date background--text"
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
v-text="new Date(item.commit.committer.date).toLocaleString()"
/>
{{ item.commit.message }} {{ item.commit.message }}
</div> </div>
@ -42,69 +57,65 @@
</template> </template>
<style scoped> <style scoped>
.card { .card {
width: 100%; width: 100%;
} }
.subtitle { .subtitle {
margin: 0.4em; margin: 0.4em;
font-size: 0.75em; font-size: 0.75em;
transform: translateY(5%); transform: translateY(5%);
} }
.date { .date {
transform: translateY(-40%); transform: translateY(-40%);
} }
.btn-icon { .btn-icon {
margin-right: 0.25em; margin-right: 0.25em;
} }
.tags {
margin-left: 0.5em;
}
.tags {
margin-left: 0.5em;
}
</style> </style>
<script> <script>
import { Browser } from "@capacitor/browser"; import { Browser } from "@capacitor/browser";
export default { export default {
computed: {
computed: { roundTweak() {
roundTweak() { return this.$store.state.tweaks.roundTweak;
return this.$store.state.tweaks.roundTweak;
}
},
data() {
return {
commits: new Array(),
installedVersion: process.env.appVersion,
};
}, },
async mounted() { },
const commits = await this.$vuetube.commits; data() {
if (commits[0].sha) { return {
//If Commit Valid commits: new Array(),
this.commits = commits; installedVersion: process.env.appVersion,
} else { };
console.log(commits); },
} async mounted() {
const commits = await this.$vuetube.commits;
if (commits[0].sha) {
//If Commit Valid
this.commits = commits;
} else {
console.log(commits);
}
},
methods: {
async openExternal(item) {
await Browser.open({
url: item.html_url,
});
}, },
methods: {
async openExternal(item) {
await Browser.open({
url: item.html_url
});
},
install(item) { install(item) {
this.$vuetube.getRuns(item, (data) => { this.$vuetube.getRuns(item, (data) => {
console.log(data); console.log(data);
}); });
},
}, },
}; },
};
</script> </script>

View File

@ -14,7 +14,6 @@
<!-- Dev Mode Open --> <!-- Dev Mode Open -->
<v-btn text class="entry" @click="dev()" /> <v-btn text class="entry" @click="dev()" />
</div> </div>
</template> </template>
@ -51,7 +50,7 @@ export default {
to: "/mods/tweaks", to: "/mods/tweaks",
}, },
{ name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" }, { name: "Startup Options", icon: "mdi-restart", to: "/mods/startup" },
{ name: "Plugins", icon: "mdi-puzzle", to: "", to: "/mods/plugins", disabled: true }, { name: "Plugins", icon: "mdi-puzzle", to: "", to: "/mods/plugins" },
{ {
name: "Updates", name: "Updates",
icon: "mdi-cloud-download-outline", icon: "mdi-cloud-download-outline",
@ -68,7 +67,7 @@ export default {
if (this.devClicks >= 6) { if (this.devClicks >= 6) {
this.$router.push("/mods/developer"); this.$router.push("/mods/developer");
} }
} },
} },
}; };
</script> </script>

View File

@ -1,108 +1,183 @@
<template> <template>
<div class="background"> <div class="background" id="watch-body">
<!-- Stock Player --> <div
<videoPlayer class="player-container"
style="position: sticky; top: 0; z-index: 696969" style="position: sticky; top: 0; z-index: 696969"
:vid-src="vidSrc" >
ref="player" <!-- Stock Player -->
v-if="useBetaPlayer !== 'true'" <videoPlayer
/> :vid-src="vidSrc"
ref="player"
<!-- VueTube Player V1 --> v-if="useBetaPlayer !== 'true'"
<vuetubePlayer :sources="sources" v-if="useBetaPlayer === 'true'" />
<v-card v-if="loaded" class="ml-2 mr-2 background" flat>
<v-card-title
class="mt-2"
style="
padding-top: 0;
padding-bottom: 0;
font-size: 0.95rem;
line-height: 1rem;
"
v-text="title"
/> />
<v-card-text>
<div style="margin-bottom: 1rem">
{{ views }} views {{ uploaded }}
</div>
<!-- Scrolling Div For Interactions ---> <!-- VueTube Player V1 -->
<div style="display: flex; margin-bottom: 1em"> <vuetubePlayer :sources="sources" v-if="useBetaPlayer === 'true'" />
<v-list-item </div>
v-for="(item, index) in interactions" <div class="content-container">
:key="index" <v-card v-if="loaded" class="ml-2 mr-2 background" flat>
style="padding: 0; flex: 0 0 20%" <v-card-title
> class="mt-2"
<v-btn style="
text padding-top: 0;
class="vertical-button" padding-bottom: 0;
style="padding: 0; margin: 0" font-size: 0.95rem;
elevation="0" line-height: 1rem;
:disabled="item.disabled" "
@click="callMethodByName(item.actionName)" v-text="video.title"
/>
<v-card-text>
<div style="margin-bottom: 1rem">
<template
v-for="text in video.metadata.contents.find(
(content) => content.slimVideoInformationRenderer
).slimVideoInformationRenderer.collapsedSubtitle.runs"
>{{ text.text }}</template
> >
<v-icon v-text="item.icon" /> </div>
<div
class="mt-2"
style="font-size: 0.66rem"
v-text="item.value || item.name"
/>
</v-btn>
</v-list-item>
<v-spacer /> <!-- Scrolling Div For Interactions --->
<v-btn text @click="showMore = !showMore"> <div style="display: flex; margin-bottom: 1em">
<v-icon v-if="showMore">mdi-chevron-up</v-icon> <v-list-item
<v-icon v-else>mdi-chevron-down</v-icon> v-for="(item, index) in interactions"
</v-btn> :key="index"
style="padding: 0; flex: 0 0 20%"
>
<v-btn
text
class="vertical-button"
style="padding: 0; margin: 0"
elevation="0"
:disabled="item.disabled"
@click="callMethodByName(item.actionName)"
>
<v-icon v-text="item.icon" />
<div
class="mt-2"
style="font-size: 0.66rem"
v-text="item.value || item.name"
/>
</v-btn>
</v-list-item>
<v-spacer />
<v-btn text @click="showMore = !showMore">
<v-icon v-if="showMore">mdi-chevron-up</v-icon>
<v-icon v-else>mdi-chevron-down</v-icon>
</v-btn>
</div>
<!-- End Scrolling Div For Interactions --->
<!-- <hr /> -->
</v-card-text>
<!-- <v-bottom-sheet
v-model="showMore"
color="background"
style="z-index: 9999999"
>
<v-sheet style="padding: 12px">
<v-btn block @click="showMore = !showMore"
><v-icon>mdi-chevron-down</v-icon></v-btn
><br />
<slim-video-description-renderer
class="scroll-y"
:render="video.renderedData.description"
/>
</v-sheet>
</v-bottom-sheet> -->
<!-- <v-bottom-sheet v-model="share" color="background" style="z-index: 9999999">
<v-sheet style="padding: 1em">
<div class="scroll-y">
{{ response.renderedData.description }}
</div> </div>
<!-- End Scrolling Div For Interactions ---> </v-sheet>
<!-- <hr /> --> </v-bottom-sheet> -->
<p>Channel Stuff</p> </v-card>
</v-card-text> <v-divider />
<div v-if="showMore" class="scroll-y ml-2 mr-2">
<slim-video-description-renderer :render="description" /> <!-- Channel Bar -->
<div class="channel-container" v-if="loaded">
<v-card class="channel-section background" :to="video.channelUrl">
<div id="details">
<div class="avatar-link mr-3">
<v-img class="avatar-thumbnail" :src="video.channelImg" />
</div>
<div class="channel-byline">
<div class="channel-name" v-text="video.channelName" />
<div
class="caption background--text"
:class="
$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'
"
v-text="video.channelSubs"
/>
</div>
</div>
<div
class="channel-buttons"
style="color: rgb(204, 0, 0); text-transform: uppercase"
>
subscribe
</div>
</v-card>
<v-divider />
</div> </div>
<!-- <v-bottom-sheet <!-- Description -->
v-model="showMore" <div class="description-container" v-if="showMore">
color="background" <div class="scroll-y ma-4">
style="z-index: 9999999" <slim-video-description-renderer
> :render="video.renderedData.description"
<v-sheet style="padding: 1em"> />
<v-btn block @click="showMore = !showMore" </div>
><v-icon>mdi-chevron-down</v-icon></v-btn <v-divider />
><br /> </div>
<div class="scroll-y"> <!-- Comments -->
{{ description }} <div class="comment-container" v-if="loaded && video.commentData">
</div> <v-card flat class="background comment-renderer">
</v-sheet> <v-text class="comment-count keep-spaces">
</v-bottom-sheet> --> <template v-for="text in video.commentData.headerText.runs">
<!-- <v-bottom-sheet v-model="share" color="background" style="z-index: 9999999"> <template v-if="text.bold">
<v-sheet style="padding: 1em"> <strong :key="text.text">{{ text.text }}</strong>
<div class="scroll-y"> </template>
{{ description }} <template v-else>{{ text.text }}</template>
</div> </template>
</v-sheet> </v-text>
</v-bottom-sheet> --> <v-icon>mdi-unfold-more-horizontal</v-icon>
</v-card> </v-card>
<vid-load-renderer v-if="!recommends" /> <v-divider />
<shelf-renderer v-else :render="recommends" /> </div>
<!-- Related Videos -->
<div class="loaders" v-if="!loaded">
<v-skeleton-loader
type="list-item-two-line, actions, divider, list-item-avatar, divider, list-item-three-line"
/>
<vid-load-renderer :count="5" />
</div>
<item-section-renderer v-else :render="recommends" />
</div>
</div> </div>
</template> </template>
<script> <script>
import { Share } from "@capacitor/share"; import { Share } from "@capacitor/share";
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
import VidLoadRenderer from "~/components/vidLoadRenderer.vue"; import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
import { getCpn } from "~/plugins/utils"; import { getCpn } from "~/plugins/utils";
import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue"; import SlimVideoDescriptionRenderer from "~/components/UtilRenderers/slimVideoDescriptionRenderer.vue";
import vuetubePlayer from "~/components/Player/index.vue" import ItemSectionRenderer from "~/components/SectionRenderers/itemSectionRenderer.vue";
import vuetubePlayer from "~/components/Player/index.vue";
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
export default { export default {
components: { ShelfRenderer, VidLoadRenderer, SlimVideoDescriptionRenderer, vuetubePlayer }, components: {
ShelfRenderer,
VidLoadRenderer,
SlimVideoDescriptionRenderer,
vuetubePlayer,
ItemSectionRenderer,
},
data() { data() {
return { return {
interactions: [ interactions: [
@ -131,16 +206,12 @@ export default {
], ],
showMore: false, showMore: false,
// share: false, // share: false,
title: null,
uploaded: null,
vidSrc: null, vidSrc: null,
sources: [], sources: [],
description: null,
views: null,
recommends: null, recommends: null,
loaded: false, loaded: false,
interval: null, interval: null,
video: null,
useBetaPlayer: false, useBetaPlayer: false,
}; };
}, },
@ -166,7 +237,7 @@ export default {
this.startTime = Math.floor(Date.now() / 1000); this.startTime = Math.floor(Date.now() / 1000);
this.getVideo(); this.getVideo();
this.useBetaPlayer = localStorage.getItem('debug.BetaPlayer'); this.useBetaPlayer = localStorage.getItem("debug.BetaPlayer");
}, },
destroyed() { destroyed() {
@ -174,10 +245,10 @@ export default {
}, },
methods: { methods: {
getVideo() { getVideo() {
this.likes = 100;
this.loaded = false; this.loaded = false;
this.$youtube.getVid(this.$route.query.v).then((result) => { this.$youtube.getVid(this.$route.query.v).then((result) => {
this.video = result;
console.log("Video info data", result); console.log("Video info data", result);
console.log(result.availableResolutions); console.log(result.availableResolutions);
@ -191,14 +262,9 @@ export default {
].url; // Takes the highest available resolution with both video and Audio. Note this will be lower than the actual highest resolution ].url; // Takes the highest available resolution with both video and Audio. Note this will be lower than the actual highest resolution
//--- Content Stuff ---// //--- Content Stuff ---//
this.title = result.title;
this.description = result.renderedData.description; // While this works, I do recommend using the rendered description instead in the future as there are some things a pure string wouldn't work with
this.views = parseInt(result.metadata.viewCount).toLocaleString();
this.likes = result.metadata.likes.toLocaleString(); this.likes = result.metadata.likes.toLocaleString();
this.uploaded = result.metadata.uploadDate;
this.interactions[0].value = result.metadata.likes.toLocaleString(); this.interactions[0].value = result.metadata.likes.toLocaleString();
this.loaded = true; this.loaded = true;
this.recommends = result.renderedData.recommendations; this.recommends = result.renderedData.recommendations;
// .catch((error) => this.$logger("Watch", error, true)); // .catch((error) => this.$logger("Watch", error, true));
console.log("recommendations:", this.recommends); console.log("recommendations:", this.recommends);
@ -227,8 +293,8 @@ export default {
async share() { async share() {
// this.share = !this.share; // this.share = !this.share;
await Share.share({ await Share.share({
title: this.title, title: this.video.title,
text: this.title, text: this.video.title,
url: "https://youtu.be/" + this.$route.query.v, url: "https://youtu.be/" + this.$route.query.v,
dialogTitle: "Share video", dialogTitle: "Share video",
}); });
@ -274,8 +340,55 @@ export default {
</script> </script>
<style> <style>
#watch-body {
height: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.content-container {
overflow-y: auto;
height: 100%;
}
.vertical-button span.v-btn__content { .vertical-button span.v-btn__content {
flex-direction: column; flex-direction: column;
justify-content: space-around; justify-content: space-around;
} }
.channel-section,
.comment-renderer {
display: flex;
align-items: center;
padding: 12px;
}
.channel-section #details,
.comment-renderer .comment-count {
flex-grow: 1;
display: flex;
align-items: center;
min-width: 0;
}
.channel-section .channel-byline {
min-width: 0;
}
.channel-section .avatar-thumbnail {
border-radius: 50%;
width: 35px;
height: 35px;
}
.channel-section .channel-name {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.keep-spaces {
white-space: pre-wrap;
}
</style> </style>

View File

@ -12,7 +12,7 @@ const url = {
const ytApiVal = { const ytApiVal = {
VERSION: "16.25", VERSION: "16.25",
CLIENTNAME: "ANDROID", CLIENTNAME: "ANDROID",
VERSION_WEB: "2.20220318.00.00", VERSION_WEB: "2.20220331.06.00",
CLIENT_WEB: 2, CLIENT_WEB: 2,
}; };

View File

@ -2,7 +2,8 @@
// 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, delay } from "./utils";
import rendererUtils from "./renderers";
import constants from "./constants"; import constants from "./constants";
class Innertube { class Innertube {
@ -39,12 +40,27 @@ class Innertube {
} }
} catch (err) { } catch (err) {
console.log(err); console.log(err);
if (this.checkErrorCallback) this.ErrorCallback(err, true); if (this.checkErrorCallback) {
if (this.retry_count >= 10) { this.ErrorCallback(html.data, true);
this.initAsync(); this.ErrorCallback(err, true);
}
if (this.retry_count < 10) {
this.retry_count += 1;
if (this.checkErrorCallback)
this.ErrorCallback(
`retry count: ${this.retry_count}`,
false,
`An error occurred while trying to init the innertube API. Retrial number: ${this.retry_count}/10`
);
await delay(5000);
await this.initAsync();
} else { } else {
if (this.checkErrorCallback) if (this.checkErrorCallback)
this.ErrorCallback("Failed to retrieve Innertube session", true); this.ErrorCallback(
"Failed to retrieve Innertube session",
true,
"An error occurred while retrieving the innertube session. Check the Logs for more information."
);
} }
} }
} catch (error) { } catch (error) {
@ -95,8 +111,11 @@ class Innertube {
}; };
} }
async getContinuationsAsync(continuation, type) { async getContinuationsAsync(continuation, type, contextAdditional = {}) {
let data = { context: this.context, continuation: continuation }; let data = {
context: { ...this.context, ...contextAdditional },
continuation: continuation,
};
let url; let url;
switch (type) { switch (type) {
case "browse": case "browse":
@ -135,7 +154,17 @@ class Innertube {
let data = { context: this.context, videoId: id }; let data = { context: this.context, videoId: id };
const responseNext = await Http.post({ const responseNext = await Http.post({
url: `${constants.URLS.YT_BASE_API}/next?key=${this.key}`, url: `${constants.URLS.YT_BASE_API}/next?key=${this.key}`,
data: data, data: {
...data,
...{
context: {
client: {
clientName: constants.YT_API_VALUES.CLIENT_WEB,
clientVersion: constants.YT_API_VALUES.VERSION_WEB,
},
},
},
},
headers: constants.INNERTUBE_HEADER(this.context.client), headers: constants.INNERTUBE_HEADER(this.context.client),
}).catch((error) => error); }).catch((error) => error);
@ -251,64 +280,72 @@ class Innertube {
const resolutions = responseInfo.streamingData; const resolutions = responseInfo.streamingData;
const columnUI = const columnUI =
responseNext.contents.singleColumnWatchNextResults.results.results; responseNext.contents.singleColumnWatchNextResults.results.results;
const vidMetadata = columnUI.contents.find(
(content) => content.slimVideoMetadataSectionRenderer
).slimVideoMetadataSectionRenderer;
const recommendations = columnUI?.contents.find(
(content) => content?.itemSectionRenderer?.targetId == "watch-next-feed"
).itemSectionRenderer;
const ownerData = vidMetadata.contents.find(
(content) => content.slimOwnerRenderer
)?.slimOwnerRenderer;
const vidData = { const vidData = {
id: details.videoId, id: details.videoId,
title: details.title, title: details.title,
isLive: details.isLiveContent, isLive: details.isLiveContent,
channelName: details.author, channelName: details.author,
channelUrl: channelSubs: ownerData?.collapsedSubtitle?.runs[0]?.text,
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find( channelUrl: rendererUtils.getNavigationEndpoints(ownerData),
(contents) => contents.elementRenderer channelImg: ownerData?.thumbnail?.thumbnails[0].url,
)?.newElement?.type?.componentType?.model?.channelBarModel
?.videoChannelBarData?.onTap?.innertubeCommand?.browseEndpoint
?.canonicalBaseUrl,
channelImg:
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(
(contents) => contents.elementRenderer
)?.newElement?.type?.componentType?.model?.channelBarModel
?.videoChannelBarData?.avatar?.image?.sources[0].url,
availableResolutions: resolutions?.formats, availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats, availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: { metadata: {
contents: vidMetadata.contents,
description: details.shortDescription, description: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails, thumbnails: details.thumbnails?.thumbnails,
uploadDate:
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(
(contents) => contents.slimVideoDescriptionRenderer
)?.slimVideoDescriptionRenderer.publishDate.runs[0].text,
isPrivate: details.isPrivate, isPrivate: details.isPrivate,
viewCount: details.viewCount, viewCount: details.viewCount,
lengthSeconds: details.lengthSeconds, lengthSeconds: details.lengthSeconds,
likes: parseInt( likes: parseInt(
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents vidMetadata.contents
.find((contents) => contents.slimVideoScrollableActionBarRenderer) .find((content) => content.slimVideoActionBarRenderer)
?.slimVideoScrollableActionBarRenderer.buttons.find( .slimVideoActionBarRenderer.buttons.find(
(button) => button.slimMetadataToggleButtonRenderer.isLike == true (button) => button.slimMetadataToggleButtonRenderer.isLike
) )
?.slimMetadataToggleButtonRenderer?.button.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace( .slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(
/\D/g, /\D/g,
"" ""
) )
), // Yes. I know. ), // Yes. I know.
}, },
renderedData: { renderedData: {
description: columnUI?.contents description: responseNext.engagementPanels
.find((contents) => contents.slimVideoMetadataSectionRenderer) .find(
.slimVideoMetadataSectionRenderer?.contents.find( (panel) =>
(contents) => contents.slimVideoDescriptionRenderer panel.engagementPanelSectionListRenderer.panelIdentifier ==
)?.slimVideoDescriptionRenderer, "video-description-ep-identifier"
recommendations: columnUI?.contents.find( )
(contents) => contents.shelfRenderer .engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find(
).shelfRenderer, (item) => item.expandableVideoDescriptionBodyRenderer
).expandableVideoDescriptionBodyRenderer,
recommendations: recommendations,
recommendationsContinuation: recommendationsContinuation:
columnUI?.continuations[0].reloadContinuationData?.continuation, recommendations.contents[recommendations.contents.length - 1]
.continuationItemRenderer?.continuationEndpoint.continuationCommand
.token,
}, },
engagementPanels: responseNext.engagementPanels,
commentData: columnUI.contents
.find((content) => content.itemSectionRenderer?.contents)
?.itemSectionRenderer.contents.find(
(content) => content.commentsEntryPointHeaderRenderer
)?.commentsEntryPointHeaderRenderer,
playbackTracking: responseInfo.playbackTracking, playbackTracking: responseInfo.playbackTracking,
}; };
console.log(vidData);
return vidData; return vidData;
} }

View File

@ -3,24 +3,33 @@ class rendererUtils {
static getNavigationEndpoints(base) { static getNavigationEndpoints(base) {
const navEndpoint = base.navigationEndpoint; const navEndpoint = base.navigationEndpoint;
if (!navEndpoint) return; if (!navEndpoint) return;
if (navEndpoint.webviewEndpoint) { if (navEndpoint.urlEndpoint) {
return navEndpoint.webviewEndpoint.url; const params = new Proxy(
new URLSearchParams(navEndpoint.urlEndpoint.url),
{
get: (searchParams, prop) => searchParams.get(prop),
}
);
if (params.q) return decodeURI(params.q);
else return new URL(navEndpoint.urlEndpoint.url).pathname;
} else if (navEndpoint.browseEndpoint) { } else if (navEndpoint.browseEndpoint) {
return navEndpoint.browseEndpoint.canonicalBaseUrl; return navEndpoint.browseEndpoint.canonicalBaseUrl;
} else if (navEndpoint.watchEndpoint) { } else if (navEndpoint.watchEndpoint) {
return `/watch?v=${navEndpoint.watchEndpoint.videoId}`; return `/watch?v=${navEndpoint.watchEndpoint.videoId}`;
} else if (navEndpoint.navigationEndpoint) { } else if (navEndpoint.navigationEndpoint) {
return; //for now return; //for now
} else if (navEndpoint.searchEndpoint) {
return `/search?q=${encodeURI(navEndpoint.searchEndpoint.query)}`;
} }
} }
static checkInternal(base) { static checkInternal(base) {
const navEndpoint = base.navigationEndpoint; const tmp = document.createElement("a");
if (!navEndpoint) return false; tmp.href = this.getNavigationEndpoints(base);
if (navEndpoint.browseEndpoint || navEndpoint.watchEndpoint) { if (tmp.host !== window.location.host || !base.navigationEndpoint) {
return true;
} else {
return false; return false;
} else {
return true;
} }
} }
} }

View File

@ -14,22 +14,24 @@ const ensureStructure = new Promise(async (resolve, reject) => {
//--- Ensure Plugins Folder ---// //--- Ensure Plugins Folder ---//
try { try {
await Filesystem.mkdir({ directory: APP_DIRECTORY, recursive: true, await Filesystem.mkdir({
directory: APP_DIRECTORY, recursive: true,
path: fs.plugins, path: fs.plugins,
}); });
} catch (e) { /* Exists */ } } catch (e) { /* Exists */ }
//--- Ensure Temp Folder ---// //--- Ensure Temp Folder ---//
try { try {
await Filesystem.mkdir({ directory: APP_DIRECTORY, recursive: true, await Filesystem.mkdir({
directory: APP_DIRECTORY, recursive: true,
path: fs.temp, path: fs.temp,
}); });
} catch (e) { /* Exists */ } } catch (e) { /* Exists */ }
perms perms
? resolve(true) ? resolve(true)
: reject(false) : reject(false)
}) })
@ -41,7 +43,7 @@ const module = {
let plugins = new Array(); let plugins = new Array();
if (await !ensureStructure) reject("Invalid Structure"); if (await !ensureStructure) reject("Invalid Structure");
// Temp Plugin List // Temp Plugin List
plugins = Filesystem.readdir({ plugins = Filesystem.readdir({
directory: APP_DIRECTORY, directory: APP_DIRECTORY,

View File

@ -17,10 +17,10 @@ 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;
} }
@ -37,16 +37,24 @@ function getCpn() {
return result; return result;
} }
function linkParser(url){ function getMutationByKey(key, mutations) {
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/; if (!key || !mutations) return undefined;
var match = url.match(regExp); return mutations.find((mutation) => mutation.entityKey === key).payload;
return (match&&match[7].length==11)? match[7] : false;
} }
function linkParser(url) {
console.log("linkParpar", url)
const regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
const match = url.match(regExp);
return (match && match[7].length == 11) ? match[7] : false;
}
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
module.exports = { module.exports = {
getBetweenStrings, getBetweenStrings,
hexToRgb, hexToRgb,
rgbToHex, rgbToHex,
getCpn, getCpn,
getMutationByKey,
linkParser, linkParser,
delay,
}; };

View File

@ -5,6 +5,7 @@ import constants from "./constants";
import rendererUtils from "./renderers"; import rendererUtils from "./renderers";
import { Buffer } from "buffer"; import { Buffer } from "buffer";
import iconv from "iconv-lite"; import iconv from "iconv-lite";
import { Toast } from "@capacitor/toast";
//--- Logger Function ---// //--- Logger Function ---//
function logger(func, data, isError = false) { function logger(func, data, isError = false) {
@ -74,9 +75,14 @@ let InnertubeAPI;
const innertubeModule = { const innertubeModule = {
async getAPI() { async getAPI() {
if (!InnertubeAPI) { if (!InnertubeAPI) {
InnertubeAPI = await Innertube.createAsync((message, isError) => { InnertubeAPI = await Innertube.createAsync(
logger(constants.LOGGER_NAMES.innertube, message, isError); (message, isError, shortMessage) => {
}); logger(constants.LOGGER_NAMES.innertube, message, isError);
if (shortMessage) {
Toast.show({ text: shortMessage });
}
}
);
} }
return InnertubeAPI; return InnertubeAPI;
}, },

View File

@ -18,6 +18,7 @@ dependencies {
implementation project(':capacitor-share') implementation project(':capacitor-share')
implementation project(':capacitor-splash-screen') implementation project(':capacitor-splash-screen')
implementation project(':capacitor-status-bar') implementation project(':capacitor-status-bar')
implementation project(':capacitor-toast')
implementation project(':hugotomazi-capacitor-navigation-bar') implementation project(':hugotomazi-capacitor-navigation-bar')
} }

View File

@ -26,17 +26,38 @@
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" /> <category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" /> <data android:scheme="http" />
<data android:scheme="https" /> <data android:scheme="https" />
<data android:host="youtu.be" />
<data android:host="m.youtube.com" />
<data android:host="youtube.com" /> <data android:host="youtube.com" />
<data android:host="m.youtube.com" />
<data android:host="www.youtube.com" /> <data android:host="www.youtube.com" />
<data android:pathPattern="/.*" /> <!-- video prefix -->
<data android:pathPrefix="/v/" />
<data android:pathPrefix="/embed/" />
<data android:pathPrefix="/watch" />
<data android:pathPrefix="/attribution_link" />
<data android:pathPrefix="/shorts/" />
<!-- channel prefix -->
<data android:pathPrefix="/channel/" />
<data android:pathPrefix="/user/" />
<data android:pathPrefix="/c/" />
<!-- playlist prefix -->
<data android:pathPrefix="/playlist" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="http" />
<data android:scheme="https" />
<data android:host="youtu.be" />
<data android:pathPrefix="/" />
</intent-filter> </intent-filter>
</activity> </activity>

View File

@ -35,6 +35,10 @@
"pkg": "@capacitor/status-bar", "pkg": "@capacitor/status-bar",
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin" "classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
}, },
{
"pkg": "@capacitor/toast",
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
},
{ {
"pkg": "@hugotomazi/capacitor-navigation-bar", "pkg": "@hugotomazi/capacitor-navigation-bar",
"classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin" "classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin"

View File

@ -29,5 +29,8 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
include ':capacitor-status-bar' include ':capacitor-status-bar'
project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android') project(':capacitor-status-bar').projectDir = new File('../node_modules/@capacitor/status-bar/android')
include ':capacitor-toast'
project(':capacitor-toast').projectDir = new File('../node_modules/@capacitor/toast/android')
include ':hugotomazi-capacitor-navigation-bar' include ':hugotomazi-capacitor-navigation-bar'
project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android') project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')

View File

@ -18,6 +18,7 @@ def capacitor_pods
pod 'CapacitorShare', :path => '..\..\node_modules\@capacitor\share' pod 'CapacitorShare', :path => '..\..\node_modules\@capacitor\share'
pod 'CapacitorSplashScreen', :path => '..\..\node_modules\@capacitor\splash-screen' pod 'CapacitorSplashScreen', :path => '..\..\node_modules\@capacitor\splash-screen'
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar' 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 'HugotomaziCapacitorNavigationBar', :path => '..\..\node_modules\@hugotomazi\capacitor-navigation-bar'
end end

View File

@ -12,6 +12,7 @@
"@capacitor/share": "^1.1.2", "@capacitor/share": "^1.1.2",
"@capacitor/splash-screen": "^1.2.2", "@capacitor/splash-screen": "^1.2.2",
"@capacitor/status-bar": "^1.0.8", "@capacitor/status-bar": "^1.0.8",
"@capacitor/toast": "^1.0.8",
"@hugotomazi/capacitor-navigation-bar": "^1.1.1", "@hugotomazi/capacitor-navigation-bar": "^1.1.1",
"iconv-lite": "^0.6.3" "iconv-lite": "^0.6.3"
} }