Merge branch 'Frontesque:main' into main
141
.github/workflows/nightly-release.yml
vendored
Normal file
|
@ -0,0 +1,141 @@
|
|||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: nightly-release
|
||||
|
||||
# Controls when the workflow will run
|
||||
on:
|
||||
# Triggers the workflow on push or pull request events but only for the main branch
|
||||
schedule:
|
||||
# Runs "at minute 55 past every hour" (see https://crontab.guru)
|
||||
- cron: '0 0 * * *'
|
||||
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
NODE_VERSION: 16
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
name: Build web assets
|
||||
runs-on: ubuntu-latest
|
||||
needs: check_date
|
||||
if: ${{ needs.check_date.outputs.should_run != 'false' }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install dependencies
|
||||
run: npm i; cd NUXT; npm i
|
||||
- name: Set App Version
|
||||
working-directory: NUXT
|
||||
run: sed -i 's/dev-local/${{ github.sha }}/' nuxt.config.js
|
||||
- name: Build web assets
|
||||
working-directory: NUXT
|
||||
run: npm run generate
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
android:
|
||||
name: Build Android platform
|
||||
runs-on: ubuntu-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
- name: Set up JDK 11
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 11
|
||||
- name: Copy web assets to native platform
|
||||
run: npx cap copy android
|
||||
- name: Update native platform
|
||||
run: npx cap update android
|
||||
- name: Build with Gradle
|
||||
working-directory: android
|
||||
run: chmod +x gradlew; ./gradlew clean assembleRelease -x test -Pandroid.injected.signing.store.file=/home/runner/work/VueTube/VueTube/android/key.jks -Pandroid.injected.signing.store.password=${{ secrets.ANDROID_STORE_PASSWORD }} -Pandroid.injected.signing.key.alias=${{ secrets.ANDROID_KEY_ALIAS }} -Pandroid.injected.signing.key.password=${{ secrets.ANDROID_KEY_PASSWORD }}
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: android
|
||||
path: android/app/build/outputs/apk/release/app-release.apk
|
||||
|
||||
ios:
|
||||
name: Build iOS platform
|
||||
runs-on: macos-latest
|
||||
needs: [build]
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Download artifacts
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: dist
|
||||
path: dist
|
||||
- name: Set up Node.js ${{ env.NODE_VERSION }}
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: ${{ env.NODE_VERSION }}
|
||||
- name: Install dependencies
|
||||
run: npm i
|
||||
- name: Copy web assets to native platform
|
||||
run: npx cap copy ios
|
||||
- name: Update native platform
|
||||
run: npx cap update ios
|
||||
- name: Add empty `GoogleService-Info.plist`
|
||||
run: echo "$GOOGLE_SERVICE_INFO_PLIST" > ios/App/App/GoogleService-Info.plist
|
||||
env:
|
||||
GOOGLE_SERVICE_INFO_PLIST: ${{secrets.GOOGLE_SERVICE_INFO_PLIST}}
|
||||
- name: Build and archive with xcodebuild
|
||||
working-directory: ios
|
||||
run: xcodebuild
|
||||
-workspace App/App.xcworkspace
|
||||
-scheme App
|
||||
-archivePath App/build/App.xarchive
|
||||
clean build archive
|
||||
CODE_SIGN_IDENTITY=""
|
||||
CODE_SIGNING_REQUIRED=NO
|
||||
CODE_SIGNING_ALLOWED="NO"
|
||||
CODE_SIGN_ENTITLEMENTS=""
|
||||
- name: Make IPA
|
||||
run: mkdir Payload && mv ~/Library/Developer/Xcode/DerivedData/App-*/Build/Products/Debug-iphoneos/App.app Payload && zip -r Payload.zip Payload && mv Payload.zip VueTube.ipa
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: iOS
|
||||
path: VueTube.ipa
|
||||
|
||||
check_date:
|
||||
runs-on: ubuntu-latest
|
||||
name: Check latest commit
|
||||
outputs:
|
||||
should_run: ${{ steps.should_run.outputs.should_run }}
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: print latest_commit
|
||||
run: echo ${{ github.sha }}
|
||||
|
||||
- id: should_run
|
||||
continue-on-error: true
|
||||
name: check latest commit is less than a day
|
||||
if: ${{ github.event_name == 'schedule' }}
|
||||
run: test -z $(git rev-list --after="24 hours" ${{ github.sha }}) && echo "::set-output name=should_run::false"
|
87
NUXT/components/Comments/commentMainRenderer.vue
Normal 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>
|
0
NUXT/components/Comments/commentThreadRenderer.vue
Normal file
|
@ -19,7 +19,7 @@
|
|||
"
|
||||
/>
|
||||
</a>
|
||||
<v-card-text class="video-info pt-2">
|
||||
<v-card-text class="video-info pt-2" v-emoji>
|
||||
<div
|
||||
v-for="title in video.title.runs"
|
||||
:key="title.text"
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
<a
|
||||
:href="
|
||||
this.$rendererUtils.getNavigationEndpoints(
|
||||
video.shortBylineText.runs[0].navigationEndpoint
|
||||
video.shortBylineText.runs[0]
|
||||
)
|
||||
"
|
||||
class="avatar-link pt-2"
|
||||
|
@ -42,7 +42,7 @@
|
|||
:src="video.channelThumbnail.thumbnails[0].url"
|
||||
/>
|
||||
</a>
|
||||
<v-card-text class="video-info pt-2">
|
||||
<v-card-text class="video-info pt-2" v-emoji>
|
||||
<div
|
||||
v-for="title in video.title.runs"
|
||||
:key="title.text"
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
<template>
|
||||
<div class="swipeableBottomSheet" :open-state="state ? 1 : 0"></div>
|
||||
</template>
|
|
@ -38,12 +38,14 @@
|
|||
import compactVideoRenderer from "~/components/CompactRenderers/compactVideoRenderer.vue";
|
||||
import compactChannelRenderer from "~/components/CompactRenderers/compactChannelRenderer.vue";
|
||||
import gridVideoRenderer from "~/components/gridRenderers/gridVideoRenderer.vue";
|
||||
import videoWithContextRenderer from "~/components/gridRenderers/videoWithContextRenderer.vue";
|
||||
|
||||
export default {
|
||||
components: {
|
||||
gridVideoRenderer,
|
||||
compactVideoRenderer,
|
||||
compactChannelRenderer,
|
||||
videoWithContextRenderer,
|
||||
},
|
||||
props: ["render"],
|
||||
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
<template>
|
||||
<div class="description">
|
||||
<template v-for="(text, index) in render.description.runs">
|
||||
<template
|
||||
v-if="
|
||||
text.navigationEndpoint && text.navigationEndpoint.webviewEndpoint
|
||||
"
|
||||
>
|
||||
<div class="description" v-if="render.descriptionBodyText">
|
||||
<template v-for="(text, index) in render.descriptionBodyText.runs">
|
||||
<template v-if="$rendererUtils.checkInternal(text)">
|
||||
<a
|
||||
@click="openExternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
@click="openInternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
:key="index"
|
||||
>{{ text.text }}</a
|
||||
>
|
||||
</template>
|
||||
<template v-else-if="$rendererUtils.checkInternal(text)">
|
||||
<template
|
||||
v-else-if="
|
||||
text.navigationEndpoint && text.navigationEndpoint.urlEndpoint
|
||||
"
|
||||
>
|
||||
<a
|
||||
@click="openInternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
@click="openExternal($rendererUtils.getNavigationEndpoints(text))"
|
||||
:key="index"
|
||||
>{{ text.text }}</a
|
||||
>
|
||||
|
@ -27,7 +27,6 @@
|
|||
<style scoped>
|
||||
.description {
|
||||
white-space: pre-line;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -51,7 +51,7 @@
|
|||
:src="video.channelThumbnail.thumbnails[0].url"
|
||||
/>
|
||||
</a>
|
||||
<v-card-text class="video-info pt-2">
|
||||
<v-card-text class="video-info pt-2" v-emoji>
|
||||
<div
|
||||
v-for="title in video.title.runs"
|
||||
:key="title.text"
|
||||
|
|
151
NUXT/components/gridRenderers/videoWithContextRenderer.vue
Normal 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" v-emoji>
|
||||
<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>
|
|
@ -33,12 +33,13 @@
|
|||
v-for="(item, index) in response"
|
||||
:key="index"
|
||||
class="px-0"
|
||||
v-emoji
|
||||
>
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
dense
|
||||
class="searchButton text-left text-capitalize"
|
||||
class="searchButton text-left"
|
||||
@click="youtubeSearch(item)"
|
||||
>
|
||||
<v-icon class="mr-5">mdi-magnify</v-icon>
|
||||
|
@ -60,7 +61,7 @@
|
|||
import { App as CapacitorApp } from "@capacitor/app";
|
||||
import { mapState } from "vuex";
|
||||
import constants from "~/plugins/constants";
|
||||
import { linkParser } from "~/plugins/utils"
|
||||
import { linkParser } from "~/plugins/utils";
|
||||
|
||||
export default {
|
||||
data: () => ({
|
||||
|
@ -106,27 +107,37 @@ export default {
|
|||
}
|
||||
});
|
||||
|
||||
// --- External URL Handling --- //
|
||||
CapacitorApp.addListener("appUrlOpen", (event) => {
|
||||
const slug = new URL(event.url);
|
||||
// We only push to the route if there is a slug present
|
||||
if (slug) {
|
||||
console.log(slug.pathname + slug.search);
|
||||
this.$router.push(slug.pathname + slug.search);
|
||||
}
|
||||
this.$logger("ExternalURL", event.url);
|
||||
// We only push to the route if there is a url present
|
||||
linkParser(event.url);
|
||||
if (result) this.$router.push(result.pathname + result.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: {
|
||||
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 ---//
|
||||
const isLink = linkParser(text);
|
||||
if (isLink) {
|
||||
this.response = [{
|
||||
text: `Watch video from ID: ${isLink}`,
|
||||
id: isLink
|
||||
}];
|
||||
this.response = [
|
||||
{
|
||||
text: `Watch Video from ID: ${isLink.searchParams.get("v")}`,
|
||||
id: isLink.searchParams.get("v"),
|
||||
},
|
||||
];
|
||||
return;
|
||||
}
|
||||
//--- End User Pastes Link, Direct Them To Video ---//
|
||||
|
@ -139,9 +150,7 @@ export default {
|
|||
},
|
||||
|
||||
youtubeSearch(item) {
|
||||
const link = item.id
|
||||
? `/watch?v=${item.id}`
|
||||
: `/search?q=${item[0]}`
|
||||
const link = item.id ? `/watch?v=${item.id}` : `/search?q=${item[0]}`;
|
||||
this.$router.push(link);
|
||||
this.search = false;
|
||||
},
|
||||
|
@ -206,11 +215,19 @@ div {
|
|||
.invert {
|
||||
filter: invert(100%);
|
||||
}
|
||||
|
||||
.emoji {
|
||||
display: inline-block;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
vertical-align: -0.1em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
.searchButton {
|
||||
width: 100%;
|
||||
text-transform: none !important;
|
||||
justify-content: left !important;
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
An error occured!
|
||||
</h1>
|
||||
<v-btn to="/">Reload Application</v-btn>
|
||||
<v-btn to="/logs">Show Logs</v-btn>
|
||||
<v-btn to="/mods/logs">Show Logs</v-btn>
|
||||
|
||||
<div style="margin-top: 5em; color: #999; font-size: 0.75em">
|
||||
<div style="font-size: 1.4em">Error Information</div>
|
||||
|
|
|
@ -25,15 +25,20 @@ export default {
|
|||
const theming = new Promise((resolve) =>
|
||||
// Set timeout is required for $vuetify.theme... dont ask me why -Front
|
||||
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)
|
||||
this.$vuetify.theme.themes.dark.primary = localStorage.getItem("primaryDark");
|
||||
this.$vuetify.theme.themes.dark.primary =
|
||||
localStorage.getItem("primaryDark");
|
||||
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)
|
||||
this.$vuetify.theme.themes.dark.background = localStorage.getItem("backgroundDark");
|
||||
this.$vuetify.theme.themes.dark.background =
|
||||
localStorage.getItem("backgroundDark");
|
||||
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.$vuetify.theme.currentTheme.background,
|
||||
!this.$vuetify.theme.dark
|
||||
|
|
|
@ -1,29 +1,44 @@
|
|||
<template>
|
||||
<div class="py-2">
|
||||
<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">
|
||||
{{ item.author ? item.author.login : item.commit.author.name }}
|
||||
<span class="subtitle background--text" :class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
v-text="`• ${item.sha.substring(0, 7)}`" />
|
||||
<span
|
||||
class="subtitle background--text"
|
||||
:class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
v-text="`• ${item.sha.substring(0, 7)}`"
|
||||
/>
|
||||
<v-spacer />
|
||||
<v-chip v-if="index == 0" class="tags" color="orange" style="
|
||||
border-radius: 0.5rem;
|
||||
border: 2px var(--v-orange-base);
|
||||
">
|
||||
<v-chip
|
||||
v-if="index == 0"
|
||||
class="tags"
|
||||
color="orange"
|
||||
style="border-radius: 0.5rem; border: 2px var(--v-orange-base)"
|
||||
>
|
||||
Latest
|
||||
</v-chip>
|
||||
<v-chip v-if="item.sha == installedVersion" class="tags" color="green" style="
|
||||
border-radius: 0.5rem;
|
||||
border: 2px var(--v-green-base);
|
||||
">
|
||||
<v-chip
|
||||
v-if="item.sha == installedVersion"
|
||||
class="tags"
|
||||
color="green"
|
||||
style="border-radius: 0.5rem; border: 2px var(--v-green-base)"
|
||||
>
|
||||
Installed
|
||||
</v-chip>
|
||||
</v-card-title>
|
||||
|
||||
<div style="margin-left: 1em">
|
||||
<div class="date background--text" :class="$vuetify.theme.dark ? 'text--lighten-4' : 'text--darken-4'"
|
||||
v-text="new Date(item.commit.committer.date).toLocaleString()" />
|
||||
<div
|
||||
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 }}
|
||||
</div>
|
||||
|
||||
|
@ -42,69 +57,65 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
.card {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin: 0.4em;
|
||||
font-size: 0.75em;
|
||||
transform: translateY(5%);
|
||||
}
|
||||
.subtitle {
|
||||
margin: 0.4em;
|
||||
font-size: 0.75em;
|
||||
transform: translateY(5%);
|
||||
}
|
||||
|
||||
.date {
|
||||
transform: translateY(-40%);
|
||||
}
|
||||
.date {
|
||||
transform: translateY(-40%);
|
||||
}
|
||||
|
||||
.btn-icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
.btn-icon {
|
||||
margin-right: 0.25em;
|
||||
}
|
||||
|
||||
.tags {
|
||||
margin-left: 0.5em;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
import { Browser } from "@capacitor/browser";
|
||||
import { Browser } from "@capacitor/browser";
|
||||
|
||||
export default {
|
||||
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
commits: new Array(),
|
||||
installedVersion: process.env.appVersion,
|
||||
};
|
||||
export default {
|
||||
computed: {
|
||||
roundTweak() {
|
||||
return this.$store.state.tweaks.roundTweak;
|
||||
},
|
||||
async mounted() {
|
||||
const commits = await this.$vuetube.commits;
|
||||
if (commits[0].sha) {
|
||||
//If Commit Valid
|
||||
this.commits = commits;
|
||||
} else {
|
||||
console.log(commits);
|
||||
}
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
commits: new Array(),
|
||||
installedVersion: process.env.appVersion,
|
||||
};
|
||||
},
|
||||
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) {
|
||||
this.$vuetube.getRuns(item, (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
},
|
||||
install(item) {
|
||||
this.$vuetube.getRuns(item, (data) => {
|
||||
console.log(data);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -14,7 +14,6 @@
|
|||
|
||||
<!-- Dev Mode Open -->
|
||||
<v-btn text class="entry" @click="dev()" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -68,7 +67,7 @@ export default {
|
|||
if (this.devClicks >= 6) {
|
||||
this.$router.push("/mods/developer");
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,172 +1,227 @@
|
|||
<template>
|
||||
<div class="background">
|
||||
<!-- Stock Player -->
|
||||
<videoPlayer
|
||||
style="position: sticky; top: 0; z-index: 696969"
|
||||
:vid-src="vidSrc"
|
||||
ref="player"
|
||||
v-if="useBetaPlayer !== 'true'"
|
||||
/>
|
||||
|
||||
<!-- VueTube Player V1 -->
|
||||
<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"
|
||||
<div class="background" id="watch-body">
|
||||
<div class="player-container">
|
||||
<!-- Stock Player -->
|
||||
<videoPlayer
|
||||
:vid-src="vidSrc"
|
||||
ref="player"
|
||||
v-if="useBetaPlayer !== 'true'"
|
||||
/>
|
||||
<v-card-text>
|
||||
<div style="margin-bottom: 1rem">
|
||||
{{ views }} views • {{ uploaded }}
|
||||
</div>
|
||||
|
||||
<!-- Scrolling Div For Interactions --->
|
||||
<div style="display: flex; margin-bottom: 1em">
|
||||
<v-list-item
|
||||
v-for="(item, index) in interactions"
|
||||
: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)"
|
||||
<!-- VueTube Player V1 -->
|
||||
<vuetubePlayer :sources="sources" v-if="useBetaPlayer === 'true'" />
|
||||
</div>
|
||||
<div class="content-container overflow-y-auto">
|
||||
<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="video.title"
|
||||
v-emoji
|
||||
/>
|
||||
<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
|
||||
class="mt-2"
|
||||
style="font-size: 0.66rem"
|
||||
v-text="item.value || item.name"
|
||||
/>
|
||||
</v-btn>
|
||||
</v-list-item>
|
||||
</div>
|
||||
|
||||
<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>
|
||||
<!-- Scrolling Div For Interactions --->
|
||||
<div style="display: flex; margin-bottom: 1em">
|
||||
<v-list-item
|
||||
v-for="(item, index) in interactions"
|
||||
: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>
|
||||
<!-- End Scrolling Div For Interactions --->
|
||||
<!-- <hr /> -->
|
||||
<p>Channel Stuff</p>
|
||||
</v-card-text>
|
||||
<div v-if="showMore" class="scroll-y ml-2 mr-2">
|
||||
<slim-video-description-renderer :render="description" />
|
||||
</v-sheet>
|
||||
</v-bottom-sheet> -->
|
||||
</v-card>
|
||||
<v-divider />
|
||||
|
||||
<!-- 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" v-emoji>
|
||||
<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>
|
||||
|
||||
<!-- <v-bottom-sheet
|
||||
v-model="showMore"
|
||||
color="background"
|
||||
style="z-index: 9999999"
|
||||
>
|
||||
<v-sheet style="padding: 1em">
|
||||
<v-btn block @click="showMore = !showMore"
|
||||
><v-icon>mdi-chevron-down</v-icon></v-btn
|
||||
><br />
|
||||
<!-- Description -->
|
||||
<div class="description-container" v-if="showMore">
|
||||
<div class="scroll-y ma-4">
|
||||
<slim-video-description-renderer
|
||||
:render="video.renderedData.description"
|
||||
/>
|
||||
</div>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
||||
<div class="scroll-y">
|
||||
{{ description }}
|
||||
</div>
|
||||
</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">
|
||||
{{ description }}
|
||||
</div>
|
||||
</v-sheet>
|
||||
</v-bottom-sheet> -->
|
||||
</v-card>
|
||||
<vid-load-renderer v-if="!recommends" />
|
||||
<shelf-renderer v-else :render="recommends" />
|
||||
<!-- Comments -->
|
||||
<div
|
||||
class="comment-container"
|
||||
v-if="loaded && video.commentData"
|
||||
@click="showComments = !showComments"
|
||||
>
|
||||
<v-card flat class="background comment-renderer">
|
||||
<v-text class="comment-count keep-spaces">
|
||||
<template v-for="text in video.commentData.headerText.runs">
|
||||
<template v-if="text.bold">
|
||||
<strong :key="text.text">{{ text.text }}</strong>
|
||||
</template>
|
||||
<template v-else>{{ text.text }}</template>
|
||||
</template>
|
||||
</v-text>
|
||||
<v-icon>mdi-unfold-more-horizontal</v-icon>
|
||||
</v-card>
|
||||
<v-divider />
|
||||
</div>
|
||||
|
||||
<v-dialog
|
||||
v-model="showComments"
|
||||
fullscreen
|
||||
hide-overlay
|
||||
transition="dialog-bottom-transition"
|
||||
>
|
||||
<v-card>
|
||||
<v-toolbar dark color="background">
|
||||
<v-btn icon dark @click="showComments = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
<v-toolbar-title class="font-weight-bold">Comments</v-toolbar-title>
|
||||
</v-toolbar>
|
||||
<v-subheader>Hello World</v-subheader>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
||||
<!-- 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>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { Share } from "@capacitor/share";
|
||||
import ShelfRenderer from "~/components/SectionRenderers/shelfRenderer.vue";
|
||||
import VidLoadRenderer from "~/components/vidLoadRenderer.vue";
|
||||
import { getCpn } from "~/plugins/utils";
|
||||
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 {
|
||||
components: { ShelfRenderer, VidLoadRenderer, SlimVideoDescriptionRenderer, vuetubePlayer },
|
||||
data() {
|
||||
return {
|
||||
interactions: [
|
||||
{
|
||||
name: "Likes",
|
||||
icon: "mdi-thumb-up-outline",
|
||||
// action: null,
|
||||
value: this.likes,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Dislikes",
|
||||
icon: "mdi-thumb-down-outline",
|
||||
// action: this.dislike(),
|
||||
actionName: "dislike",
|
||||
value: this.dislikes,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
icon: "mdi-share-outline",
|
||||
// action: this.share(),
|
||||
actionName: "share",
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
showMore: false,
|
||||
// share: false,
|
||||
title: null,
|
||||
uploaded: null,
|
||||
vidSrc: null,
|
||||
sources: [],
|
||||
description: null,
|
||||
views: null,
|
||||
recommends: null,
|
||||
loaded: false,
|
||||
interval: null,
|
||||
|
||||
useBetaPlayer: false,
|
||||
};
|
||||
components: {
|
||||
ShelfRenderer,
|
||||
VidLoadRenderer,
|
||||
SlimVideoDescriptionRenderer,
|
||||
vuetubePlayer,
|
||||
ItemSectionRenderer,
|
||||
},
|
||||
|
||||
data: function () {
|
||||
return this.initializeState();
|
||||
},
|
||||
watch: {
|
||||
// Watch for change in the route query string (in this case, ?v=xxxxxxxx to ?v=yyyyyyyy)
|
||||
$route: {
|
||||
deep: true,
|
||||
handler(newRt, oldRt) {
|
||||
if (newRt.query.v != oldRt.query.v) {
|
||||
if (newRt.query.v && newRt.query.v != oldRt.query.v) {
|
||||
// Exit fullscreen if currently in fullscreen
|
||||
// if (this.$refs.player) this.$refs.player.webkitExitFullscreen();
|
||||
// Reset player and run getVideo function again
|
||||
this.vidSrc = "";
|
||||
this.startTime = Math.floor(Date.now() / 1000);
|
||||
// this.vidSrc = "";
|
||||
// this.startTime = Math.floor(Date.now() / 1000);
|
||||
// this.getVideo();
|
||||
clearInterval(this.interval);
|
||||
this.getVideo();
|
||||
Object.assign(this.$data, this.initializeState());
|
||||
this.mountedInit();
|
||||
}
|
||||
},
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.startTime = Math.floor(Date.now() / 1000);
|
||||
this.getVideo();
|
||||
|
||||
this.useBetaPlayer = localStorage.getItem('debug.BetaPlayer');
|
||||
this.mountedInit();
|
||||
},
|
||||
|
||||
destroyed() {
|
||||
|
@ -174,10 +229,10 @@ export default {
|
|||
},
|
||||
methods: {
|
||||
getVideo() {
|
||||
this.likes = 100;
|
||||
this.loaded = false;
|
||||
|
||||
this.$youtube.getVid(this.$route.query.v).then((result) => {
|
||||
this.video = result;
|
||||
console.log("Video info data", result);
|
||||
console.log(result.availableResolutions);
|
||||
|
||||
|
@ -191,14 +246,9 @@ export default {
|
|||
].url; // Takes the highest available resolution with both video and Audio. Note this will be lower than the actual highest resolution
|
||||
|
||||
//--- 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.uploaded = result.metadata.uploadDate;
|
||||
this.interactions[0].value = result.metadata.likes.toLocaleString();
|
||||
this.loaded = true;
|
||||
|
||||
this.recommends = result.renderedData.recommendations;
|
||||
// .catch((error) => this.$logger("Watch", error, true));
|
||||
console.log("recommendations:", this.recommends);
|
||||
|
@ -227,8 +277,8 @@ export default {
|
|||
async share() {
|
||||
// this.share = !this.share;
|
||||
await Share.share({
|
||||
title: this.title,
|
||||
text: this.title,
|
||||
title: this.video.title,
|
||||
text: this.video.title,
|
||||
url: "https://youtu.be/" + this.$route.query.v,
|
||||
dialogTitle: "Share video",
|
||||
});
|
||||
|
@ -269,13 +319,110 @@ export default {
|
|||
this.playbackTracking.videostatsPlaybackUrl.baseUrl
|
||||
);
|
||||
},
|
||||
|
||||
initializeState() {
|
||||
return {
|
||||
interactions: [
|
||||
{
|
||||
name: "Likes",
|
||||
icon: "mdi-thumb-up-outline",
|
||||
// action: null,
|
||||
value: this.likes,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Dislikes",
|
||||
icon: "mdi-thumb-down-outline",
|
||||
// action: this.dislike(),
|
||||
actionName: "dislike",
|
||||
value: this.dislikes,
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Share",
|
||||
icon: "mdi-share-outline",
|
||||
// action: this.share(),
|
||||
actionName: "share",
|
||||
disabled: false,
|
||||
},
|
||||
],
|
||||
showMore: false,
|
||||
showComments: false,
|
||||
// share: false,
|
||||
vidSrc: null,
|
||||
sources: [],
|
||||
recommends: null,
|
||||
loaded: false,
|
||||
interval: null,
|
||||
video: null,
|
||||
useBetaPlayer: false,
|
||||
};
|
||||
},
|
||||
|
||||
mountedInit() {
|
||||
this.startTime = Math.floor(Date.now() / 1000);
|
||||
this.getVideo();
|
||||
this.useBetaPlayer = localStorage.getItem("debug.BetaPlayer");
|
||||
|
||||
// Reset vertical scrolling
|
||||
const scrollableList = document.querySelectorAll(".overflow-y-auto");
|
||||
scrollableList.forEach((scrollable) => {
|
||||
scrollable.scrollTo(0, 0);
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
||||
<style>
|
||||
#watch-body {
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.content-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.vertical-button span.v-btn__content {
|
||||
flex-direction: column;
|
||||
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>
|
||||
|
|
|
@ -12,7 +12,7 @@ const url = {
|
|||
const ytApiVal = {
|
||||
VERSION: "16.25",
|
||||
CLIENTNAME: "ANDROID",
|
||||
VERSION_WEB: "2.20220318.00.00",
|
||||
VERSION_WEB: "2.20220411.09.00",
|
||||
CLIENT_WEB: 2,
|
||||
};
|
||||
|
||||
|
|
|
@ -2,7 +2,8 @@
|
|||
// https://www.youtube.com/youtubei/v1
|
||||
|
||||
import { Http } from "@capacitor-community/http";
|
||||
import { getBetweenStrings } from "./utils";
|
||||
import { getBetweenStrings, delay } from "./utils";
|
||||
import rendererUtils from "./renderers";
|
||||
import constants from "./constants";
|
||||
|
||||
class Innertube {
|
||||
|
@ -39,12 +40,27 @@ class Innertube {
|
|||
}
|
||||
} catch (err) {
|
||||
console.log(err);
|
||||
if (this.checkErrorCallback) this.ErrorCallback(err, true);
|
||||
if (this.retry_count >= 10) {
|
||||
this.initAsync();
|
||||
if (this.checkErrorCallback) {
|
||||
this.ErrorCallback(html.data, true);
|
||||
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 {
|
||||
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) {
|
||||
|
@ -95,8 +111,11 @@ class Innertube {
|
|||
};
|
||||
}
|
||||
|
||||
async getContinuationsAsync(continuation, type) {
|
||||
let data = { context: this.context, continuation: continuation };
|
||||
async getContinuationsAsync(continuation, type, contextAdditional = {}) {
|
||||
let data = {
|
||||
context: { ...this.context, ...contextAdditional },
|
||||
continuation: continuation,
|
||||
};
|
||||
let url;
|
||||
switch (type) {
|
||||
case "browse":
|
||||
|
@ -135,7 +154,17 @@ class Innertube {
|
|||
let data = { context: this.context, videoId: id };
|
||||
const responseNext = await Http.post({
|
||||
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),
|
||||
}).catch((error) => error);
|
||||
|
||||
|
@ -251,64 +280,72 @@ class Innertube {
|
|||
const resolutions = responseInfo.streamingData;
|
||||
const columnUI =
|
||||
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 = {
|
||||
id: details.videoId,
|
||||
title: details.title,
|
||||
isLive: details.isLiveContent,
|
||||
channelName: details.author,
|
||||
channelUrl:
|
||||
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(
|
||||
(contents) => contents.elementRenderer
|
||||
)?.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,
|
||||
channelSubs: ownerData?.collapsedSubtitle?.runs[0]?.text,
|
||||
channelUrl: rendererUtils.getNavigationEndpoints(ownerData),
|
||||
channelImg: ownerData?.thumbnail?.thumbnails[0].url,
|
||||
availableResolutions: resolutions?.formats,
|
||||
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
|
||||
metadata: {
|
||||
contents: vidMetadata.contents,
|
||||
description: details.shortDescription,
|
||||
thumbnails: details.thumbnails?.thumbnails,
|
||||
uploadDate:
|
||||
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents.find(
|
||||
(contents) => contents.slimVideoDescriptionRenderer
|
||||
)?.slimVideoDescriptionRenderer.publishDate.runs[0].text,
|
||||
isPrivate: details.isPrivate,
|
||||
viewCount: details.viewCount,
|
||||
lengthSeconds: details.lengthSeconds,
|
||||
likes: parseInt(
|
||||
columnUI?.contents[0].slimVideoMetadataSectionRenderer?.contents
|
||||
.find((contents) => contents.slimVideoScrollableActionBarRenderer)
|
||||
?.slimVideoScrollableActionBarRenderer.buttons.find(
|
||||
(button) => button.slimMetadataToggleButtonRenderer.isLike == true
|
||||
vidMetadata.contents
|
||||
.find((content) => content.slimVideoActionBarRenderer)
|
||||
.slimVideoActionBarRenderer.buttons.find(
|
||||
(button) => button.slimMetadataToggleButtonRenderer.isLike
|
||||
)
|
||||
?.slimMetadataToggleButtonRenderer?.button.toggleButtonRenderer?.defaultText?.accessibility?.accessibilityData?.label?.replace(
|
||||
.slimMetadataToggleButtonRenderer.button.toggleButtonRenderer.defaultText.accessibility.accessibilityData.label.replace(
|
||||
/\D/g,
|
||||
""
|
||||
)
|
||||
), // Yes. I know.
|
||||
},
|
||||
renderedData: {
|
||||
description: columnUI?.contents
|
||||
.find((contents) => contents.slimVideoMetadataSectionRenderer)
|
||||
.slimVideoMetadataSectionRenderer?.contents.find(
|
||||
(contents) => contents.slimVideoDescriptionRenderer
|
||||
)?.slimVideoDescriptionRenderer,
|
||||
recommendations: columnUI?.contents.find(
|
||||
(contents) => contents.shelfRenderer
|
||||
).shelfRenderer,
|
||||
description: responseNext.engagementPanels
|
||||
.find(
|
||||
(panel) =>
|
||||
panel.engagementPanelSectionListRenderer.panelIdentifier ==
|
||||
"video-description-ep-identifier"
|
||||
)
|
||||
.engagementPanelSectionListRenderer.content.structuredDescriptionContentRenderer.items.find(
|
||||
(item) => item.expandableVideoDescriptionBodyRenderer
|
||||
).expandableVideoDescriptionBodyRenderer,
|
||||
recommendations: recommendations,
|
||||
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,
|
||||
};
|
||||
|
||||
console.log(vidData);
|
||||
|
||||
return vidData;
|
||||
}
|
||||
|
||||
|
|
|
@ -3,24 +3,33 @@ class rendererUtils {
|
|||
static getNavigationEndpoints(base) {
|
||||
const navEndpoint = base.navigationEndpoint;
|
||||
if (!navEndpoint) return;
|
||||
if (navEndpoint.webviewEndpoint) {
|
||||
return navEndpoint.webviewEndpoint.url;
|
||||
if (navEndpoint.urlEndpoint) {
|
||||
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) {
|
||||
return navEndpoint.browseEndpoint.canonicalBaseUrl;
|
||||
} else if (navEndpoint.watchEndpoint) {
|
||||
return `/watch?v=${navEndpoint.watchEndpoint.videoId}`;
|
||||
} else if (navEndpoint.navigationEndpoint) {
|
||||
return; //for now
|
||||
} else if (navEndpoint.searchEndpoint) {
|
||||
return `/search?q=${encodeURI(navEndpoint.searchEndpoint.query)}`;
|
||||
}
|
||||
}
|
||||
|
||||
static checkInternal(base) {
|
||||
const navEndpoint = base.navigationEndpoint;
|
||||
if (!navEndpoint) return false;
|
||||
if (navEndpoint.browseEndpoint || navEndpoint.watchEndpoint) {
|
||||
return true;
|
||||
} else {
|
||||
const tmp = document.createElement("a");
|
||||
tmp.href = this.getNavigationEndpoints(base);
|
||||
if (tmp.host !== window.location.host || !base.navigationEndpoint) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,22 +14,24 @@ const ensureStructure = new Promise(async (resolve, reject) => {
|
|||
|
||||
//--- Ensure Plugins Folder ---//
|
||||
try {
|
||||
await Filesystem.mkdir({ directory: APP_DIRECTORY, recursive: true,
|
||||
await Filesystem.mkdir({
|
||||
directory: APP_DIRECTORY, recursive: true,
|
||||
path: fs.plugins,
|
||||
});
|
||||
} catch (e) { /* Exists */ }
|
||||
|
||||
//--- Ensure Temp Folder ---//
|
||||
try {
|
||||
await Filesystem.mkdir({ directory: APP_DIRECTORY, recursive: true,
|
||||
await Filesystem.mkdir({
|
||||
directory: APP_DIRECTORY, recursive: true,
|
||||
path: fs.temp,
|
||||
});
|
||||
} catch (e) { /* Exists */ }
|
||||
|
||||
perms
|
||||
perms
|
||||
? resolve(true)
|
||||
: reject(false)
|
||||
|
||||
|
||||
})
|
||||
|
||||
|
||||
|
@ -41,7 +43,7 @@ const module = {
|
|||
let plugins = new Array();
|
||||
|
||||
if (await !ensureStructure) reject("Invalid Structure");
|
||||
|
||||
|
||||
// Temp Plugin List
|
||||
plugins = Filesystem.readdir({
|
||||
directory: APP_DIRECTORY,
|
||||
|
|
|
@ -37,16 +37,53 @@ function getCpn() {
|
|||
return result;
|
||||
}
|
||||
|
||||
function linkParser(url){
|
||||
var regExp = /^.*((youtu.be\/)|(v\/)|(\/u\/\w\/)|(embed\/)|(watch\?))\??v?=?([^#&?]*).*/;
|
||||
var match = url.match(regExp);
|
||||
return (match&&match[7].length==11)? match[7] : false;
|
||||
function getMutationByKey(key, mutations) {
|
||||
if (!key || !mutations) return undefined;
|
||||
return mutations.find((mutation) => mutation.entityKey === key).payload;
|
||||
}
|
||||
|
||||
function setHttp(link) {
|
||||
if (link.search(/^http[s]?\:\/\//) == -1) {
|
||||
link = "http://" + link;
|
||||
}
|
||||
return link;
|
||||
}
|
||||
|
||||
// Replace inputted html with tweemoji
|
||||
function parseEmoji(body) {
|
||||
if (twemoji)
|
||||
return twemoji.parse(body, {
|
||||
folder: "svg",
|
||||
ext: ".svg",
|
||||
});
|
||||
}
|
||||
|
||||
function linkParser(url) {
|
||||
let result;
|
||||
if (url) {
|
||||
try {
|
||||
const slug = new URL(setHttp(url));
|
||||
const host = slug.hostname.toLowerCase().replace(/^www\./, "");
|
||||
if (host == "youtube.com") {
|
||||
result = slug;
|
||||
} else if (host == "youtu.be") {
|
||||
result = new URL("/watch", window.location.origin);
|
||||
result.searchParams.set("v", slug.pathname.split("/")[1]);
|
||||
}
|
||||
} finally {
|
||||
return result instanceof URL ? result : false;
|
||||
}
|
||||
}
|
||||
}
|
||||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
module.exports = {
|
||||
getBetweenStrings,
|
||||
hexToRgb,
|
||||
rgbToHex,
|
||||
getCpn,
|
||||
getMutationByKey,
|
||||
linkParser,
|
||||
delay,
|
||||
parseEmoji,
|
||||
};
|
||||
|
|
|
@ -3,8 +3,16 @@ import { Http } from "@capacitor-community/http";
|
|||
import { StatusBar, Style } from "@capacitor/status-bar";
|
||||
import { NavigationBar } from "@hugotomazi/capacitor-navigation-bar";
|
||||
import constants from "./constants";
|
||||
import { hexToRgb, rgbToHex } from "./utils";
|
||||
import { hexToRgb, rgbToHex, parseEmoji } from "./utils";
|
||||
import { Haptics, ImpactStyle } from "@capacitor/haptics";
|
||||
import Vue from "vue";
|
||||
|
||||
Vue.directive("emoji", {
|
||||
inserted: function (el) {
|
||||
const twemojiParse = parseEmoji(el.innerHTML);
|
||||
if (twemojiParse) el.innerHTML = twemojiParse;
|
||||
},
|
||||
});
|
||||
|
||||
const module = {
|
||||
//--- Get GitHub Commits ---//
|
||||
|
|
|
@ -5,6 +5,7 @@ import constants from "./constants";
|
|||
import rendererUtils from "./renderers";
|
||||
import { Buffer } from "buffer";
|
||||
import iconv from "iconv-lite";
|
||||
import { Toast } from "@capacitor/toast";
|
||||
|
||||
//--- Logger Function ---//
|
||||
function logger(func, data, isError = false) {
|
||||
|
@ -74,9 +75,14 @@ let InnertubeAPI;
|
|||
const innertubeModule = {
|
||||
async getAPI() {
|
||||
if (!InnertubeAPI) {
|
||||
InnertubeAPI = await Innertube.createAsync((message, isError) => {
|
||||
logger(constants.LOGGER_NAMES.innertube, message, isError);
|
||||
});
|
||||
InnertubeAPI = await Innertube.createAsync(
|
||||
(message, isError, shortMessage) => {
|
||||
logger(constants.LOGGER_NAMES.innertube, message, isError);
|
||||
if (shortMessage) {
|
||||
Toast.show({ text: shortMessage });
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
return InnertubeAPI;
|
||||
},
|
||||
|
|
|
@ -18,6 +18,7 @@ dependencies {
|
|||
implementation project(':capacitor-share')
|
||||
implementation project(':capacitor-splash-screen')
|
||||
implementation project(':capacitor-status-bar')
|
||||
implementation project(':capacitor-toast')
|
||||
implementation project(':hugotomazi-capacitor-navigation-bar')
|
||||
|
||||
}
|
||||
|
|
|
@ -26,17 +26,38 @@
|
|||
</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:host="m.youtube.com" />
|
||||
<data android:host="youtube.com" />
|
||||
<data android:host="m.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>
|
||||
|
||||
</activity>
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
"pkg": "@capacitor/status-bar",
|
||||
"classpath": "com.capacitorjs.plugins.statusbar.StatusBarPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@capacitor/toast",
|
||||
"classpath": "com.capacitorjs.plugins.toast.ToastPlugin"
|
||||
},
|
||||
{
|
||||
"pkg": "@hugotomazi/capacitor-navigation-bar",
|
||||
"classpath": "br.com.tombus.capacitor.plugin.navigationbar.NavigationBarPlugin"
|
||||
|
|
Before Width: | Height: | Size: 113 KiB After Width: | Height: | Size: 90 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 352 B |
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 418 B |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 492 B |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 605 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 353 B |
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 419 B |
Before Width: | Height: | Size: 5.9 KiB After Width: | Height: | Size: 493 B |
Before Width: | Height: | Size: 9.4 KiB After Width: | Height: | Size: 605 B |
Before Width: | Height: | Size: 697 B After Width: | Height: | Size: 325 B |
Before Width: | Height: | Size: 3.6 KiB After Width: | Height: | Size: 3.1 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 7.3 KiB |
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 957 B |
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 2.2 KiB |
Before Width: | Height: | Size: 4.2 KiB After Width: | Height: | Size: 3.2 KiB |
Before Width: | Height: | Size: 732 B After Width: | Height: | Size: 692 B |
Before Width: | Height: | Size: 5.4 KiB After Width: | Height: | Size: 4.5 KiB |
Before Width: | Height: | Size: 21 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 9.6 KiB After Width: | Height: | Size: 7.9 KiB |
Before Width: | Height: | Size: 50 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 13 KiB |
Before Width: | Height: | Size: 94 KiB After Width: | Height: | Size: 63 KiB |
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 2.7 KiB |
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 20 KiB |
|
@ -29,5 +29,8 @@ project(':capacitor-splash-screen').projectDir = new File('../node_modules/@capa
|
|||
include ':capacitor-status-bar'
|
||||
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'
|
||||
project(':hugotomazi-capacitor-navigation-bar').projectDir = new File('../node_modules/@hugotomazi/capacitor-navigation-bar/android')
|
||||
|
|
|
@ -18,6 +18,7 @@ def capacitor_pods
|
|||
pod 'CapacitorShare', :path => '..\..\node_modules\@capacitor\share'
|
||||
pod 'CapacitorSplashScreen', :path => '..\..\node_modules\@capacitor\splash-screen'
|
||||
pod 'CapacitorStatusBar', :path => '..\..\node_modules\@capacitor\status-bar'
|
||||
pod 'CapacitorToast', :path => '..\..\node_modules\@capacitor\toast'
|
||||
pod 'HugotomaziCapacitorNavigationBar', :path => '..\..\node_modules\@hugotomazi\capacitor-navigation-bar'
|
||||
end
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
"@capacitor/share": "^1.1.2",
|
||||
"@capacitor/splash-screen": "^1.2.2",
|
||||
"@capacitor/status-bar": "^1.0.8",
|
||||
"@capacitor/toast": "^1.0.8",
|
||||
"@hugotomazi/capacitor-navigation-bar": "^1.1.1",
|
||||
"iconv-lite": "^0.6.3"
|
||||
}
|
||||
|
|
65
readme.md
|
@ -1,25 +1,41 @@
|
|||
# VueTube
|
||||
<img src="https://github.com/Frontesque/VueTube/raw/main/Icons/Stable.svg" alt="VueTube icon" width="200"/>
|
||||
A simple FOSS video streaming client aimed to recreate ALL features from their respective apps (and more)
|
||||
<p align="center">
|
||||
<a href="https://vuetube.app/">
|
||||
<img src="https://cdn.discordapp.com/attachments/751596360108605500/963045605071028274/vuetube-logo.svg" alt="VueTube icon" width="500"/>
|
||||
</a>
|
||||
</br>
|
||||
<sub>Logo by <a href="https://github.com/afnzmn">@afnzmn</a></sub>
|
||||
</br>
|
||||
</br>
|
||||
<strong>A simple FOSS video streaming client aimed to recreate ALL features from their respective apps (and more)</strong>
|
||||
</br>
|
||||
Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
|
||||
</p>
|
||||
|
||||
- Pronounced View Tube
|
||||
<p align="center">
|
||||
<a href="https://github.com/Frontesque/VueTube/commits/main"><img src="https://img.shields.io/github/commit-activity/m/Frontesque/VueTube?label=Commits" alt="Commits"></img></a>
|
||||
<a href="https://github.com/Frontesque/VueTube/issues" alt="Issues"><img src="https://img.shields.io/github/issues/Frontesque/VueTube"></img></a>
|
||||
<a><img src="https://img.shields.io/github/languages/count/Frontesque/VueTube" alt="Languages"></img></a>
|
||||
<a href="https://github.com/Frontesque/VueTube/blob/main/LICENSE" alt="License"><img src="https://img.shields.io/github/license/Frontesque/VueTube"></img></a>
|
||||
<a><img src="https://img.shields.io/github/stars/Frontesque/VueTube" alt="Stars"></img></a>
|
||||
<a><img src="https://img.shields.io/snyk/vulnerabilities/github/FrontEsque/VueTube" alt="Vulnerabilities"></img></a>
|
||||
<a><img src="https://img.shields.io/librariesio/github/Frontesque/VueTube" alt="Dependencies"></img></a>
|
||||
<a><img src="https://img.shields.io/tokei/lines/github/Frontesque/VueTube" alt="Lines"></img></a>
|
||||
<a href="https://github.com/Frontesque/VueTube/actions/workflows/ci.yml" alt="CI"><img src="https://github.com/Frontesque/VueTube/actions/workflows/ci.yml/badge.svg"></img></a>
|
||||
<a href="https://vuetube.app" alt="Website"><img src="https://img.shields.io/website?down_message=offline&up_message=online&url=https%3A%2F%2Fvuetube.app"></img></a>
|
||||
<a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a>
|
||||
<a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a>
|
||||
<a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
|
||||
|
||||
## Links
|
||||
- Website: https://vuetube.app
|
||||
- Discord: [![Discord](https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord)](https://discord.gg/7P8KJrdd5W)
|
||||
- Reddit: https://reddit.com/r/VueTube
|
||||
- Telegram: https://t.me/VueTube
|
||||
|
||||
## Builds
|
||||
[![Build](https://github.com/Frontesque/VueTube/actions/workflows/ci.yml/badge.svg)](https://github.com/Frontesque/VueTube/actions/workflows/ci.yml)
|
||||
|
||||
## Features
|
||||
- 🎨 Themes: Light, Dark, OLED
|
||||
# Features
|
||||
- 🎨 Themes: Light, Dark, OLED, All the colors of the rainbow
|
||||
- 🖌️ Customizable UI: You can fully customize the accent color, and other parts of the UI to remove features that you don't use!
|
||||
- ⬆️ Auto Update: Be notified when an update is available & downgrade if you dislike it!
|
||||
- 👁️ Tracking Protection: No telemetry is sent from your device to YouTube
|
||||
|
||||
### Plans
|
||||
# Install
|
||||
To install please visit www.vuetube.app/install
|
||||
|
||||
# Plans
|
||||
- 🔍 Advanced Search
|
||||
- 🗞️ Locally store watch history
|
||||
- 📺 A custom video player
|
||||
|
@ -28,17 +44,24 @@ A simple FOSS video streaming client aimed to recreate ALL features from their r
|
|||
- 🖼️ Picture in picture mode
|
||||
- and more!
|
||||
|
||||
### Technologies used
|
||||
# Screenshots
|
||||
View on our website: [https://vuetube.app/info/screenshots](https://vuetube.app/info/screenshots)
|
||||
|
||||
## Technologies used
|
||||
|
||||
<a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368956760074/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
|
||||
|
||||
### Why am I doing this?
|
||||
## Why am I doing this?
|
||||
Well this has been thrown around on the Return Youtube Dislike discord server for quite some time, so I figured that I should probably take a crack at it!
|
||||
|
||||
Also, YouTube Vanced just shut down
|
||||
|
||||
## Screenshots
|
||||
[View on our website: https://vuetube.app/info/screenshots](https://vuetube.app/info/screenshots)
|
||||
|
||||
## Want to contribute?
|
||||
Please read our website on how to do so: https://vuetube.app/contributing
|
||||
|
||||
# Contributors
|
||||
<a href="https://github.com/Frontesque/VueTube/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=Frontesque/VueTube" />
|
||||
</a>
|
||||
|
||||
<sub>Made with [contrib.rocks](https://contrib.rocks). </sub>
|
||||
|
|