This commit is contained in:
Alex 2023-06-04 18:53:59 +12:00
commit 7f8de63222
30 changed files with 718 additions and 231 deletions

16
.github/dependabot.yml vendored Normal file
View File

@ -0,0 +1,16 @@
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "npm"
directory: "/NUXT/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@ -15,9 +15,9 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
@ -29,7 +29,7 @@ jobs:
working-directory: NUXT
run: npm run generate
- name: Upload artifacts
uses: actions/upload-artifact@v2
uses: actions/upload-artifact@v3
with:
name: dist
path: dist
@ -39,20 +39,20 @@ jobs:
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: dist
path: dist
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
run: npm i
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
- name: Copy web assets to native platform
@ -63,7 +63,7 @@ jobs:
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
uses: actions/upload-artifact@v3
with:
name: android
path: android/app/build/outputs/apk/release/app-release.apk
@ -74,14 +74,14 @@ jobs:
needs: [build]
steps:
- name: Checkout
uses: actions/checkout@v2
uses: actions/checkout@v3
- name: Download artifacts
uses: actions/download-artifact@v2
uses: actions/download-artifact@v3
with:
name: dist
path: dist
- name: Set up Node.js ${{ env.NODE_VERSION }}
uses: actions/setup-node@v2
uses: actions/setup-node@v3
with:
node-version: ${{ env.NODE_VERSION }}
- name: Install dependencies
@ -109,7 +109,7 @@ jobs:
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
uses: actions/upload-artifact@v3
with:
name: iOS
path: VueTube.ipa

View File

@ -1,7 +1,7 @@
<template>
<div
class="author-comment-badge-renderer"
v-if="metadata && iconTypeMap.hasOwnProperty(metadata.icon.iconType)"
v-if="metadata && iconTypeMap.hasOwnProperty(metadata.icon)"
>
<v-tooltip top>
<template v-slot:activator="{ on, attrs }">

View File

@ -27,7 +27,6 @@
},
}"
mediagroup="vuetubecute"
autoplay
width="100%"
:src="vidSrc"
:height="isFullscreen ? '100%' : 'auto'"
@ -380,7 +379,7 @@ export default {
required: true,
},
recommends: {
type: Array,
type: Object,
default: () => {
return [];
},
@ -416,15 +415,78 @@ export default {
mounted() {
console.log("sources", this.sources);
console.log("recommends", this.recommends);
console.log("video", this.video);
this.vid = this.$refs.player;
this.aud = this.$refs.audio;
// TODO: this.$store.state.player.quality, check if exists and select the closest one
if (this.$store.state.player.preload) this.prebuffer(this.sources[5].url);
else {
this.audSrc = this.sources[this.sources.length - 1].url;
this.vidSrc = this.sources[5].url;
/**
* Video quality selection which device can play normally
* @returns {number[]}
*/
function getPreferredQuality() {
let width;
let height;
// Detecting device - https://stackoverflow.com/a/11381730/18543384
window.mobileCheck = function () {
let check = false;
(function (a) {
if (
/(android|bb\d+|meego).+mobile|avantgo|bada\/|blackberry|blazer|compal|elaine|fennec|hiptop|iemobile|ip(hone|od)|iris|kindle|lge |maemo|midp|mmp|mobile.+firefox|netfront|opera m(ob|in)i|palm( os)?|phone|p(ixi|re)\/|plucker|pocket|psp|series(4|6)0|symbian|treo|up\.(browser|link)|vodafone|wap|windows ce|xda|xiino/i.test(
a
) ||
/1207|6310|6590|3gso|4thp|50[1-6]i|770s|802s|a wa|abac|ac(er|oo|s-)|ai(ko|rn)|al(av|ca|co)|amoi|an(ex|ny|yw)|aptu|ar(ch|go)|as(te|us)|attw|au(di|\-m|r |s )|avan|be(ck|ll|nq)|bi(lb|rd)|bl(ac|az)|br(e|v)w|bumb|bw\-(n|u)|c55\/|capi|ccwa|cdm\-|cell|chtm|cldc|cmd\-|co(mp|nd)|craw|da(it|ll|ng)|dbte|dc\-s|devi|dica|dmob|do(c|p)o|ds(12|\-d)|el(49|ai)|em(l2|ul)|er(ic|k0)|esl8|ez([4-7]0|os|wa|ze)|fetc|fly(\-|_)|g1 u|g560|gene|gf\-5|g\-mo|go(\.w|od)|gr(ad|un)|haie|hcit|hd\-(m|p|t)|hei\-|hi(pt|ta)|hp( i|ip)|hs\-c|ht(c(\-| |_|a|g|p|s|t)|tp)|hu(aw|tc)|i\-(20|go|ma)|i230|iac( |\-|\/)|ibro|idea|ig01|ikom|im1k|inno|ipaq|iris|ja(t|v)a|jbro|jemu|jigs|kddi|keji|kgt( |\/)|klon|kpt |kwc\-|kyo(c|k)|le(no|xi)|lg( g|\/(k|l|u)|50|54|\-[a-w])|libw|lynx|m1\-w|m3ga|m50\/|ma(te|ui|xo)|mc(01|21|ca)|m\-cr|me(rc|ri)|mi(o8|oa|ts)|mmef|mo(01|02|bi|de|do|t(\-| |o|v)|zz)|mt(50|p1|v )|mwbp|mywa|n10[0-2]|n20[2-3]|n30(0|2)|n50(0|2|5)|n7(0(0|1)|10)|ne((c|m)\-|on|tf|wf|wg|wt)|nok(6|i)|nzph|o2im|op(ti|wv)|oran|owg1|p800|pan(a|d|t)|pdxg|pg(13|\-([1-8]|c))|phil|pire|pl(ay|uc)|pn\-2|po(ck|rt|se)|prox|psio|pt\-g|qa\-a|qc(07|12|21|32|60|\-[2-7]|i\-)|qtek|r380|r600|raks|rim9|ro(ve|zo)|s55\/|sa(ge|ma|mm|ms|ny|va)|sc(01|h\-|oo|p\-)|sdk\/|se(c(\-|0|1)|47|mc|nd|ri)|sgh\-|shar|sie(\-|m)|sk\-0|sl(45|id)|sm(al|ar|b3|it|t5)|so(ft|ny)|sp(01|h\-|v\-|v )|sy(01|mb)|t2(18|50)|t6(00|10|18)|ta(gt|lk)|tcl\-|tdg\-|tel(i|m)|tim\-|t\-mo|to(pl|sh)|ts(70|m\-|m3|m5)|tx\-9|up(\.b|g1|si)|utst|v400|v750|veri|vi(rg|te)|vk(40|5[0-3]|\-v)|vm40|voda|vulc|vx(52|53|60|61|70|80|81|83|85|98)|w3c(\-| )|webc|whit|wi(g |nc|nw)|wmlb|wonu|x700|yas\-|your|zeto|zte\-/i.test(
a.substr(0, 4)
)
)
check = true;
})(navigator.userAgent || navigator.vendor || window.opera);
return check;
};
// The smartphone screen width is smaller than the length (1080x1920).
// On a computer, the opposite is true (1920x1080).
// Detecting smaller side of the display.
if (window.mobileCheck) {
width = window.screen.width * window.devicePixelRatio;
height = window.screen.height * window.devicePixelRatio;
} else {
width = window.screen.height * window.devicePixelRatio;
height = window.screen.width * window.devicePixelRatio;
}
return [width, height];
}
// TODO: this.$store.state.player.quality, check if exists and select the closest one
let displayInfo = getPreferredQuality();
let indexOfPreferredQuality = 0;
console.warn(displayInfo);
for (let i = this.sources.length; i > 0; i--) {
if (i === this.sources.length) continue;
else {
// if quality height <= to the smaller side of the display,
// write the index of the source to a variable indexOfPreferredQuality.
if (
this.sources[i].height <= displayInfo[1] &&
this.sources[i].mimeType.includes("video")
) {
indexOfPreferredQuality = i;
}
if (this.sources[i].mimeType.indexOf("audio") > -1) {
this.audSrc = this.sources[i].url;
}
}
}
this.vidSrc = this.sources[indexOfPreferredQuality].url;
// this.prebuffer(this.sources[indexOfPreferredQuality].url);
this.sources.forEach((source) => {
if (source.mimeType.indexOf("audio") > -1) {
this.audSrc = source.url;
return;
}
});
// TODO: detect this.isMusic from the video or channel metadata instead of just SB segments
this.$youtube.getSponsorBlock(this.video.id, (data) => {
// console.warn("sbreturn", data);
if (Array.isArray(data)) {
@ -439,9 +501,7 @@ export default {
}
});
// TODO: detect this.isMusic from the video or channel metadata instead of just SB segments
this.$refs.player.addEventListener("loadeddata", this.loadedDataEvent);
this.aud.addEventListener("loadeddata", this.loadedAudioEvent);
},
created() {
screen.orientation.addEventListener("change", () =>
@ -452,19 +512,28 @@ export default {
this.cleanup();
},
methods: {
loadedAudioEvent() {
this.$refs.player.addEventListener("loadeddata", this.loadedDataEvent);
this.loadedDataEvent();
},
loadedDataEvent() {
// console.log(e);
// if (vid.networkState === vid.NETWORK_LOADING) {
// // The user agent is actively trying to download data.
// }
// networkState: An integer property that represents the network state of the video. The possible values are:
// NETWORK_EMPTY (0): No source has been set or the video element's load() method has not been called.
// NETWORK_IDLE (1): The video element's load() method has been called, and the video is fetching the media resource.
// NETWORK_LOADING (2): The video is in the process of downloading the media resource.
// NETWORK_NO_SOURCE (3): No suitable source found for the video.
// if (vid.readyState < vid.HAVE_FUTURE_DATA) {
// // There is not enough data to keep playing from this point
// }
if (this.vid.readyState >= 3) {
this.$refs.audio.play();
//readyState: An integer property that represents the readiness state of the video. The possible values are:
// HAVE_NOTHING (0): No information about the media resource is available.
// HAVE_METADATA (1): Basic metadata about the media resource, such as duration, is available.
// HAVE_CURRENT_DATA (2): Data for the current playback position is available but not enough to start playback.
// HAVE_FUTURE_DATA (3): Data for the current and at least the next frame is available.
// HAVE_ENOUGH_DATA (4): Enough data is available to start playback.
if (this.vid.readyState >= 3 && this.aud.readyState >= 3) {
this.vid.play();
this.aud.play();
this.bufferingDetected = false;
this.$refs.audio.currentTime = this.vid.currentTime;
if (!this.isMusic) {
this.$refs.audio.playbackRate = this.$store.state.player.speed;
@ -508,7 +577,11 @@ export default {
clearTimeout(this.bufferingDetected);
this.bufferingDetected = false;
}
if (this.$refs.audio.paused && !this.$refs.player.paused)
if (
this.$refs.audio.paused &&
!this.$refs.player.paused &&
this.$refs.player.readyState >= 3
)
this.$refs.audio.play();
this.buffered = (this.vid.buffered.end(0) / this.vid.duration) * 100;
},
@ -528,14 +601,14 @@ export default {
clearTimeout(this.bufferingDetected);
this.$refs.audio.currentTime = this.vid.currentTime;
this.bufferingDetected = false;
this.$refs.audio.play();
}
this.$refs.audio.play();
},
cleanup() {
if (this.xhr) this.xhr.abort();
if (this.isFullscreen) this.exitFullscreen();
if (this.bufferingDetected) clearTimeout(this.bufferingDetected);
screen.orientation.removeEventListener("change");
// screen.orientation.removeEventListener("change");
this.$refs.player.removeEventListener("loadeddata", this.loadedDataEvent);
this.$refs.player.removeEventListener("timeupdate", this.timeUpdateEvent);
this.$refs.player.removeEventListener("progress", this.progressEvent);
@ -551,9 +624,8 @@ export default {
"load",
() => {
if (this.xhr.status === 200) {
var blob = this.xhr.response;
console.error(this.xhr);
this.blobToDataURL(blob, (dataurl) => {
this.blobToDataURL(this.xhr.response, (dataurl) => {
console.log(dataurl);
this.vidSrc = dataurl;
this.buffered = 100;
@ -579,13 +651,10 @@ export default {
this.xhr.send();
},
// !NOTE: (BUG) too big to process for 1080p vids over 2 minutes
blobToDataURL(blob, callback) {
var a = new FileReader();
a.onload = function (e) {
callback(e.target.result);
};
a.readAsDataURL(blob);
let url = URL.createObjectURL(new Blob([blob]));
callback(url);
},
shortNext() {
this.shortTransition = true;

View File

@ -18,10 +18,7 @@
<script>
export default {
props: {
video: {
type: Object,
required: true,
},
video: {},
buffering: {
type: Boolean,
required: false,

View File

@ -38,10 +38,7 @@ export default {
type: Number,
required: true,
},
controls: {
type: Boolean,
required: true,
},
controls: {},
buffered: {
type: Number,
required: true,

View File

@ -7,7 +7,14 @@
scrollable
>
<template #activator="{ on, attrs }">
<v-btn fab text small color="white" v-bind="attrs" v-on="on">
<v-btn
fab
text
small
color="white"
v-bind="attrs"
v-on="on"
>
{{
sources.find((src) => src.url == currentSource.src).qualityLabel
? sources.find((src) => src.url == currentSource.src).qualityLabel
@ -29,7 +36,7 @@
</v-subheader>
<v-divider />
<v-card-text
style="max-height: 50vh"
style="max-height: 50vh; flex-direction: column !important"
class="pa-0 d-flex flex-column-reverse"
>
<v-list-item
@ -58,7 +65,7 @@
<v-list-item-title>
{{ src.qualityLabel ? src.qualityLabel : "" }} ({{
src.quality
}}) {{ src.bitrate }}bps
}}) {{ (src.bitrate / 1000000).toFixed(2) }}Mbps
</v-list-item-title>
<v-list-item-subtitle>
{{ src.mimeType }} {{ src.averageBitrate }}
@ -72,12 +79,10 @@
</template>
<script>
export default {
props: {
currentSource: {
type: String,
required: true,
},
currentSource: {},
sources: {
type: Array,
required: true,

View File

@ -58,14 +58,8 @@
<script>
export default {
props: {
video: {
type: Object,
required: true,
},
controls: {
type: Boolean,
required: true,
},
video: {},
controls: {},
fullscreen: {
type: Boolean,
required: true,
@ -74,10 +68,7 @@ export default {
type: Array,
required: true,
},
currentTime: {
type: Number,
required: true,
},
currentTime: {},
duration: {
type: Number,
required: true,

View File

@ -44,10 +44,7 @@ export default {
type: Boolean,
required: true,
},
controls: {
type: Boolean,
required: true,
},
controls: {},
},
data: () => ({
colors: {

View File

@ -12,14 +12,8 @@ export default {
type: Number,
required: true,
},
currentTime: {
type: Number,
required: true,
},
controls: {
type: Boolean,
required: true,
},
currentTime: {},
controls: {},
},
computed: {
humanDuration() {

View File

@ -84,10 +84,10 @@ export default {
try {
const videoId =
this.playlist.videos.length === 0 ? "" : this.playlist.videos[0].id;
return `https://img.youtube.com/vi/${videoId}/maxresdefault.jpg`;
return `https://img.youtube.com/vi/${videoId}/hqdefault.jpg`;
} catch (e) {
alert(e.message);
return `https://img.youtube.com/vi//maxresdefault.jpg`;
return `https://img.youtube.com/vi//hqdefault.jpg`;
}
},
},

View File

@ -89,7 +89,7 @@ export default {
props: { video: { type: Object, required: true } },
computed: {
thumbnail() {
return `https://img.youtube.com/vi/${this.video.id}/maxresdefault.jpg`;
return `https://img.youtube.com/vi/${this.video.id}/hqdefault.jpg`;
},
},
methods: {

View File

@ -0,0 +1,133 @@
<template>
<div class="yt-text-formatter">
<p v-html="formatTitle">
{{ textRuns.content }}
</p>
</div>
</template>
<script>
export default {
props: {
textRuns: {},
additionalInfo: {},
},
emits: ["play", "pause"],
data: () => ({
paused: false,
}),
computed: {
formatTitle() {
let img =
"<img src = 'https://www.gstatic.com/youtube/img/watch/yt_favicon.png' style=\"height: 10px; width: 14px;\"/>";
if (this.textRuns.content && this.textRuns.commandRuns) {
let tempContent = this.textRuns.content;
let arrayWithReplaceParts = [];
this.textRuns.commandRuns.forEach((commandRun) => {
//[textToReplace, urlFromEndpoint]
const textBeforeReplace = this.textRuns.content.substring(
commandRun.startIndex,
commandRun.startIndex + commandRun.length
);
// console.log("isExistInArray "+ textBeforeReplace + ": " + isExistInArray);
if (commandRun.onTap.innertubeCommand.commandMetadata) {
arrayWithReplaceParts.push([
textBeforeReplace,
commandRun.onTap.innertubeCommand.commandMetadata
.webCommandMetadata.url,
]);
} else if (commandRun.onTap.innertubeCommand.urlEndpoint) {
arrayWithReplaceParts.push([
this.textRuns.content.substring(
commandRun.startIndex,
commandRun.startIndex + commandRun.length
),
commandRun.onTap.innertubeCommand.urlEndpoint.url,
]);
}
});
const duplicates = [];
arrayWithReplaceParts = arrayWithReplaceParts.filter((subArr) => {
const isFirstElementDuplicate = duplicates.includes(subArr[0]);
if (isFirstElementDuplicate) {
return false;
} else {
duplicates.push(subArr[0]);
return true;
}
});
console.log(arrayWithReplaceParts);
for (let i = 0; i < arrayWithReplaceParts.length; i++) {
for (let j = i + 1; j < arrayWithReplaceParts.length; j++) {
if (
JSON.stringify(arrayWithReplaceParts[i]) ===
JSON.stringify(arrayWithReplaceParts[j])
) {
arrayWithReplaceParts.splice(j, 1);
j--;
}
}
}
// Replacing urls in description
arrayWithReplaceParts.forEach((text) => {
if (text[1].indexOf("/hashtag/") > -1) {
//skip
} else if (text[1].indexOf("watch?v=") > -1) {
let nameOfUrl = text[0].replace(/   •/, " •");
let newUrl =
"<a" +
' onclick=openInternal("' +
text[1] +
'") style="background-color: rgba(0,0,0,0.051); border-radius: 8px; white-space: nowrap;">' +
img +
nameOfUrl +
"</a>";
tempContent = tempContent.replaceAll(text[0], newUrl);
} else if (
text[1].indexOf("/channel/") > -1 ||
text[1].indexOf("youtube.com/c/") > -1
) {
let nameOfUrl = text[0].replace(/   /, " ").replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
let newUrl =
"<a" +
' onclick=openInternal("channel") style="background-color: rgba(0,0,0,0.051); border-radius: 8px; white-space: nowrap;">' +
img +
nameOfUrl +
"</a>";
tempContent = tempContent.replaceAll(
new RegExp(`\\b${text[0]}\\b`, "g"),
newUrl
);
} else {
let params = new Proxy(new URLSearchParams(text[1]), {
get: (searchParams, prop) => searchParams.get(prop),
});
let url = decodeURI(params.q);
let newUrl =
"<a" + ' onclick=openExternal("' + url + '")>' + text[0] + "</a>";
tempContent = tempContent.replaceAll(text[0], newUrl);
}
});
return tempContent;
} else {
return this.textRuns.content;
}
},
},
created() {
window.openExternal = this.openExternal;
window.openInternal = this.openInternal;
},
methods: {
openExternal(url) {
this.$vuetube.openExternal(url);
},
async openInternal(url) {
await this.$router.push(url);
},
},
};
</script>

View File

@ -1,25 +1,24 @@
<template>
<div class="description" v-if="render.descriptionBodyText">
<yt-text-formatter :textRuns="render.descriptionBodyText.runs">
<div v-if="render.attributedDescriptionBodyText.content" class="description">
<yt-text-formatter :text-runs="render.attributedDescriptionBodyText">
</yt-text-formatter>
</div>
</template>
<script>
import YtTextFormatter from "~/components/UtilRenderers/YtTextFormatterNew.vue";
export default {
components: {
YtTextFormatter,
},
props: ["render"],
};
</script>
<style scoped>
.description {
white-space: pre-line;
font-size: 0.9rem;
}
</style>
<script>
import YtTextFormatter from "~/components/UtilRenderers/YtTextFormatter.vue";
export default {
props: ["render"],
components: {
YtTextFormatter,
},
};
</script>

View File

@ -30,12 +30,20 @@ export default {
computed: {
thumbnailOverlayText() {
return this.video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer
.text.runs[0].text;
this.video.thumbnailOverlays.forEach((thumbnail) => {
if (thumbnail.thumbnailOverlayTimeStatusRenderer) {
return thumbnail.thumbnailOverlayTimeStatusRenderer.text.runs[0].text;
}
});
return "";
},
thumbnailOverlayStyle() {
return this.video.thumbnailOverlays[0].thumbnailOverlayTimeStatusRenderer
.style;
this.video.thumbnailOverlays.forEach((thumbnail) => {
if (thumbnail.thumbnailOverlayTimeStatusRenderer) {
return thumbnail.thumbnailOverlayTimeStatusRenderer.style;
}
});
return "DEFAULT";
},
},

View File

@ -89,7 +89,7 @@ export default {
props: ["video"],
computed: {
thumbnail() {
return `https://img.youtube.com/vi/${this.video.id}/maxresdefault.jpg`;
return `https://img.youtube.com/vi/${this.video.id}/hqdefault.jpg`;
},
},
methods: {
@ -98,4 +98,4 @@ export default {
},
},
};
</script>
</script>

View File

@ -11,24 +11,24 @@
},
"dependencies": {
"@capacitor/splash-screen": "^1.2.2",
"@capacitor/status-bar": "^1.0.8",
"@capacitor/status-bar": "^5.0.2",
"core-js": "^3.25.0",
"nuxt": "^2.15.8",
"nuxt": "^3.5.2",
"uuid": "^9.0.0",
"vue": "^2.7.10",
"vue-server-renderer": "^2.7.10",
"vue-template-compiler": "^2.7.10",
"vuetify": "^2.6.9",
"webpack": "^4.46.0"
"webpack": "^5.84.1"
},
"devDependencies": {
"@nuxtjs/vuetify": "^1.12.3",
"babel-eslint": "^10.1.0",
"eslint": "^7.32.0",
"eslint": "^8.42.0",
"eslint-config-prettier": "^8.3.0",
"eslint-loader": "^4.0.2",
"eslint-plugin-prettier": "^4.2.1",
"eslint-plugin-vue": "^8.2.0",
"eslint-plugin-vue": "^9.14.1",
"prettier": "^2.5.1"
}
}

View File

@ -68,8 +68,15 @@ export default {
.then((result) => {
if (result) this.recommends = [result];
})
.catch(error => {});
.catch((error) => {
console.error(error);
});
}
},
};
</script>
<style>
a {
-webkit-user-drag: none;
}
</style>

View File

@ -55,13 +55,19 @@
>
<div>
{{ lang.published }}:
{{ new Date(update.created_at).toLocaleString() }}
{{ new Date(latestVersion.assets[0].created_at).toLocaleString() }}
</div>
<div>
{{ lang.size }}:
{{ require("~/plugins/utils").humanFileSize(update.size) }}
{{
require("~/plugins/utils").humanFileSize(
latestVersion.assets[0].size
)
}}
</div>
<div>
{{ lang.users }}: {{ latestVersion.assets[0].download_count }}
</div>
<div>{{ lang.users }}: {{ update.download_count }}</div>
</div>
<div
@ -123,7 +129,9 @@ export default {
latestVersion: "",
lang: {},
status: "checking",
update: {},
update: {
created_at: "",
},
downloading: false,
};
},
@ -185,7 +193,7 @@ export default {
async install() {
this.downloading = true;
await this.$update(this.update.browser_download_url).catch(() => {
await this.$update(this.latestVersion.assets[0].url).catch(() => {
this.downloading = false;
});
//window.open(this.update.browser_download_url, '_blank');

View File

@ -40,6 +40,19 @@ module.exports = {
return headers;
},
INNERTUBE_NEW_HEADER: (info) => {
let headers = {
accept: "*/*",
"user-agent": info.userAgent,
"accept-language": `${info.hl}-${info.gl},${info.hl};q=0.9`,
"content-type": "application/json",
"x-goog-authuser": 0,
"x-goog-visitor-id": info.visitorData || "",
"x-youtube-client-name": "2",
"x-youtube-client-version": "2.20230502.01.00",
};
return headers;
},
INNERTUBE_CLIENT: (info) => {
let client = {
gl: info.gl,
@ -59,4 +72,43 @@ module.exports = {
};
return client;
},
INNERTUBE_VIDEO: (info) => {
let client = {
gl: info.gl,
hl: info.hl,
deviceMake: info.deviceMake,
deviceModel: info.deviceModel,
userAgent: info.userAgent,
clientName: "MWEB",
clientVersion: "2.20230502.01.00",
osName: info.osName,
osVersion: info.osVersion,
platform: "MOBILE",
playerType: "UNIPLAYER",
screenPixelDensity: "3",
originalUrl: info.originalUrl,
configInfo: info.configInfo,
remoteHost: info.remoteHost,
visitorData: info.visitorData,
clientFormFactor: "SMALL_FORM_FACTOR",
screenDensityFloat: "1",
timeZone: info.timeZone,
browserName: info.browserName,
browserVersion: info.browserVersion,
acceptHeader:
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7",
deviceExperimentId: info.deviceExperimentId,
screenWidthPoints: info.screenWidthPoints,
screenHeightPoints: info.screenHeightPoints,
utcOffsetMinutes: info.utcOffsetMinutes,
userInterfaceTheme: "USER_INTERFACE_THEME_LIGHT",
memoryTotalKbytes: "8000000",
clientScreen: "WATCH",
mainAppWebInfo: {
webDisplayMode: "WEB_DISPLAY_MODE_BROWSER",
isWebNativeShareAvailable: true,
},
};
return client;
},
};

View File

@ -8,7 +8,7 @@
import { Http } from "@capacitor-community/http";
import { getBetweenStrings, delay } from "./utils";
import rendererUtils from "./renderers";
import constants from "./constants";
import constants, { YT_API_VALUES } from "./constants";
class Innertube {
//--- Initiation ---//
@ -16,23 +16,122 @@ class Innertube {
constructor(ErrorCallback) {
this.ErrorCallback = ErrorCallback || undefined;
this.retry_count = 0;
this.playerParams = "";
this.signatureTimestamp = 0;
}
checkErrorCallback() {
return typeof this.ErrorCallback === "function";
}
async makeDecipherFunction(html) {
// Get url of base.js file
const baseJsUrl =
constants.URLS.YT_URL +
getBetweenStrings(html.data, '"jsUrl":"', '","cssUrl"');
// Get base.js content
const baseJs = await Http.get({
url: baseJsUrl,
}).catch((error) => error);
// Example:
//;var IF={k4:function(a,b){var c=a[0];a[0]=a[b%a.length];a[b%a.length]=c},
// VN:function(a){a.reverse()},
// DW:function(a,b){a.splice(0,b)}};
let isMatch;
if (
/;var [A-Za-z]+=\{[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\},\n[A-Za-z0-9]+:function\(a\)\{[^}]*\},\n[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\}\};/.exec(
baseJs.data
)
) {
isMatch =
/;var [A-Za-z]+=\{[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\},\n[A-Za-z0-9]+:function\(a\)\{[^}]*\},\n[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\}\};/.exec(
baseJs.data
);
} else if (
/;var [A-Za-z]+=\{[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\},\n[A-Za-z0-9]+:function\([A-Za-z],[A-Za-z]\)\{[^}]*\},\n[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\}\};/.exec(
baseJs.data
)
) {
isMatch =
/;var [A-Za-z]+=\{[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\},\n[A-Za-z0-9]+:function\([A-Za-z],[A-Za-z]\)\{[^}]*\},\n[A-Za-z0-9]+:function\([^)]*\)\{[^}]*\}\};/.exec(
baseJs.data
);
}
if (isMatch) {
console.log("The input string matches the regex pattern.");
const firstPart = isMatch[0].substring(1);
if (
/\{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return [A-Za-z]\.join\(""\)\};/.exec(
baseJs.data
)
) {
isMatch =
/\{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return [A-Za-z]\.join\(""\)\};/.exec(
baseJs.data
);
} else if (
/{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return +[A-Za-z]\.join\(""\)};/.exec(
baseJs.data
)
) {
isMatch =
/{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return +[A-Za-z]\.join\(""\)};/.exec(
baseJs.data
);
} else if (
/\{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return +[A-Za-z]\.join\(""\)};/.exec(
baseJs.data
)
) {
isMatch =
/\{[A-Za-z]=[A-Za-z]\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return +[A-Za-z]\.join\(""\)};/.exec(
baseJs.data
);
} else {
isMatch =
/\{a=a\.split\(""\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);[A-Za-z]+\.[A-Za-z0-9]+\([^)]*\);return a\.join\(""\)\};/.exec(
baseJs.data
);
}
if (!isMatch) {
console.warn(
"The second part of decipher string does not match the regex pattern."
);
}
// Example:
// {a=a.split("");IF.k4(a,4);IF.VN(a,68);IF.DW(a,2);IF.VN(a,66);IF.k4(a,19);IF.DW(a,2);IF.VN(a,36);IF.DW(a,2);IF.k4(a,41);return a.join("")};
// Get second part of decipher function
const secondPart =
"var decodeUrl=function(a)" + isMatch[0] + "return decodeUrl;";
let decodeFunction = firstPart + secondPart;
let decodeUrlFunction = new Function(decodeFunction);
this.decodeUrl = decodeUrlFunction();
let signatureIntValue = /.sts="[0-9]+";/.exec(baseJs.data);
// Get signature timestamp
this.signatureTimestamp = signatureIntValue[0].replace(/\D/g, "");
} else {
console.warn(
"The first part of decipher string does not match the regex pattern."
);
}
}
async initAsync() {
const html = await Http.get({
url: constants.URLS.YT_URL,
params: { hl: "en" },
}).catch((error) => error);
await this.makeDecipherFunction(html);
try {
if (html instanceof Error && this.checkErrorCallback)
this.ErrorCallback(html.message, true);
try {
const data = JSON.parse(
getBetweenStrings(html.data, "ytcfg.set(", ");")
"{" + getBetweenStrings(html.data, "ytcfg.set({", ");")
);
if (data.INNERTUBE_CONTEXT) {
this.key = data.INNERTUBE_API_KEY;
@ -81,7 +180,11 @@ class Innertube {
//--- API Calls ---//
async browseAsync(action_type, args = {}) {
let data = { context: this.context };
let data = {
context: {
client: constants.INNERTUBE_CLIENT(this.context.client),
},
};
switch (action_type) {
case "recommendations":
@ -160,7 +263,12 @@ class Innertube {
}
async getVidAsync(id) {
let data = { context: this.context, videoId: id };
let data = {
context: {
client: constants.INNERTUBE_VIDEO(this.context.client),
},
videoId: id,
};
const responseNext = await Http.post({
url: `${constants.URLS.YT_BASE_API}/next?key=${this.key}`,
data: {
@ -179,8 +287,34 @@ class Innertube {
const response = await Http.post({
url: `${constants.URLS.YT_BASE_API}/player?key=${this.key}`,
data: data,
headers: constants.INNERTUBE_HEADER(this.context.client),
data: {
...data,
...{
playerParams: this.playerParams,
contentCheckOk: false,
mwebCapabilities: {
mobileClientSupportsLivestream: true,
},
playbackContext: {
contentPlaybackContext: {
currentUrl: "/watch?v=" + id + "&pp=" + this.playerParams,
vis: 0,
splay: false,
autoCaptionsDefaultOn: false,
autonavState: "STATE_NONE",
html5Preference: "HTML5_PREF_WANTS",
signatureTimestamp: this.signatureTimestamp,
referer: "https://m.youtube.com/",
lactMilliseconds: "-1",
watchAmbientModeContext: {
watchAmbientModeEnabled: true,
},
},
},
},
},
// headers: constants.INNERTUBE_HEADER(this.context.client),
headers: constants.INNERTUBE_NEW_HEADER(this.context.client),
}).catch((error) => error);
if (response.error)
@ -270,7 +404,7 @@ class Innertube {
static getThumbnail(id, resolution) {
if (resolution == "max") {
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
const url = `https://img.youtube.com/vi/${id}/hqdefault.jpg`;
let img = new Image();
img.src = url;
img.onload = function () {
@ -319,6 +453,8 @@ class Innertube {
const responseInfo = response.data.output;
const responseNext = response.data.outputNext;
const details = responseInfo.videoDetails;
const publishDate =
responseInfo.microformat.playerMicroformatRenderer.publishDate;
// const columnUI =
// responseInfo[3].response?.contents.singleColumnWatchNextResults?.results
// ?.results;
@ -337,6 +473,33 @@ class Innertube {
(content) => content.slimOwnerRenderer
)?.slimOwnerRenderer;
try {
console.log(vidMetadata.contents);
this.playerParams =
ownerData.navigationEndpoint.watchEndpoint.playerParams;
} catch (e) {}
// Deciphering urls
resolutions.formats
.concat(resolutions.adaptiveFormats)
.forEach((source) => {
if (source.signatureCipher) {
const params = new Proxy(
new URLSearchParams(source.signatureCipher),
{
get: (searchParams, prop) => searchParams.get(prop),
}
);
if (params.s) {
let cipher = decodeURIComponent(params.s);
let decipheredValue = this.decodeUrl(cipher);
// console.log("decipheredValue", decipheredValue);
source["url"] = (params.url + "&sig=" + decipheredValue).replace(
/&amp;/g,
"&"
);
}
}
});
const vidData = {
id: details.videoId,
title: details.title,
@ -350,6 +513,7 @@ class Innertube {
availableResolutions: resolutions?.formats,
availableResolutionsAdaptive: resolutions?.adaptiveFormats,
metadata: {
publishDate: publishDate,
contents: vidMetadata.contents,
description: details.shortDescription,
thumbnails: details.thumbnails?.thumbnails,

View File

@ -7,7 +7,7 @@ module.exports = {
library: "Knihovna",
restart: "Restartovat",
later: "Později",
settingRestart: "Úprava tohoto nastavení vyžaduje restart aplikace pro provedení změn."
settingRestart: "Úprava tohoto nastavení vyžaduje restartování aplikace pro provedení změn."
},
index: {
@ -31,7 +31,7 @@ module.exports = {
mods: {
general: {
language: "Jatyk",
language: "Jazyk",
},
theme: {
normal: "Normální",
@ -63,7 +63,7 @@ module.exports = {
more: "Více",
},
about: {
appinformation: "Informace o aplikace",
appinformation: "Informace o aplikaci",
appversion: "Verze aplikace",
deviceinformation: "Informace o zařízení",
platform: "Platforma",

View File

@ -55,7 +55,9 @@ module.exports = {
language: "言語",
backup: "バックアップ",
backupinfo: "アプリの設定をバックアップ、または復元する。",
restore: "復元"
restore: "復元",
personalizedrecommendations: "Personalized recommendations",
personalizedrecommendationsinfo: "Receive personalized recommendations in exchange for sending watch time telemetry.",
},
theme: {
normal: "通常",
@ -73,6 +75,17 @@ module.exports = {
roundthumbnails: "丸みを帯びたサムネイル",
roundwatchpagecomponents: "視聴画面のコンポーネントを丸く",
radius: "半径",
launchscreen: "起動画面",
centeredlayout: "中央揃えのレイアウト",
fullscreenlayout: "全画面レイアウト",
themedicon: "テーマアイコン",
bottomnavigation: "下部のナビゲーション",
shift: "シフト",
showlabels: "ラベルを表示する",
mdi: "MDI",
materialsymbols: "マテリアルシンボル",
fluentuiicons: "FluentUI アイコン",
ibmcarbonicons: "IBM カーボン アイコン",
},
startup: {
defaultpage: "起動時のページ",

View File

@ -1,93 +1,121 @@
module.exports = {
name: "Українська",
global: {
home: "Головна",
subscriptions: "Підписки",
library: "Бібліотека",
restart: "Перезапустити",
later: "Пізніше",
settingRestart: "Зміна цього параметра вимагає перезапуску"
},
index: {
connecting: "Підключення",
plugins: "Завантаження плагінів",
launching: "Запуск",
},
settings: {
general: "Загальні",
theme: "Тема",
player: "Відеоплеєр",
uitweaker: "Налаштування UI",
startupoptions: "Параметри запуску",
plugins: "Плагіни",
updates: "Оновлення",
logs: "Журнали",
about: "Про додаток",
devmode: "Редактор реєстру",
},
mods: {
general: {
language: "Мова",
},
theme: {
normal: "Звичайна",
adaptive: "Адаптивна",
custom: "Власна",
dark: "Темна",
black: "Чорна",
darkmode: "Темний режим",
darkmodetagline: "Bravo Six, Going Dark.",
},
tweaks: {
fullscreen: "Повноекранний режим",
navbarblur: "Розмиття навігаційної панелі",
roundedcorners: "Закруглені кути",
roundthumbnails: "Закруглені мініатюри",
roundwatchpagecomponents: "Закруглені компоненти сторінки перегляду",
radius: "Радіус",
},
startup: {
defaultpage: "Домашня сторінка",
},
updates: {
install: "Встановити",
view: "Переглянути",
latest: "Остання",
installed: "Встановлена",
},
logs: {
more: "Більше",
},
about: {
appinformation: "Інформація про додаток",
appversion: "Версія додатку",
deviceinformation: "Інформація про пристрій",
platform: "Платформа",
os: "Операційна система",
model: "Модель",
manufacturer: "Виробник",
emulator: "Емулятор",
github: "GitHub",
discord: "Discord",
},
},
events: {
welcome: "Ласкаво просимо до VueTube",
tagline: "Майбутнє відео-стрімінгу",
next: "Далі",
updated: "VueTube оновлено!",
awesome: "Добре!",
langsetup: "Давайте виберемо мову!",
featuresetup: "Давайте виберемо деякі функції!",
enableryd: "Увімкнути Return YouTube Dislike",
enablespb: "Увімкнути SponsorBlock",
thanks: "Дякуємо за використання VueTube",
enjoy: "Ми сподіваємося, що вам сподобається!",
packageinstaller: "Виберіть пакет для завантаження"
},
};
module.exports = {
name: "Українська",
global: {
home: "Головна",
subscriptions: "Підписки",
library: "Бібліотека",
restart: "Перезапустити",
later: "Пізніше",
settingRestart: "Зміна цього параметра вимагає перезапуску"
},
index: {
connecting: "Підключення",
plugins: "Завантаження плагінів",
launching: "Запуск",
},
settings: {
general: "Загальні",
theme: "Тема",
player: "Відеоплеєр",
uitweaker: "Налаштування UI",
startupoptions: "Параметри запуску",
plugins: "Плагіни",
updates: "Оновлення",
logs: "Журнали",
about: "Про додаток",
devmode: "Редактор реєстру",
},
mods: {
general: {
language: "Мова",
backup: "Backup",
backupinfo: "Backup or restore your application settings",
restore: "Restore",
personalizedrecommendations: "Персоналізовані рекомендації",
personalizedrecommendationsinfo:
"Отримуйте персоналізовані рекомендації в обмін на надсилання телеметричних даних про час перегляду.",
},
developer: {
registryeditor: "Registry editor",
registrywarning: "CHANGING ENTRIES MAY CAUSE YOUR APP TO BREAK!",
createentry: "Create entry",
createentryfull: "Create registry entry",
cancel: "Cancel",
create: "Create",
key: "Key",
value: "Value",
confirmdelete: "Confirm delete",
areyousure: "Are you sure that you want to delete?",
delete: "Delete",
change: "Change",
},
theme: {
normal: "Звичайна",
adaptive: "Адаптивна",
custom: "Власна",
dark: "Темна",
black: "Чорна",
darkmode: "Темний режим",
darkmodetagline: "Bravo Six, Going Dark.",
},
tweaks: {
fullscreen: "Повноекранний режим",
navbarblur: "Розмиття навігаційної панелі",
roundedcorners: "Закруглені кути",
roundthumbnails: "Закруглені мініатюри",
roundwatchpagecomponents: "Закруглені компоненти сторінки перегляду",
radius: "Радіус",
},
startup: {
defaultpage: "Домашня сторінка",
},
updates: {
install: "Встановити",
view: "Переглянути",
latest: "Остання",
installed: "Встановлена",
size: "Розмір",
users: "Завантажень",
published: "Опубліковано",
okay: "ОК",
refresh: "Оновити",
update: "Завантажити",
later: "Пізніше",
},
logs: {
more: "Більше",
},
about: {
appinformation: "Інформація про додаток",
appversion: "Версія додатку",
deviceinformation: "Інформація про пристрій",
platform: "Платформа",
os: "Операційна система",
model: "Модель",
manufacturer: "Виробник",
emulator: "Емулятор",
github: "GitHub",
discord: "Discord",
},
},
events: {
welcome: "Ласкаво просимо до VueTube",
tagline: "Майбутнє відео-стрімінгу",
next: "Далі",
updated: "VueTube оновлено!",
awesome: "Добре!",
langsetup: "Давайте виберемо мову!",
featuresetup: "Давайте виберемо деякі функції!",
enableryd: "Увімкнути Return YouTube Dislike",
enablespb: "Увімкнути SponsorBlock",
thanks: "Дякуємо за використання VueTube",
enjoy: "Ми сподіваємося, що вам сподобається!",
packageinstaller: "Виберіть пакет для завантаження"
},
};

View File

@ -13,7 +13,7 @@ class rendererUtils {
} else if (base.watchEndpoint) {
return `/watch?v=${base.watchEndpoint.videoId}`;
} else if (base.navigationEndpoint) {
return; //for now
return base.navigationEndpoint.browseEndpoint.canonicalBaseUrl; //for now
} else if (base.searchEndpoint) {
return `/search?q=${encodeURI(base.searchEndpoint.query)}`;
}

View File

@ -102,7 +102,7 @@ const innertubeModule = {
getThumbnail(id, resolution, backupThumbnail) {
if (resolution == "max") {
const url = `https://img.youtube.com/vi/${id}/maxresdefault.jpg`;
const url = `https://img.youtube.com/vi/${id}/hqdefault.jpg`;
let img = new Image();
img.src = url;
img.onload = function () {

View File

@ -6,7 +6,7 @@ const getDefaultState = () => {
banner: null,
title: null,
subscribe: null,
subscribeAlt: null,
subscribeAlt: "",
descriptionPreview: null,
subscribers: null,
videosCount: null,
@ -22,12 +22,18 @@ export const actions = {
Object.assign(state, getDefaultState());
state.loading = true;
console.log(channelUrl);
const channelRequest =
let channelRequest = "";
console.warn(channelUrl);
if (
channelUrl.includes("/c/") ||
channelUrl.includes("/user/") ||
channelUrl.includes("/channel/")
? `https://youtube.com/${channelUrl}`
: `https://youtube.com/channel/${channelUrl}`;
channelUrl.includes("/channel/") ||
channelUrl.includes("/@")
) {
channelRequest = `https://youtube.com/${channelUrl}`;
} else {
channelRequest = `https://youtube.com/channel/${channelUrl}`;
}
this.$youtube
.getChannel(channelRequest)
.then((channel) => {

View File

@ -14,7 +14,10 @@ export const state = () => ({
export const mutations = {
initPlayer(state) {
if (process.client) {
state.loop = JSON.parse(localStorage.getItem("loop")) === true; // defaults to false
state.loop =
localStorage.getItem("loop") !== "undefined"
? JSON.parse(localStorage.getItem("loop"))
: true; // defaults to false
state.speed = JSON.parse(localStorage.getItem("speed")) || 1; // defaults to 1
state.speedAutosave = !(
// false if false, defaults to true

View File

@ -21,7 +21,7 @@
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />>
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />

View File

@ -4,7 +4,7 @@
"@capacitor/android": "^3.7.0",
"@capacitor/app": "^1.1.1",
"@capacitor/cli": "^5.0.4",
"@capacitor/core": "^3.7.0",
"@capacitor/core": "^5.0.4",
"@capacitor/device": "^1.1.2",
"@capacitor/filesystem": "^1.1.0",
"@capacitor/haptics": "^1.1.4",