Merge branch 'main' of https://github.com/VueTubeApp/VueTube
8
.github/ISSUE_TEMPLATE/config.yml
vendored
Normal file
|
@ -0,0 +1,8 @@
|
|||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: 💬 Discord
|
||||
url: https://vuetube.app/discord
|
||||
about: Join the Discord server to chat and ask questions
|
||||
- name: 💬 Telegram
|
||||
url: https://t.me/vuetube
|
||||
about: Join the Telegram group to chat and ask questions
|
37
.github/ISSUE_TEMPLATE/question.yml
vendored
Normal file
|
@ -0,0 +1,37 @@
|
|||
name: ❓ Question
|
||||
description: Ask a quesion related to VueTube
|
||||
labels: [question]
|
||||
body:
|
||||
|
||||
- type: textarea
|
||||
id: question
|
||||
attributes:
|
||||
label: Ask your question
|
||||
description: What do you want to know?
|
||||
placeholder: |
|
||||
Example:
|
||||
"How do I add a plugin?"
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: textarea
|
||||
id: aditional-info
|
||||
attributes:
|
||||
label: Aditional information
|
||||
placeholder: |
|
||||
Additional useful information, for example, a screenshot.
|
||||
|
||||
- type: checkboxes
|
||||
id: acknowledgements
|
||||
attributes:
|
||||
label: Acknowledgements
|
||||
description: Your question will be closed if you haven't done these steps.
|
||||
options:
|
||||
- label: I have searched the existing issues and this is a new question, **NOT** a duplicate or related to another open issue.
|
||||
required: true
|
||||
- label: I have written a short but informative title.
|
||||
required: true
|
||||
- label: I will fill out all of the requested information in this form.
|
||||
required: true
|
||||
- label: My question isn't asked in FAQ (Frequently Asked Questions).
|
||||
required: true
|
2
.github/workflows/ci.yml
vendored
|
@ -106,7 +106,7 @@ jobs:
|
|||
CODE_SIGNING_ALLOWED="NO"
|
||||
CODE_SIGN_ENTITLEMENTS=""
|
||||
- name: Make IPA
|
||||
run: mkdir Payload && mv ~/Library/Developer/Xcode/DerivedData/App-*/Build/Products/Debug-maccatalyst/App.app/ Payload && zip -r Payload.zip Payload && mv Payload.zip VueTube.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
|
||||
|
|
|
@ -1,14 +0,0 @@
|
|||
<svg width="512" height="512" viewBox="0 0 512 512" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect width="512" height="512" fill="url(#paint0_linear_6_11)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M327.768 269.391C338.454 263.236 338.454 247.814 327.766 241.66L327.761 241.658L291.527 220.81L331.399 202.218L345.714 210.453L345.736 210.466L386.392 233.857C403.102 243.471 403.102 267.582 386.392 277.196L345.743 300.582L345.714 300.599L328.778 310.343L288.905 291.75L327.761 269.395L327.768 269.391ZM206 290.605V311.783V311.79C206.005 324.092 219.314 331.788 229.979 325.652L252.89 312.471L292.763 331.063L247.932 356.856L247.903 356.873L207.467 380.137C190.801 389.725 170 377.695 170 358.467L170 311.783L170 273.818L206 290.605ZM295.384 181.497L255.512 200.09L229.979 185.4L229.964 185.392C219.3 179.27 206 186.968 206 199.269V223.177L170 239.965L170 199.269L170 152.585C170 133.357 190.801 121.327 207.467 130.915L247.917 154.187L247.932 154.196L295.384 181.497Z" fill="url(#paint1_linear_6_11)"/>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_6_11" x1="0" y1="0" x2="512" y2="512" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#1B3245"/>
|
||||
<stop offset="1" stop-color="#0C2028"/>
|
||||
</linearGradient>
|
||||
<linearGradient id="paint1_linear_6_11" x1="170" y1="128" x2="431" y2="389" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#00E1C3"/>
|
||||
<stop offset="1" stop-color="#00D1D5"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
</svg>
|
Before Width: | Height: | Size: 1.4 KiB |
|
@ -1,11 +1,5 @@
|
|||
<template>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
disabled
|
||||
style="position: absolute; top: 0.25rem; right: 3rem"
|
||||
>
|
||||
<v-btn fab text small disabled>
|
||||
<v-icon>mdi-closed-caption-outline</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
|
|
@ -1,13 +1,6 @@
|
|||
<template>
|
||||
<!-- TODO: change /home to $router.goBack() or $router.go(-1) -->
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
style="position: absolute; top: 0.25rem; right: 0.25rem"
|
||||
to="/home"
|
||||
color="white"
|
||||
>
|
||||
<!-- // TODO: change /home to $router.goBack() or $router.go(-1) -->
|
||||
<v-btn fab text small to="/home" color="white">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
|
|
@ -1,13 +1,8 @@
|
|||
<template>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
color="white"
|
||||
style="position: absolute; bottom: 0.25rem; right: 0.25rem"
|
||||
@click.stop="$emit('fullscreen')"
|
||||
>
|
||||
<v-icon>{{ fullscreen ? "mdi-fullscreen-exit" : "mdi-fullscreen" }}</v-icon>
|
||||
<v-btn fab text small color="white" @click.stop="$emit('fullscreen')">
|
||||
<v-icon size="1.25rem">{{
|
||||
fullscreen ? "mdi-fullscreen-exit" : "mdi-fullscreen"
|
||||
}}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -8,159 +8,245 @@
|
|||
}"
|
||||
class="d-flex flex-column"
|
||||
style="position: relative"
|
||||
:style="{
|
||||
borderRadius: $store.state.tweaks.roundWatch
|
||||
? `${$store.state.tweaks.roundTweak / 3}rem`
|
||||
: '0',
|
||||
}"
|
||||
@click="controls = !controls"
|
||||
:style="{ height: isFullscreen ? '100vh' : 'auto' }"
|
||||
>
|
||||
<video
|
||||
ref="player"
|
||||
autoplay
|
||||
width="100%"
|
||||
:height="isFullscreen ? '100%' : 'auto'"
|
||||
:src="vidSrc"
|
||||
:height="isFullscreen ? '100%' : 'auto'"
|
||||
style="transition: filter 0.15s ease-in-out"
|
||||
:class="controls || seeking ? 'dim' : ''"
|
||||
:style="contain ? 'object-fit: contain;' : 'object-fit: cover;'"
|
||||
:class="controls || seeking || skipping ? 'dim' : ''"
|
||||
:style="{
|
||||
objectFit: contain ? 'contain' : 'cover',
|
||||
borderRadius:
|
||||
$store.state.tweaks.roundWatch && !isFullscreen
|
||||
? `${$store.state.tweaks.roundTweak / 3}rem ${$store.state.tweaks.roundTweak / 3}rem 0rem 0rem !important`
|
||||
: '0',
|
||||
}"
|
||||
poster="https://media.discordapp.net/attachments/970793575153561640/974728851441729556/bam.png"
|
||||
@click="controlsHandler()"
|
||||
@loadedmetadata="checkDimensions()"
|
||||
/>
|
||||
|
||||
<div
|
||||
v-if="isFullscreen && controls"
|
||||
style="
|
||||
position: absolute;
|
||||
width: calc(100% - 12rem);
|
||||
left: 3rem;
|
||||
top: 0.5rem;
|
||||
"
|
||||
>
|
||||
<h4>{{ video.title }}</h4>
|
||||
<div style="color: #aaa; font-size: 0.75rem">{{ video.channelName }}</div>
|
||||
</div>
|
||||
<!-- // NOTE: replace poster URL with "none" -->
|
||||
|
||||
<!-- // TODO: merge the bottom 2 into 1 reusable component -->
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
color="white"
|
||||
:class="skipping == -10 ? '' : 'invisible'"
|
||||
style="
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: opacity 0.15s;
|
||||
border-radius: 0 100vh 100vh 0;
|
||||
text-transform: none;
|
||||
font-size: 0.5rem;
|
||||
"
|
||||
@dblclick.stop="$refs.player.currentTime -= 10"
|
||||
@click="controlsHandler()"
|
||||
@dblclick="skipHandler(-10)"
|
||||
>
|
||||
<v-icon>mdi-rewind</v-icon>
|
||||
<!-- {{ skipping }} seconds -->
|
||||
</v-btn>
|
||||
|
||||
<v-btn
|
||||
text
|
||||
tile
|
||||
color="white"
|
||||
:class="skipping == 10 ? '' : 'invisible'"
|
||||
style="
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
width: 50%;
|
||||
height: 100%;
|
||||
position: absolute;
|
||||
transition: opacity 0.15s;
|
||||
border-radius: 100vh 0 0 100vh;
|
||||
text-transform: none;
|
||||
font-size: 0.5rem;
|
||||
"
|
||||
@dblclick.stop="$refs.player.currentTime += 10"
|
||||
@click="controlsHandler()"
|
||||
@dblclick="skipHandler(10)"
|
||||
>
|
||||
<v-icon>mdi-fast-forward</v-icon>
|
||||
<!-- {{ skipping }} seconds] -->
|
||||
</v-btn>
|
||||
|
||||
<div
|
||||
style="transition: opacity 0.15s ease-in-out"
|
||||
:style="controls ? 'opacity: 1;' : 'opacity: 0; pointer-events: none'"
|
||||
v-if="seeking"
|
||||
class="d-flex justify-center"
|
||||
style="width: 100%; top: 0.5rem; position: absolute; font-size: 0.66rem"
|
||||
>
|
||||
<minimize />
|
||||
<loop />
|
||||
<captions />
|
||||
<close />
|
||||
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
style="
|
||||
position: absolute;
|
||||
top: calc(50% - 1.25rem);
|
||||
left: calc(50% - 10rem);
|
||||
"
|
||||
color="white"
|
||||
@click.stop="$refs.player.currentTime -= 5"
|
||||
>
|
||||
<v-icon size="1rem">mdi-rewind-5</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
style="
|
||||
position: absolute;
|
||||
top: calc(50% - 1.75rem);
|
||||
left: calc(50% - 6.5rem);
|
||||
"
|
||||
color="white"
|
||||
disabled
|
||||
@click.stop=""
|
||||
>
|
||||
<v-icon size="2rem">mdi-skip-previous</v-icon>
|
||||
</v-btn>
|
||||
<playpause
|
||||
v-if="$refs.player"
|
||||
:video="$refs.player"
|
||||
@close="controls = false"
|
||||
/>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
style="
|
||||
position: absolute;
|
||||
top: calc(50% - 1.75rem);
|
||||
left: calc(50% + 3rem);
|
||||
"
|
||||
color="white"
|
||||
disabled
|
||||
@click.stop=""
|
||||
>
|
||||
<v-icon size="2rem">mdi-skip-next</v-icon>
|
||||
</v-btn>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
style="
|
||||
position: absolute;
|
||||
top: calc(50% - 1.25rem);
|
||||
left: calc(50% + 7rem);
|
||||
"
|
||||
color="white"
|
||||
@click.stop="$refs.player.currentTime += 5"
|
||||
>
|
||||
<v-icon size="1rem">mdi-fast-forward-5</v-icon>
|
||||
</v-btn>
|
||||
|
||||
<watchtime v-if="$refs.player" :video="$refs.player" />
|
||||
<!-- // TODO: merge the bottom 2 into 1 reusable component -->
|
||||
<quality v-if="$refs.player" :video="$refs.player" :sources="sources" />
|
||||
<speed v-if="$refs.player" :video="$refs.player" />
|
||||
<fullscreen
|
||||
:fullscreen="isFullscreen"
|
||||
@fullscreen="(controls = $refs.player.paused), handleFullscreenChange()"
|
||||
/>
|
||||
<v-icon small class="pr-2">mdi-rewind</v-icon>
|
||||
Double tap left or right to skip 10 seconds
|
||||
<v-icon small class="pl-2">mdi-fast-forward</v-icon>
|
||||
</div>
|
||||
<!-- NOTE: breaks in fullscreen -->
|
||||
|
||||
<!-- controls container -->
|
||||
<div
|
||||
style="transition: opacity 0.15s ease-in-out"
|
||||
:style="
|
||||
controls && !seeking
|
||||
? 'opacity: 1;'
|
||||
: 'opacity: 0; pointer-events: none'
|
||||
"
|
||||
>
|
||||
<!-- top controls row -->
|
||||
<div
|
||||
style="position: absolute; width: 100%; top: 0.25rem"
|
||||
class="d-flex justify-center px-2"
|
||||
>
|
||||
<minimize />
|
||||
<div v-if="isFullscreen" class="pt-2" @click.self="controlsHandler()">
|
||||
<h4>{{ video.title }}</h4>
|
||||
<div style="color: #aaa; font-size: 0.75rem">
|
||||
{{ video.channelName }}
|
||||
</div>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<captions />
|
||||
<loop
|
||||
v-if="$refs.player"
|
||||
class="mx-2"
|
||||
:loop="$refs.player.loop"
|
||||
@loop="$refs.player.loop = !$refs.player.loop"
|
||||
/>
|
||||
<close />
|
||||
</div>
|
||||
<!-- top controls row end -->
|
||||
|
||||
<!-- center controls row -->
|
||||
<div
|
||||
class="d-flex justify-center align-center"
|
||||
style="
|
||||
transform: translate(-50%, -50%);
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
top: 50%;
|
||||
"
|
||||
>
|
||||
<v-btn fab text color="white" class="px-8" disabled>
|
||||
<v-icon size="2rem" color="white">mdi-skip-previous</v-icon>
|
||||
</v-btn>
|
||||
<playpause
|
||||
v-if="$refs.player"
|
||||
:video="$refs.player"
|
||||
@play="$refs.player.play()"
|
||||
@pause="$refs.player.pause()"
|
||||
/>
|
||||
<v-btn fab text color="white" class="px-8" disabled>
|
||||
<v-icon size="2rem" color="white">mdi-skip-next</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<!-- center controls row end -->
|
||||
|
||||
<!-- time & fullscreen row -->
|
||||
<div
|
||||
:style="isFullscreen ? 'bottom: 4.25rem' : 'bottom: 0.5rem'"
|
||||
class="d-flex justify-between align-center pl-4 pr-2"
|
||||
style="position: absolute; width: 100%"
|
||||
@click.self="controlsHandler()"
|
||||
>
|
||||
<watchtime
|
||||
v-if="$refs.player"
|
||||
:current-time="$refs.player.currentTime"
|
||||
:duration="$refs.player.duration"
|
||||
/>
|
||||
<v-spacer />
|
||||
<fullscreen
|
||||
style="z-index: 2"
|
||||
:fullscreen="isFullscreen"
|
||||
@fullscreen="fullscreenHandler(true)"
|
||||
/>
|
||||
</div>
|
||||
<!-- time & fullscreen row end -->
|
||||
|
||||
<!-- bottom controls row -->
|
||||
<div
|
||||
style="position: absolute; width: 100%; bottom: 0.5rem"
|
||||
class="d-flex justify-between align-center px-2"
|
||||
@click.self="controlsHandler()"
|
||||
>
|
||||
<div v-if="isFullscreen">
|
||||
<v-btn fab text small color="white" class="mr-2" disabled>
|
||||
<v-icon>mdi-thumb-up-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab text small color="white" class="mr-2" disabled>
|
||||
<v-icon>mdi-thumb-down-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab text small color="white" class="mr-2" disabled>
|
||||
<v-icon>mdi-share-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab text small color="white" class="mr-2" disabled>
|
||||
<v-icon>mdi-plus-box-multiple-outline</v-icon>
|
||||
</v-btn>
|
||||
<v-btn fab text small color="white" class="mr-2" disabled>
|
||||
<v-icon>mdi-comment-text-outline</v-icon>
|
||||
</v-btn>
|
||||
</div>
|
||||
<v-spacer />
|
||||
<!-- // TODO: merge the bottom 2 into 1 reusable component -->
|
||||
<quality
|
||||
v-if="$refs.player"
|
||||
:sources="sources"
|
||||
style="z-index: 77777"
|
||||
:current-source="$refs.player"
|
||||
@quality="qualityHandler($event)"
|
||||
/>
|
||||
<speed
|
||||
v-if="$refs.player"
|
||||
class="mx-2"
|
||||
style="z-index: 77777"
|
||||
:current-speed="$refs.player.playbackRate"
|
||||
@speed="$refs.player.playbackRate = $event"
|
||||
/>
|
||||
<v-btn v-if="isFullscreen" fab text small disabled @click.stop="">
|
||||
<v-icon>mdi-cards-outline</v-icon>
|
||||
</v-btn>
|
||||
<!-- placeholder for moving fullscreen button above -->
|
||||
<v-btn v-else fab text small disabled> </v-btn>
|
||||
</div>
|
||||
<!-- bottom controls row -->
|
||||
</div>
|
||||
<!-- controls container end -->
|
||||
|
||||
<progressbar
|
||||
v-if="$refs.player"
|
||||
:current-time="$refs.player.currentTime"
|
||||
:duration="$refs.player.duration"
|
||||
:fullscreen="isFullscreen"
|
||||
:controls="controls"
|
||||
:buffered="buffered"
|
||||
:seeking="seeking"
|
||||
/>
|
||||
|
||||
<sponsorblock
|
||||
v-if="$refs.player && blocks.length > 0"
|
||||
:duration="$refs.player.duration"
|
||||
:fullscreen="isFullscreen"
|
||||
:controls="controls"
|
||||
:seeking="seeking"
|
||||
:blocks="blocks"
|
||||
/>
|
||||
|
||||
<seekbar
|
||||
v-if="$refs.player"
|
||||
v-show="!isFullscreen || controls"
|
||||
:duration="$refs.player.duration"
|
||||
:fullscreen="isFullscreen"
|
||||
:current-time="progress"
|
||||
:video="$refs.player"
|
||||
:sources="sources"
|
||||
:controls="controls"
|
||||
:sources="sources"
|
||||
:seeking="seeking"
|
||||
@seeking="seeking = !seeking"
|
||||
@scrub="$refs.player.currentTime = $event"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -176,8 +262,12 @@ import captions from "~/components/Player/captions.vue";
|
|||
import playpause from "~/components/Player/playpause.vue";
|
||||
import watchtime from "~/components/Player/watchtime.vue";
|
||||
import fullscreen from "~/components/Player/fullscreen.vue";
|
||||
import progressbar from "~/components/Player/progressbar.vue";
|
||||
import sponsorblock from "~/components/Player/sponsorblock.vue";
|
||||
export default {
|
||||
components: {
|
||||
sponsorblock,
|
||||
progressbar,
|
||||
fullscreen,
|
||||
watchtime,
|
||||
playpause,
|
||||
|
@ -202,66 +292,161 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
isFullscreen: false,
|
||||
fullscreenLock: false,
|
||||
verticalFullscreen: false,
|
||||
midRotation: false,
|
||||
controls: false,
|
||||
seeking: false,
|
||||
contain: true,
|
||||
skipping: 0,
|
||||
progress: 0,
|
||||
buffered: 0,
|
||||
watched: 0,
|
||||
blocks: [],
|
||||
vidSrc: "",
|
||||
isVerticalVideo: false,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
console.log("sources", this.sources);
|
||||
this.vidSrc = this.sources[this.sources.length - 1].url;
|
||||
// TODO: detect orientation change and enter fullscreen
|
||||
// TODO: detect video loading state and send this.loading to play button :loading = loading
|
||||
let vid = this.$refs.player;
|
||||
|
||||
|
||||
this.$youtube.getSponsorBlock(this.$route.query.v, (data) => {
|
||||
sponsorBlock = data.segment;
|
||||
this.$youtube.getSponsorBlock(this.video.id, (data) => {
|
||||
console.log("sbreturn", data);
|
||||
if (Array.isArray(data)) {
|
||||
this.blocks = data;
|
||||
}
|
||||
});
|
||||
|
||||
this.$refs.player.ontimeupdate = () => {
|
||||
let vidTime = this.$refs.player.currentTime;
|
||||
for (let i = 0; i < sponsorBlock.length; i++) {
|
||||
if (vidTime > sponsorBlock[i][0] && vidTime < sponsorBlock[0][i]) {
|
||||
this.$refs.player.currentTime = sponsorBlock[i][0];
|
||||
break;
|
||||
}
|
||||
vid.addEventListener("loadeddata", (e) => {
|
||||
// TODO: detect video loading state and send this.loading to play button :loading = loading here
|
||||
// console.log(e);
|
||||
if (vid.readyState >= 3) {
|
||||
vid.addEventListener("timeupdate", () => {
|
||||
if (!this.seeking) this.progress = vid.currentTime; // for seekbar
|
||||
|
||||
// console.log("sb check", this.blocks);
|
||||
// iterate over data.segments array
|
||||
// for sponsorblock
|
||||
if (this.blocks.length > 0)
|
||||
this.blocks.forEach((sponsor) => {
|
||||
let vidTime = vid.currentTime;
|
||||
|
||||
if (
|
||||
vidTime >= sponsor.segment[0] &&
|
||||
vidTime <= sponsor.segment[1]
|
||||
) {
|
||||
console.log("Skipping the sponsor");
|
||||
this.$youtube.showToast("Skipped sponsor");
|
||||
vid.currentTime = sponsor.segment[1] + 1;
|
||||
}
|
||||
});
|
||||
});
|
||||
// TODO: detect video loading state and send this.loading to play button :loading = loading here
|
||||
vid.addEventListener("progress", () => {
|
||||
this.buffered = (vid.buffered.end(0) / vid.duration) * 100;
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
created() {
|
||||
screen.orientation.addEventListener("change", () =>
|
||||
this.fullscreenHandler(false)
|
||||
);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this.isFullscreen) this.exitFullscreen();
|
||||
screen.orientation.removeEventListener("change");
|
||||
},
|
||||
methods: {
|
||||
handleFullscreenChange() {
|
||||
if (document?.fullscreenElement === this.$refs.vidcontainer) {
|
||||
this.exitFullscreen();
|
||||
// TODO: make accumulative onclick after first dblclick (don't set timeout untill stopped clicking)
|
||||
skipHandler(time) {
|
||||
this.skipping = time;
|
||||
setTimeout(() => {
|
||||
this.skipping = false;
|
||||
}, 500);
|
||||
|
||||
this.$refs.player.currentTime += time;
|
||||
},
|
||||
controlsHandler() {
|
||||
if (!this.seeking)
|
||||
this.controls
|
||||
? (clearTimeout(this.controls), (this.controls = false))
|
||||
: setTimeout(() => {
|
||||
if (!this.skipping) {
|
||||
this.controls = setTimeout(() => {
|
||||
if (!this.seeking && !this.$refs.player.paused)
|
||||
this.controls = false;
|
||||
}, 2345);
|
||||
}
|
||||
}, 250);
|
||||
},
|
||||
qualityHandler(q) {
|
||||
console.log(q);
|
||||
let time = this.$refs.player.currentTime;
|
||||
this.$refs.player.src = q;
|
||||
this.$refs.player.currentTime = time;
|
||||
},
|
||||
checkDimensions() {
|
||||
if (this.$refs.player.videoHeight > this.$refs.player.videoWidth) {
|
||||
this.isVerticalVideo = true;
|
||||
} else {
|
||||
this.openFullscreen();
|
||||
this.isVerticalVideo = false;
|
||||
}
|
||||
},
|
||||
exitFullscreen() {
|
||||
const cancellFullScreen =
|
||||
document.exitFullscreen ||
|
||||
document.mozCancelFullScreen ||
|
||||
document.webkitExitFullscreen ||
|
||||
document.msExitFullscreen;
|
||||
cancellFullScreen.call(document);
|
||||
screen.orientation.lock("portrait");
|
||||
screen.orientation.unlock();
|
||||
fullscreenHandler(pressedFullscreenBtn) {
|
||||
// Prevent fullscreen button press from being handled twice
|
||||
// (once by pressing fullscreen button, another by the resulting rotation)
|
||||
if (this.midRotation) {
|
||||
this.midRotation = false;
|
||||
return;
|
||||
}
|
||||
// Toggle fullscreen state
|
||||
if (this.isFullscreen) {
|
||||
this.exitFullscreen(pressedFullscreenBtn);
|
||||
} else {
|
||||
this.enterFullscreen(pressedFullscreenBtn);
|
||||
}
|
||||
},
|
||||
exitFullscreen(unlock) {
|
||||
if (unlock) {
|
||||
if (this.verticalFullscreen) {
|
||||
// Unset vertical fullscreen mode
|
||||
screen.orientation.unlock();
|
||||
this.fullscreenLock = false;
|
||||
this.verticalFullscreen = false;
|
||||
} else {
|
||||
// Unset standard fullscreen mode
|
||||
this.midRotation = true;
|
||||
screen.orientation.lock("portrait");
|
||||
this.fullscreenLock = true;
|
||||
// Locks the rotation to portrait for two seconds,
|
||||
// and will then rotate back to landscape if the
|
||||
// user doesn't rotate first
|
||||
setTimeout(() => {
|
||||
this.fullscreenLock = false;
|
||||
screen.orientation.unlock();
|
||||
}, 2 * 1000);
|
||||
}
|
||||
}
|
||||
this.$vuetube.navigationBar.show();
|
||||
this.$vuetube.statusBar.show();
|
||||
this.isFullscreen = false;
|
||||
},
|
||||
openFullscreen() {
|
||||
const element = this.$refs.vidcontainer;
|
||||
const requestFullScreen =
|
||||
element.requestFullscreen ||
|
||||
element.webkitRequestFullScreen ||
|
||||
element.mozRequestFullScreen ||
|
||||
element.msRequestFullScreen;
|
||||
requestFullScreen.call(element);
|
||||
screen.orientation.lock("landscape");
|
||||
enterFullscreen(force) {
|
||||
if (force) {
|
||||
this.fullscreenLock = true;
|
||||
if (this.isVerticalVideo) {
|
||||
// Vertical fullscreen mode (vertical video only)
|
||||
screen.orientation.lock("portrait");
|
||||
this.verticalFullscreen = true;
|
||||
} else {
|
||||
// Standard fullscreen mode
|
||||
this.midRotation = true;
|
||||
screen.orientation.lock("landscape");
|
||||
}
|
||||
}
|
||||
this.$vuetube.navigationBar.hide();
|
||||
this.$vuetube.statusBar.hide();
|
||||
this.isFullscreen = true;
|
||||
|
@ -275,7 +460,7 @@ export default {
|
|||
|
||||
<style>
|
||||
.dim {
|
||||
filter: brightness(42%);
|
||||
filter: brightness(33%);
|
||||
}
|
||||
.invisible {
|
||||
opacity: 0;
|
||||
|
|
|
@ -1,11 +1,17 @@
|
|||
<template>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
disabled
|
||||
style="position: absolute; top: 0.25rem; right: 6rem"
|
||||
>
|
||||
<v-icon>mdi-sync</v-icon>
|
||||
<v-btn fab text small @click="$emit('loop')">
|
||||
<v-icon color="white">{{ loop ? "mdi-sync-circle" : "mdi-sync" }}</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
loop: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
emits: ["loop"],
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -1,11 +1,5 @@
|
|||
<template>
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
disabled
|
||||
style="position: absolute; top: 0.25rem; left: 0.25rem"
|
||||
>
|
||||
<v-btn fab text small disabled>
|
||||
<v-icon>mdi-chevron-down</v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
|
|
@ -3,11 +3,9 @@
|
|||
fab
|
||||
text
|
||||
large
|
||||
style="position: absolute; top: calc(50% - 2rem); left: calc(50% - 2rem)"
|
||||
color="white"
|
||||
@click.stop="
|
||||
(paused = !video.paused),
|
||||
video.paused ? (video.play(), $emit('close')) : video.pause()
|
||||
@click="
|
||||
(paused = !video.paused), video.paused ? $emit('play') : $emit('pause')
|
||||
"
|
||||
>
|
||||
<v-icon size="3.5rem">
|
||||
|
@ -18,8 +16,13 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
emits: ["close"],
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["play", "pause"],
|
||||
data: () => ({
|
||||
paused: false,
|
||||
}),
|
||||
|
|
51
NUXT/components/Player/progressbar.vue
Normal file
|
@ -0,0 +1,51 @@
|
|||
<template>
|
||||
<v-progress-linear
|
||||
style="
|
||||
position: absolute;
|
||||
background: #ffffff22;
|
||||
transform: translateY(50%);
|
||||
"
|
||||
background-opacity="0.5"
|
||||
background-color="white"
|
||||
:buffer-value="buffered"
|
||||
:value="(currentTime / duration) * 100"
|
||||
:class="!fullscreen || controls ? '' : 'invisible'"
|
||||
color="primary"
|
||||
:height="seeking ? 4 : 2"
|
||||
:style="
|
||||
fullscreen
|
||||
? 'width: calc(100% - 2rem); left: 1rem; bottom: 3.5rem;'
|
||||
: 'width: 100%; left: 0; bottom: 1px;'
|
||||
"
|
||||
/>
|
||||
</template>
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
seeking: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
currentTime: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
controls: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
buffered: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
|
@ -3,45 +3,47 @@
|
|||
<v-bottom-sheet
|
||||
v-model="sheet"
|
||||
:attach="$parent.$refs.vidcontainer"
|
||||
style="z-index: 777777"
|
||||
scrollable
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
color="white"
|
||||
style="position: absolute; bottom: 0.25rem; right: 3rem"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
{{ sources.find((src) => src.url == video.src).qualityLabel }}
|
||||
<v-btn fab text small color="white" v-bind="attrs" v-on="on">
|
||||
{{ sources.find((src) => src.url == currentSource.src).qualityLabel }}
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card
|
||||
v-touch="{
|
||||
down: () => (sheet = false),
|
||||
}"
|
||||
class="background"
|
||||
>
|
||||
<v-subheader>Quality for current video</v-subheader>
|
||||
<v-card-text style="max-height: 50vh" class="pa-0">
|
||||
<v-card class="background">
|
||||
<v-subheader
|
||||
v-touch="{
|
||||
down: () => (sheet = false),
|
||||
}"
|
||||
>
|
||||
Quality for current video
|
||||
<v-spacer />
|
||||
<v-btn fab text small color="white" @click="sheet = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-subheader>
|
||||
<v-divider />
|
||||
<v-card-text
|
||||
style="max-height: 50vh"
|
||||
class="pa-0 d-flex flex-column-reverse"
|
||||
>
|
||||
<v-list-item
|
||||
v-for="src in sources"
|
||||
:key="src"
|
||||
@click="(sheet = false), (video.src = src.url)"
|
||||
@click="(sheet = false), $emit('quality', src.url)"
|
||||
>
|
||||
<v-list-item-avatar>
|
||||
<v-icon
|
||||
:color="
|
||||
video.src === src.url
|
||||
currentSource.src === src.url
|
||||
? 'primary'
|
||||
: $vuetify.theme.dark
|
||||
? 'background lighten-2'
|
||||
: 'background darken-2'
|
||||
"
|
||||
v-text="
|
||||
video.src === src.url
|
||||
currentSource.src === src.url
|
||||
? 'mdi-radiobox-marked'
|
||||
: 'mdi-radiobox-blank'
|
||||
"
|
||||
|
@ -59,7 +61,17 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video", "sources"],
|
||||
props: {
|
||||
currentSource: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
sources: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["quality"],
|
||||
data: () => ({
|
||||
sheet: false,
|
||||
}),
|
||||
|
|
|
@ -7,43 +7,29 @@
|
|||
style="display: none"
|
||||
:src="vidWrs"
|
||||
/>
|
||||
<v-progress-linear
|
||||
query
|
||||
active
|
||||
style="width: 100%; background: #ffffff22"
|
||||
background-opacity="0.5"
|
||||
background-color="white"
|
||||
:buffer-value="buffered"
|
||||
:value="percent"
|
||||
color="primary"
|
||||
height="3"
|
||||
:style="
|
||||
fullscreen
|
||||
? 'width: calc(100% - 2rem); left: 1rem; position: absolute; bottom: 3rem;'
|
||||
: 'width: 100%'
|
||||
"
|
||||
/>
|
||||
<!-- Scrubber -->
|
||||
<v-slider
|
||||
id="scrubber"
|
||||
hide-details
|
||||
height="2"
|
||||
dense
|
||||
color="transparent"
|
||||
thumb-color="primary"
|
||||
track-color="transparent"
|
||||
:class="!controls && !fullscreen && !scrubbing ? 'invisible' : ''"
|
||||
style="position: absolute; z-index: 2"
|
||||
:class="!controls && !fullscreen && !seeking ? 'invisible' : ''"
|
||||
style="position: absolute; z-index: 69420"
|
||||
:style="
|
||||
fullscreen
|
||||
? 'width: calc(100% - 2rem); left: 1rem; bottom: 3rem;'
|
||||
: 'width: calc(100% - 0.8rem); left: 0.4rem; bottom: 0;'
|
||||
? 'width: calc(100% - 2rem); left: 1rem; bottom: 55px;'
|
||||
: 'width: calc(100% - 0.5rem); left: 0.25rem; bottom: 0;'
|
||||
"
|
||||
:thumb-size="0"
|
||||
:max="duration"
|
||||
:value="progress"
|
||||
@start="(scrubbing = true), $emit('seeking')"
|
||||
@end="(scrubbing = false), $emit('seeking')"
|
||||
@change="scrub($event)"
|
||||
@input="scrubbing ? seek($event) : null"
|
||||
:value="currentTime"
|
||||
@start="$emit('seeking')"
|
||||
@end="$emit('seeking')"
|
||||
@change="$emit('scrub', $event)"
|
||||
@input="seeking ? seek($event) : null"
|
||||
>
|
||||
<template #thumb-label="{ value }">
|
||||
<div style="transform: translateY(-50%)">
|
||||
|
@ -70,164 +56,44 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ["sources", "video", "controls", "fullscreen"],
|
||||
|
||||
data() {
|
||||
return {
|
||||
scrubbing: false,
|
||||
percent: 0,
|
||||
progress: 0,
|
||||
buffered: 0,
|
||||
duration: 0,
|
||||
vidSrc: "",
|
||||
vidWrs: "",
|
||||
};
|
||||
props: {
|
||||
video: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
controls: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
sources: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
currentTime: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
seeking: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["scrub", "seeking"],
|
||||
data: () => ({
|
||||
vidWrs: "",
|
||||
}),
|
||||
mounted() {
|
||||
console.log("sources", this.sources);
|
||||
this.vidSrc = this.sources[this.sources.length - 1].url;
|
||||
this.vidWrs = this.sources[1].url;
|
||||
let vid = this.video;
|
||||
vid.addEventListener("loadeddata", (e) => {
|
||||
// console.log("%c loadeddata", "color: #00ff00");
|
||||
console.log(e);
|
||||
//Video should now be loaded but we can add a second check
|
||||
if (vid.readyState >= 3) {
|
||||
vid.ontimeupdate = () => {
|
||||
// console.log("%c timeupdate", "color: #aaaaff");
|
||||
this.duration = vid.duration;
|
||||
if (!this.scrubbing) this.progress = vid.currentTime;
|
||||
this.percent = (vid.currentTime / vid.duration) * 100;
|
||||
};
|
||||
vid.onprogress = () => {
|
||||
// console.log("%c progress", "color: #ff00ff");
|
||||
this.buffered = (vid.buffered.end(0) / vid.duration) * 100;
|
||||
};
|
||||
}
|
||||
});
|
||||
},
|
||||
methods: {
|
||||
// TODO: better scrubbing preview
|
||||
loadVideoFrames() {
|
||||
// Exit loop if desired number of frames have been extracted
|
||||
if (this.frames.length >= frameCount) {
|
||||
this.visibleFrame = 0;
|
||||
|
||||
// Append all canvases to container div
|
||||
this.frames.forEach((frame) => {
|
||||
this.frameContainerElement.appendChild(frame);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// If extraction hasn’t started, set desired time for first frame
|
||||
if (this.frames.length === 0) {
|
||||
this.requestedTime = 0;
|
||||
} else {
|
||||
this.requestedTime = this.requestedTime + this.frameTimestep;
|
||||
}
|
||||
|
||||
// Send seek request to video player for the next frame.
|
||||
this.videoElement.currentTime = this.requestedTime;
|
||||
},
|
||||
extractFrame(videoWidth, videoHeight) {
|
||||
// Create DOM canvas object
|
||||
var canvas = document.createElement("canvas");
|
||||
canvas.className = "video-scrubber-frame";
|
||||
canvas.height = videoHeight;
|
||||
canvas.width = videoWidth;
|
||||
|
||||
// Copy current frame to canvas
|
||||
var context = canvas.getContext("2d");
|
||||
context.drawImage(this.videoElement, 0, 0, videoWidth, videoHeight);
|
||||
this.frames.push(canvas);
|
||||
|
||||
// Load the next frame
|
||||
loadVideoFrames();
|
||||
},
|
||||
prefetch_file(url, fetched_callback, progress_callback, error_callback) {
|
||||
var xhr = new XMLHttpRequest();
|
||||
xhr.open("GET", url, true);
|
||||
xhr.responseType = "blob";
|
||||
|
||||
xhr.addEventListener(
|
||||
"load",
|
||||
function () {
|
||||
if (xhr.status === 200) {
|
||||
var URL = window.URL || window.webkitURL;
|
||||
var blob_url = URL.createObjectURL(xhr.response);
|
||||
fetched_callback(blob_url);
|
||||
} else {
|
||||
error_callback();
|
||||
}
|
||||
},
|
||||
false
|
||||
);
|
||||
|
||||
var prev_pc = 0;
|
||||
xhr.addEventListener("progress", function (event) {
|
||||
if (event.lengthComputable) {
|
||||
var pc = Math.round((event.loaded / event.total) * 100);
|
||||
if (pc != prev_pc) {
|
||||
prev_pc = pc;
|
||||
progress_callback(pc);
|
||||
}
|
||||
}
|
||||
});
|
||||
xhr.send();
|
||||
},
|
||||
async extractFramesFromVideo(videoUrl, fps = 25) {
|
||||
// fully download it first (no buffering):
|
||||
console.log(videoUrl);
|
||||
console.log(fps);
|
||||
let videoBlob = await fetch(videoUrl, {
|
||||
headers: { range: "bytes=0-567139" },
|
||||
}).then((r) => r.blob());
|
||||
console.log(videoBlob);
|
||||
let videoObjectUrl = URL.createObjectURL(videoBlob);
|
||||
let video = document.createElement("video");
|
||||
|
||||
let seekResolve;
|
||||
video.addEventListener("seeked", async function () {
|
||||
if (seekResolve) seekResolve();
|
||||
});
|
||||
|
||||
video.src = videoObjectUrl;
|
||||
|
||||
// workaround chromium metadata bug (https://stackoverflow.com/q/38062864/993683)
|
||||
while (
|
||||
(video.duration === Infinity || isNaN(video.duration)) &&
|
||||
video.readyState < 2
|
||||
) {
|
||||
await new Promise((r) => setTimeout(r, 1000));
|
||||
video.currentTime = 10000000 * Math.random();
|
||||
}
|
||||
let duration = video.duration;
|
||||
|
||||
let canvas = document.createElement("canvas");
|
||||
let context = canvas.getContext("2d");
|
||||
let [w, h] = [video.videoWidth, video.videoHeight];
|
||||
canvas.width = w;
|
||||
canvas.height = h;
|
||||
|
||||
let interval = 1;
|
||||
let currentTime = 0;
|
||||
|
||||
while (currentTime < duration) {
|
||||
video.currentTime = currentTime;
|
||||
await new Promise((r) => (seekResolve = r));
|
||||
|
||||
context.drawImage(video, 0, 0, w, h);
|
||||
let base64ImageData = canvas.toDataURL();
|
||||
console.log(base64ImageData);
|
||||
this.frames.push(base64ImageData);
|
||||
|
||||
currentTime += interval;
|
||||
}
|
||||
console.log("%c frames", "color: #00ff00");
|
||||
console.log(this.frames);
|
||||
},
|
||||
// TODO: scrubbing preview end
|
||||
seek(e) {
|
||||
// console.log(`scrubbing ${e}`);
|
||||
let vid = this.$refs.playerfake;
|
||||
|
@ -243,9 +109,127 @@ export default {
|
|||
this.video.clientHeight / 3
|
||||
);
|
||||
},
|
||||
scrub(e) {
|
||||
this.video.currentTime = e;
|
||||
},
|
||||
// TODO: better scrubbing preview (don't delet ples 🙏)
|
||||
// loadVideoFrames() {
|
||||
// // Exit loop if desired number of frames have been extracted
|
||||
// if (this.frames.length >= frameCount) {
|
||||
// this.visibleFrame = 0;
|
||||
|
||||
// // Append all canvases to container div
|
||||
// this.frames.forEach((frame) => {
|
||||
// this.frameContainerElement.appendChild(frame);
|
||||
// });
|
||||
// return;
|
||||
// }
|
||||
|
||||
// // If extraction hasn’t started, set desired time for first frame
|
||||
// if (this.frames.length === 0) {
|
||||
// this.requestedTime = 0;
|
||||
// } else {
|
||||
// this.requestedTime = this.requestedTime + this.frameTimestep;
|
||||
// }
|
||||
|
||||
// // Send seek request to video player for the next frame.
|
||||
// this.videoElement.currentTime = this.requestedTime;
|
||||
// },
|
||||
// extractFrame(videoWidth, videoHeight) {
|
||||
// // Create DOM canvas object
|
||||
// var canvas = document.createElement("canvas");
|
||||
// canvas.className = "video-scrubber-frame";
|
||||
// canvas.height = videoHeight;
|
||||
// canvas.width = videoWidth;
|
||||
|
||||
// // Copy current frame to canvas
|
||||
// var context = canvas.getContext("2d");
|
||||
// context.drawImage(this.videoElement, 0, 0, videoWidth, videoHeight);
|
||||
// this.frames.push(canvas);
|
||||
|
||||
// // Load the next frame
|
||||
// loadVideoFrames();
|
||||
// },
|
||||
// prefetch_file(url, fetched_callback, progress_callback, error_callback) {
|
||||
// var xhr = new XMLHttpRequest();
|
||||
// xhr.open("GET", url, true);
|
||||
// xhr.responseType = "blob";
|
||||
|
||||
// xhr.addEventListener(
|
||||
// "load",
|
||||
// function () {
|
||||
// if (xhr.status === 200) {
|
||||
// var URL = window.URL || window.webkitURL;
|
||||
// var blob_url = URL.createObjectURL(xhr.response);
|
||||
// fetched_callback(blob_url);
|
||||
// } else {
|
||||
// error_callback();
|
||||
// }
|
||||
// },
|
||||
// false
|
||||
// );
|
||||
|
||||
// var prev_pc = 0;
|
||||
// xhr.addEventListener("progress", function (event) {
|
||||
// if (event.lengthComputable) {
|
||||
// var pc = Math.round((event.loaded / event.total) * 100);
|
||||
// if (pc != prev_pc) {
|
||||
// prev_pc = pc;
|
||||
// progress_callback(pc);
|
||||
// }
|
||||
// }
|
||||
// });
|
||||
// xhr.send();
|
||||
// },
|
||||
// async extractFramesFromVideo(videoUrl, fps = 25) {
|
||||
// // fully download it first (no buffering):
|
||||
// console.log(videoUrl);
|
||||
// console.log(fps);
|
||||
// let videoBlob = await fetch(videoUrl, {
|
||||
// headers: { range: "bytes=0-567139" },
|
||||
// }).then((r) => r.blob());
|
||||
// console.log(videoBlob);
|
||||
// let videoObjectUrl = URL.createObjectURL(videoBlob);
|
||||
// let video = document.createElement("video");
|
||||
|
||||
// let seekResolve;
|
||||
// video.addEventListener("seeked", async function () {
|
||||
// if (seekResolve) seekResolve();
|
||||
// });
|
||||
|
||||
// video.src = videoObjectUrl;
|
||||
|
||||
// // workaround chromium metadata bug (https://stackoverflow.com/q/38062864/993683)
|
||||
// while (
|
||||
// (video.duration === Infinity || isNaN(video.duration)) &&
|
||||
// video.readyState < 2
|
||||
// ) {
|
||||
// await new Promise((r) => setTimeout(r, 1000));
|
||||
// video.currentTime = 10000000 * Math.random();
|
||||
// }
|
||||
// let duration = video.duration;
|
||||
|
||||
// let canvas = document.createElement("canvas");
|
||||
// let context = canvas.getContext("2d");
|
||||
// let [w, h] = [video.videoWidth, video.videoHeight];
|
||||
// canvas.width = w;
|
||||
// canvas.height = h;
|
||||
|
||||
// let interval = 1;
|
||||
// let currentTime = 0;
|
||||
|
||||
// while (currentTime < duration) {
|
||||
// video.currentTime = currentTime;
|
||||
// await new Promise((r) => (seekResolve = r));
|
||||
|
||||
// context.drawImage(video, 0, 0, w, h);
|
||||
// let base64ImageData = canvas.toDataURL();
|
||||
// console.log(base64ImageData);
|
||||
// this.frames.push(base64ImageData);
|
||||
|
||||
// currentTime += interval;
|
||||
// }
|
||||
// console.log("%c frames", "color: #00ff00");
|
||||
// console.log(this.frames);
|
||||
// },
|
||||
// TODO: scrubbing preview end
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -3,47 +3,44 @@
|
|||
<v-bottom-sheet
|
||||
v-model="sheet"
|
||||
:attach="$parent.$refs.vidcontainer"
|
||||
style="z-index: 777777"
|
||||
scrollable
|
||||
>
|
||||
<template #activator="{ on, attrs }">
|
||||
<v-btn
|
||||
fab
|
||||
text
|
||||
small
|
||||
color="white"
|
||||
style="position: absolute; bottom: 0.25rem; right: 6rem"
|
||||
v-bind="attrs"
|
||||
v-on="on"
|
||||
>
|
||||
{{ video.playbackRate }}X
|
||||
<v-btn fab text small color="white" v-bind="attrs" v-on="on">
|
||||
{{ currentSpeed }}X
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card
|
||||
v-touch="{
|
||||
down: () => (sheet = false),
|
||||
}"
|
||||
class="background"
|
||||
>
|
||||
<v-subheader>Playback Speed</v-subheader>
|
||||
<v-card class="background">
|
||||
<v-subheader
|
||||
v-touch="{
|
||||
down: () => (sheet = false),
|
||||
}"
|
||||
>
|
||||
Playback Speed
|
||||
<v-spacer />
|
||||
<v-btn fab text small color="white" @click="sheet = false">
|
||||
<v-icon>mdi-close</v-icon>
|
||||
</v-btn>
|
||||
</v-subheader>
|
||||
<v-divider />
|
||||
<v-card-text style="height: 50vh" class="pa-0">
|
||||
<v-list-item
|
||||
v-for="sped in speeds"
|
||||
:key="sped"
|
||||
@click="(sheet = false), (video.playbackRate = sped)"
|
||||
@click="(sheet = false), $emit('speed', sped)"
|
||||
>
|
||||
<!-- // TODO: save playbackRate to localStorage and manage via store/video/index.js -->
|
||||
<v-list-item-avatar>
|
||||
<v-icon
|
||||
:color="
|
||||
video.playbackRate === sped
|
||||
currentSpeed === sped
|
||||
? 'primary'
|
||||
: $vuetify.theme.dark
|
||||
? 'background lighten-2'
|
||||
: 'background darken-2'
|
||||
"
|
||||
v-text="
|
||||
video.playbackRate === sped ? 'mdi-check' : 'mdi-speedometer'
|
||||
"
|
||||
v-text="currentSpeed === sped ? 'mdi-check' : 'mdi-speedometer'"
|
||||
></v-icon>
|
||||
</v-list-item-avatar>
|
||||
<v-list-item-title>{{ sped }}X</v-list-item-title>
|
||||
|
@ -56,7 +53,13 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
props: {
|
||||
currentSpeed: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
emits: ["speed"],
|
||||
data: () => ({
|
||||
sheet: false,
|
||||
speeds: [0.25, 0.5, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4, 8, 16],
|
||||
|
|
67
NUXT/components/Player/sponsorblock.vue
Normal file
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<div>
|
||||
<v-progress-linear
|
||||
v-for="block in blocks"
|
||||
:key="block.UUID"
|
||||
:buffer-value="(block.segment[1] / duration) * 100"
|
||||
:value="(block.segment[0] / duration) * 100"
|
||||
style="
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
background: transparent;
|
||||
transform: translateY(50%);
|
||||
"
|
||||
:class="!fullscreen || controls ? '' : 'invisible'"
|
||||
background-color="white"
|
||||
background-opacity="1"
|
||||
color="transparent"
|
||||
:height="seeking ? 4 : 2"
|
||||
:style="
|
||||
fullscreen
|
||||
? 'width: calc(100% - 2rem); left: 1rem; bottom: 3.5rem;'
|
||||
: 'width: 100%; left: 0; bottom: 1px;'
|
||||
"
|
||||
/>
|
||||
<!-- // TODO:background-color="colors[block.category]" -->
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
blocks: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
seeking: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
fullscreen: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
controls: {
|
||||
type: Boolean,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
data: () => ({
|
||||
colors: {
|
||||
sponsor: "green",
|
||||
selfpromo: "yellow",
|
||||
exclusive_access: "teal",
|
||||
interaction: "fuchsia",
|
||||
poi_highlight: "deeppink",
|
||||
intro: "lightblue",
|
||||
outro: "blue",
|
||||
music_offtopic: "orange",
|
||||
filter: "purple",
|
||||
},
|
||||
}),
|
||||
};
|
||||
</script>
|
|
@ -1,32 +1,21 @@
|
|||
<template>
|
||||
<div
|
||||
style="
|
||||
color: #fff;
|
||||
left: 1rem;
|
||||
bottom: 1rem;
|
||||
font-size: 0.75rem;
|
||||
position: absolute;
|
||||
"
|
||||
>
|
||||
{{ watched }}
|
||||
<span style="color: #aaa"> / {{ duration }} </span>
|
||||
<div style="color: #fff; font-size: 0.75rem">
|
||||
{{ $vuetube.humanTime(currentTime) }}
|
||||
<span style="color: #aaa"> / {{ $vuetube.humanTime(duration) }} </span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: ["video"],
|
||||
data() {
|
||||
return {
|
||||
watched: 0,
|
||||
duration: 0,
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.video.addEventListener("timeupdate", () => {
|
||||
this.duration = this.$vuetube.humanTime(this.video.duration);
|
||||
this.watched = this.$vuetube.humanTime(this.video.currentTime);
|
||||
});
|
||||
props: {
|
||||
duration: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
currentTime: {
|
||||
type: Number,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
|
@ -8,6 +8,7 @@
|
|||
<style scoped>
|
||||
.description {
|
||||
white-space: pre-line;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
|
|
@ -68,7 +68,7 @@
|
|||
text
|
||||
tile
|
||||
dense
|
||||
class="searchButton text-left text-none"
|
||||
class="searchButton text-left text-none no-spacing"
|
||||
@click="youtubeSearch(item)"
|
||||
>
|
||||
<v-icon class="mr-5">mdi-magnify</v-icon>
|
||||
|
@ -326,6 +326,10 @@ div {
|
|||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.no-spacing {
|
||||
letter-spacing: 0px !important;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style scoped>
|
||||
|
|
|
@ -53,6 +53,8 @@ export default {
|
|||
buildModules: ["@nuxtjs/vuetify"],
|
||||
modules: [],
|
||||
|
||||
ssr: false,
|
||||
|
||||
vuetify: {
|
||||
customVariables: ["~/assets/variables.scss"],
|
||||
treeShake: true,
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
<center style="margin: 2em;">
|
||||
<h1>Registry Editor</h1>
|
||||
<v-alert text outlined type="warning">
|
||||
EDITING ENTRIES MAY CAUSE YOUR APP TO BREAK!
|
||||
CHANGING ENTRIES MAY CAUSE YOUR APP TO BREAK!
|
||||
</v-alert>
|
||||
</center>
|
||||
|
||||
|
@ -36,7 +36,6 @@
|
|||
<v-card class="rounded-lg" :class="$vuetify.theme.dark ? 'background lighten-1' : 'background darken-1'">
|
||||
<v-card-title class="text-h5">Confirm Delete</v-card-title>
|
||||
<v-card-text>Are you sure that you want to delete <span class="highlight" v-text="selectedKey" />?</v-card-text>
|
||||
<v-alert text outlined type="warning" style="margin: -0.5em 2em 1em 2em;">Deleting random keys may cause the app to break!</v-alert>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
@ -59,7 +58,6 @@
|
|||
/>
|
||||
|
||||
</v-card-text>
|
||||
<v-alert text outlined type="warning" style="margin: -2em 2em 1em 2em;">Editing random keys may cause the app to break!</v-alert>
|
||||
<v-divider />
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
>
|
||||
<v-btn
|
||||
text
|
||||
class="entry text-left text-capitalize"
|
||||
class="entry text-left text-capitalize no-spacing"
|
||||
:to="item.to"
|
||||
:disabled="item.disabled"
|
||||
:style="{
|
||||
|
@ -49,7 +49,7 @@
|
|||
<v-btn
|
||||
v-if="devmode"
|
||||
text
|
||||
class="entry text-left text-capitalize"
|
||||
class="entry text-left text-capitalize no-spacing"
|
||||
to="/mods/developer"
|
||||
:style="{
|
||||
borderRadius: `${$store.state.tweaks.roundTweak / 2}rem`,
|
||||
|
|
|
@ -19,7 +19,3 @@
|
|||
</p>
|
||||
</center>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {};
|
||||
</script>
|
||||
|
|
|
@ -21,7 +21,7 @@
|
|||
<v-card v-if="loaded" class="background rounded-0" flat>
|
||||
<div
|
||||
v-ripple
|
||||
class="d-flex justify-space-between align-start px-3 pt-3"
|
||||
class="d-flex justify-space-between align-start px-3 pt-4"
|
||||
@click="showMore = !showMore"
|
||||
>
|
||||
<div class="d-flex flex-column">
|
||||
|
@ -163,7 +163,7 @@
|
|||
|
||||
<!-- Description -->
|
||||
<div v-if="showMore">
|
||||
<div class="scroll-y ma-4">
|
||||
<div class="scroll-y ma-4 pt-1">
|
||||
<slim-video-description-renderer
|
||||
:render="video.renderedData.description"
|
||||
/>
|
||||
|
@ -227,6 +227,7 @@
|
|||
hide-overlay
|
||||
persistent
|
||||
no-click-animation
|
||||
style="z-index: 2 !important"
|
||||
attach="#content-container"
|
||||
>
|
||||
<mainCommentRenderer
|
||||
|
@ -366,6 +367,12 @@ export default {
|
|||
dialogTitle: "Share video",
|
||||
});
|
||||
},
|
||||
vlc() {
|
||||
this.$youtube.showToast("Opening in VLC");
|
||||
|
||||
// redirect to vlc://url
|
||||
window.location.href = "vlc://" + this.video.availableResolutions[this.video.availableResolutions.length-1].url;
|
||||
},
|
||||
sendWatchTime() {
|
||||
const player = this.$refs.player.getPlayer();
|
||||
const rt = Math.floor(Date.now() / 1000) - this.startTime;
|
||||
|
@ -434,6 +441,12 @@ export default {
|
|||
actionName: "enqueue",
|
||||
disabled: true,
|
||||
},
|
||||
{
|
||||
name: "Open in VLC",
|
||||
icon: "mdi-traffic-cone",
|
||||
actionName: "vlc",
|
||||
disabled: false,
|
||||
},
|
||||
// {
|
||||
// name: "Quality",
|
||||
// icon: "mdi-high-definition",
|
||||
|
|
|
@ -141,31 +141,11 @@ const module = {
|
|||
Math.floor((((seconds % 31536000) % 86400) % 3600) % 60), //Seconds
|
||||
];
|
||||
levels = levels.filter((level) => level !== null);
|
||||
|
||||
for (let i = 1; i < levels.length; i++) {
|
||||
levels[i] = levels[i].toString().padStart(2, "0");
|
||||
}
|
||||
|
||||
// join the array into a string with : as a separator
|
||||
const returntext = levels.join(":");
|
||||
|
||||
console.log("Human Time:", returntext);
|
||||
while (returntext.startsWith(":00")) {
|
||||
returntext = returntext.substring(3);
|
||||
} // Remove Prepending 0s (eg. 00:00:00:01:00)
|
||||
if (returntext.startsWith(":0")) {
|
||||
returntext = returntext.substring(2);
|
||||
} else {
|
||||
returntext = returntext.substring(1);
|
||||
} // Prevent Time Starting With 0 (eg. 01:00)
|
||||
|
||||
if (!returntext.includes(":")) {
|
||||
if (returntext.length == 1) {
|
||||
returntext = "0" + returntext; // Make tens digit in seconds always visible (eg. 0:09)
|
||||
}
|
||||
returntext = "0:" + returntext; // Make minutes visible as 0 when sub 60 seconds (eg. 0:51)
|
||||
}
|
||||
|
||||
let returntext = levels.join(":");
|
||||
return returntext;
|
||||
},
|
||||
//--- End Convert Time To Human Readable String ---//
|
||||
|
|
|
@ -78,6 +78,9 @@ const searchModule = {
|
|||
logger("codeRun", err, true);
|
||||
callback(err);
|
||||
});
|
||||
},
|
||||
showToast(text) {
|
||||
Toast.show({ text: text });
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
@ -1,17 +0,0 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="deploymentTargetDropDown">
|
||||
<targetSelectedWithDropDown>
|
||||
<Target>
|
||||
<type value="QUICK_BOOT_TARGET" />
|
||||
<deviceKey>
|
||||
<Key>
|
||||
<type value="VIRTUAL_DEVICE_PATH" />
|
||||
<value value="$USER_HOME$/.android/avd/Pixel_3a_API_31_arm64-v8a.avd" />
|
||||
</Key>
|
||||
</deviceKey>
|
||||
</Target>
|
||||
</targetSelectedWithDropDown>
|
||||
<timeTargetWasSelectedWithDropDown value="2022-05-14T02:50:17.689302Z" />
|
||||
</component>
|
||||
</project>
|
Before Width: | Height: | Size: 159 KiB After Width: | Height: | Size: 132 KiB |
Before Width: | Height: | Size: 13 KiB After Width: | Height: | Size: 9.6 KiB |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 1.9 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 4 KiB |
Before Width: | Height: | Size: 5 KiB After Width: | Height: | Size: 4.9 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 18 KiB |
Before Width: | Height: | Size: 7.5 KiB After Width: | Height: | Size: 7.4 KiB |
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 9.5 KiB |
Before Width: | Height: | Size: 63 KiB After Width: | Height: | Size: 46 KiB |
Before Width: | Height: | Size: 4.4 KiB After Width: | Height: | Size: 4.2 KiB |
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
Before Width: | Height: | Size: 17 KiB After Width: | Height: | Size: 16 KiB |
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 85 KiB |
Before Width: | Height: | Size: 6.9 KiB After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 24 KiB After Width: | Height: | Size: 22 KiB |
|
@ -2,5 +2,9 @@
|
|||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
<feature name="CDVOrientation">
|
||||
<param name="android-package" value="cordova.plugins.screenorientation.CDVOrientation"/>
|
||||
</feature>
|
||||
|
||||
|
||||
</widget>
|
|
@ -0,0 +1,98 @@
|
|||
/*
|
||||
*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing,
|
||||
* software distributed under the License is distributed on an
|
||||
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||
* KIND, either express or implied. See the License for the
|
||||
* specific language governing permissions and limitations
|
||||
* under the License.
|
||||
*
|
||||
*/
|
||||
|
||||
package cordova.plugins.screenorientation;
|
||||
|
||||
import org.apache.cordova.CallbackContext;
|
||||
import org.apache.cordova.CordovaPlugin;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONException;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.util.Log;
|
||||
|
||||
public class CDVOrientation extends CordovaPlugin {
|
||||
|
||||
private static final String TAG = "YoikScreenOrientation";
|
||||
|
||||
/**
|
||||
* Screen Orientation Constants
|
||||
*/
|
||||
|
||||
private static final String ANY = "any";
|
||||
private static final String PORTRAIT_PRIMARY = "portrait-primary";
|
||||
private static final String PORTRAIT_SECONDARY = "portrait-secondary";
|
||||
private static final String LANDSCAPE_PRIMARY = "landscape-primary";
|
||||
private static final String LANDSCAPE_SECONDARY = "landscape-secondary";
|
||||
private static final String PORTRAIT = "portrait";
|
||||
private static final String LANDSCAPE = "landscape";
|
||||
|
||||
@Override
|
||||
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) {
|
||||
|
||||
Log.d(TAG, "execute action: " + action);
|
||||
|
||||
// Route the Action
|
||||
if (action.equals("screenOrientation")) {
|
||||
return routeScreenOrientation(args, callbackContext);
|
||||
}
|
||||
|
||||
// Action not found
|
||||
callbackContext.error("action not recognised");
|
||||
return false;
|
||||
}
|
||||
|
||||
private boolean routeScreenOrientation(JSONArray args, CallbackContext callbackContext) {
|
||||
|
||||
String action = args.optString(0);
|
||||
|
||||
|
||||
|
||||
String orientation = args.optString(1);
|
||||
|
||||
Log.d(TAG, "Requested ScreenOrientation: " + orientation);
|
||||
|
||||
Activity activity = cordova.getActivity();
|
||||
|
||||
if (orientation.equals(ANY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
} else if (orientation.equals(LANDSCAPE_PRIMARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT_PRIMARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
|
||||
} else if (orientation.equals(LANDSCAPE)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
} else if (orientation.equals(LANDSCAPE_SECONDARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE);
|
||||
} else if (orientation.equals(PORTRAIT_SECONDARY)) {
|
||||
activity.setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT);
|
||||
}
|
||||
|
||||
callbackContext.success();
|
||||
return true;
|
||||
|
||||
|
||||
}
|
||||
}
|
|
@ -352,15 +352,14 @@
|
|||
DEVELOPMENT_TEAM = VRCJ7YWR89;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.Frontesque.vuetube;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG;
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Debug;
|
||||
};
|
||||
|
@ -374,14 +373,13 @@
|
|||
DEVELOPMENT_TEAM = VRCJ7YWR89;
|
||||
INFOPLIST_FILE = App/Info.plist;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 12.2;
|
||||
"IPHONEOS_DEPLOYMENT_TARGET[sdk=macosx*]" = 14.2;
|
||||
LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks";
|
||||
PRODUCT_BUNDLE_IDENTIFIER = com.Frontesque.vuetube;
|
||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||
SUPPORTS_MACCATALYST = YES;
|
||||
SUPPORTS_MACCATALYST = NO;
|
||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
|
||||
SWIFT_VERSION = 5.0;
|
||||
TARGETED_DEVICE_FAMILY = "1,2,6";
|
||||
TARGETED_DEVICE_FAMILY = "1,2";
|
||||
};
|
||||
name = Release;
|
||||
};
|
||||
|
|
|
@ -2,5 +2,9 @@
|
|||
<widget version="1.0.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
|
||||
<access origin="*" />
|
||||
|
||||
<feature name="CDVOrientation">
|
||||
<param name="ios-package" value="CDVOrientation"/>
|
||||
</feature>
|
||||
|
||||
|
||||
</widget>
|
|
@ -9,17 +9,18 @@ install! 'cocoapods', :disable_input_output_paths => true
|
|||
def capacitor_pods
|
||||
pod 'Capacitor', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCordova', :path => '../../node_modules/@capacitor/ios'
|
||||
pod 'CapacitorCommunityHttp', :path => '..\..\node_modules\@capacitor-community\http'
|
||||
pod 'CapacitorApp', :path => '..\..\node_modules\@capacitor\app'
|
||||
pod 'CapacitorBrowser', :path => '..\..\node_modules\@capacitor\browser'
|
||||
pod 'CapacitorDevice', :path => '..\..\node_modules\@capacitor\device'
|
||||
pod 'CapacitorFilesystem', :path => '..\..\node_modules\@capacitor\filesystem'
|
||||
pod 'CapacitorHaptics', :path => '..\..\node_modules\@capacitor\haptics'
|
||||
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'
|
||||
pod 'CapacitorCommunityHttp', :path => '../../node_modules/@capacitor-community/http'
|
||||
pod 'CapacitorApp', :path => '../../node_modules/@capacitor/app'
|
||||
pod 'CapacitorBrowser', :path => '../../node_modules/@capacitor/browser'
|
||||
pod 'CapacitorDevice', :path => '../../node_modules/@capacitor/device'
|
||||
pod 'CapacitorFilesystem', :path => '../../node_modules/@capacitor/filesystem'
|
||||
pod 'CapacitorHaptics', :path => '../../node_modules/@capacitor/haptics'
|
||||
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'
|
||||
pod 'CordovaPlugins', :path => '../capacitor-cordova-ios-plugins'
|
||||
end
|
||||
|
||||
target 'App' do
|
||||
|
|
38
readme.md
|
@ -28,22 +28,48 @@ Pronounced View Tube (<code>/ˈvjuːˌtjuːb/</code>)
|
|||
<a href="https://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a>
|
||||
</p>
|
||||
|
||||
Read this in other languages: [Español](readmeES.md)
|
||||
|
||||
## Features
|
||||
|
||||
<img src="/resources/vuetube.png" alt="VueTube icon" width="300"/>
|
||||
|
||||
- 🎨 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 by default
|
||||
- 📺 Custom video player
|
||||
- 👎 Return YouTube Dislike
|
||||
|
||||
## Install
|
||||
|
||||
<img src="/resources/install.png" alt="VueTube icon" width="300"/>
|
||||
|
||||
To install please visit www.vuetube.app/install
|
||||
|
||||
<details>
|
||||
<summary>Or click here to display all versions avaiable</summary>
|
||||
<br />
|
||||
|
||||
### Android
|
||||
| <a href=https://nightly.link/VueTubeApp/VueTube/workflows/ci/main/android.zip><img id="im" width="200" src=/resources/getunstable.png></a> | <a href=https://cdn.discordapp.com/attachments/946910031562027029/972164599816273930/VueTube-Canary-May-6-2022.apk><img id="im" width="200" src=/resources/getcanary.png></a> | <a href=https://vuetube.app/install><img id="im" width="200" src=/resources/getstable.png></a> |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| A lot of bugs, but early access to features | Less bugs than unstable, slightly more features than stable | Not available until the app becomes more developed |
|
||||
|
||||
|
||||
### iOS
|
||||
| <a href=https://nightly.link/VueTubeApp/VueTube/workflows/ci/main/iOS.zip><img id="im" width="200" src=/resources/getunstable.png></a> | <a href=https://cdn.discordapp.com/attachments/949908267855921163/972164558930198528/VueTube-Canary-May-6-2022.ipa><img id="im" width="200" src=/resources/getcanary.png></a> | <a href=https://vuetube.app/install><img id="im" width="200" src=/resources/getstable.png></a> |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| A lot of bugs, but early access to features | Less bugs than unstable, slightly more features than stable | Not available until the app becomes more developed |
|
||||
|
||||
</details>
|
||||
|
||||
## Plans
|
||||
|
||||
<img src="/resources/plans.png" alt="VueTube icon" width="300"/>
|
||||
|
||||
- 🔍 Advanced Search
|
||||
- 🗞️ Locally store watch history
|
||||
- 📺 A custom video player
|
||||
- ✂️ Shorts
|
||||
- 🧑 Google account sign in
|
||||
- 🖼️ Picture in picture mode
|
||||
|
@ -53,6 +79,16 @@ To install please visit www.vuetube.app/install
|
|||
|
||||
View on our website: www.vuetube.app/info/screenshots
|
||||
|
||||
<details>
|
||||
<summary> Or click here to display screenshots </summary>
|
||||
<br />
|
||||
|
||||
<img src="https://vuetube.app/wtch.png" width="400">
|
||||
<img src="https://vuetube.app/stng.png" width="400">
|
||||
<img src="https://vuetube.app/srch.png" width="400">
|
||||
|
||||
</details>
|
||||
|
||||
### 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/810799100940255260/973719873467342908/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
|
||||
|
|
116
readmeES.md
Normal file
|
@ -0,0 +1,116 @@
|
|||
<p align="center">
|
||||
<a href="https://vuetube.app/">
|
||||
<img src="https://cdn.discordapp.com/attachments/751596360108605500/980418672331988992/VueTube_Dark.svg" alt="VueTube icon" width="500"/>
|
||||
</a>
|
||||
</br>
|
||||
<sub>Logo por <a href="https://github.com/afnzmn">@afnzmn</a></sub>
|
||||
</br>
|
||||
</br>
|
||||
<strong>Un cliente sencillo de streaming de vídeo FOSS diseñado para recrear TODAS las características de sus respectivas aplicaciones (y más) </strong>
|
||||
</br>
|
||||
Se pronuncia Viu Tuf (<code>/ˈvjuːˌtjuːb/</code>)
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/VueTubeApp/VueTube/commits/main"><img src="https://img.shields.io/github/commit-activity/m/VueTubeApp/VueTube?label=Commits" alt="Commits"></img></a>
|
||||
<a href="https://github.com/VueTubeApp/VueTube/issues" alt="Issues"><img src="https://img.shields.io/github/issues/VueTubeApp/VueTube"></img></a>
|
||||
<a><img src="https://img.shields.io/github/languages/count/VueTubeApp/VueTube" alt="Languages"></img></a>
|
||||
<a href="https://github.com/VueTubeApp/VueTube/blob/main/LICENSE" alt="License"><img src="https://img.shields.io/github/license/VueTubeApp/VueTube"></img></a>
|
||||
<a><img src="https://img.shields.io/github/stars/VueTubeApp/VueTube" alt="Stars"></img></a>
|
||||
<a><img src="https://img.shields.io/snyk/vulnerabilities/github/VueTubeApp/VueTube" alt="Vulnerabilities"></img></a>
|
||||
<a><img src="https://img.shields.io/librariesio/github/VueTubeApp/VueTube" alt="Dependencies"></img></a>
|
||||
<a><img src="https://img.shields.io/tokei/lines/github/VueTubeApp/VueTube" alt="Lines"></img></a>
|
||||
<a href="https://github.com/VueTubeApp/VueTube/actions/workflows/ci.yml" alt="CI"><img src="https://github.com/VueTubeApp/VueTube/actions/workflows/ci.yml/badge.svg"></img></a>
|
||||
<a href="https://vuetube.app" alt="Website"><img src="https://img.shields.io/website?down_message=offline&up_message=online&url=https%3A%2F%2Fvuetube.app"></img></a>
|
||||
<a href="https://reddit.com/r/vuetube" alt="Reddit"><img src="https://img.shields.io/reddit/subreddit-subscribers/vuetube?label=r%2FVuetube&logo=reddit&logoColor=white"></img></a>
|
||||
<a href="https://t.me/VueTube" alt="Telegram"><img src="https://img.shields.io/endpoint?color=neon&style=flat&url=https%3A%2F%2Ftg.sumanjay.workers.dev%2Fvuetube"></img></a>
|
||||
<a href="https://discord.gg/7P8KJrdd5W" alt="Discord"><img src="https://img.shields.io/discord/946587366242533377?label=Discord&style=flat&logo=discord&logoColor=white"></img></a>
|
||||
<a href="https://twitter.com/VueTubeApp" alt="Twitter"><img src="https://img.shields.io/twitter/follow/VueTubeApp?label=Follow&style=flat&logo=twitter"></img></a>
|
||||
</p>
|
||||
|
||||
Leer en otros idiomas: [English](readme.md)
|
||||
|
||||
## Características
|
||||
|
||||
<img src="/resources/vuetubeES.PNG" alt="VueTube icon" width="300"/>
|
||||
|
||||
- 🎨 Temas: Claro, Oscuro, OLED, Todos los colores del arcoíris
|
||||
- 🖌️ Interfaz personalizable: ¡Puedes personalizar completamente el color principal, y otras partes de la interfaz para eliminar características que no usas!
|
||||
- ⬆️ Actualizaciones automáticas: ¡Recibe una notificación cuando haya una actualización disponible y baja de versión si no te gusta!
|
||||
- 👁️ Protección contra el rastreo: No se envían datos desde tu dispositivo por defecto
|
||||
- 📺 Reproductor de vídeo personalizado
|
||||
- 👎 Return YouTube Dislike
|
||||
|
||||
## Instalar
|
||||
|
||||
<img src="/resources/installES.PNG" alt="VueTube icon" width="300"/>
|
||||
|
||||
Para instalar, por favor, visita www.vuetube.app/install
|
||||
|
||||
<details>
|
||||
<summary>O haz clic aquí para mostrar todas las versiones disponibles</summary>
|
||||
<br />
|
||||
|
||||
| <a href=https://nightly.link/VueTubeApp/VueTube/workflows/ci/main/android.zip><img id="im" width="200" src=/resources/getunstable.png></a> | <a href=https://cdn.discordapp.com/attachments/946910031562027029/972164599816273930/VueTube-Canary-May-6-2022.apk><img id="im" width="200" src=/resources/getcanary.png></a> | <a href=https://vuetube.app/install><img id="im" width="200" src=/resources/getstable.png></a> |
|
||||
| ------------- | ------------- | ------------- |
|
||||
| Un montón de bugs, pero acceso anticipado a funciones | Menos bugs que la inestable, aún así más funciones que la estable | No disponible hasta que la app este más desarrollada |
|
||||
|
||||
</details>
|
||||
|
||||
## Planes
|
||||
|
||||
<img src="/resources/plansES.PNG" alt="VueTube icon" width="300"/>
|
||||
|
||||
- 🔍 Búsqueda avanzada
|
||||
- 🗞️ Historial de búsqueda local
|
||||
- ✂️ Shorts (Cortos)
|
||||
- 🧑 Inicio de sesión con tu cuenta de Google
|
||||
- 🖼️ Modo Imagen en imagen
|
||||
- ¡y más!
|
||||
|
||||
## Capturas de pantalla
|
||||
|
||||
Echalas un vistazo en nuestro sitio web: www.vuetube.app/info/screenshots
|
||||
|
||||
<details>
|
||||
<summary> O haz clic aquí para mostrar las capturas </summary>
|
||||
<br />
|
||||
|
||||
<img src="https://vuetube.app/wtch.png" width="400">
|
||||
<img src="https://vuetube.app/stng.png" width="400">
|
||||
<img src="https://vuetube.app/srch.png" width="400">
|
||||
|
||||
</details>
|
||||
|
||||
### Tecnologías usadas
|
||||
|
||||
<a href="https://capacitorjs.com/solution/vue"><img src="https://cdn.discordapp.com/attachments/953538236716814356/955694368742834176/Capacitator-Dark.svg" height=40/></a> <a href="https://vuetifyjs.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/973719873467342908/Vuetify-Dark.svg" height=40/></a> <a href="https://nuxtjs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/NuxtJS-Dark.svg" height=40/></a> <a href="https://vuejs.org/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/VueJS-Dark.svg" height=40/></a> <a href="https://javascript.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/JavaScript.svg" height=40/></a> <a href="https://java.com/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Java-Dark.svg" height=40/></a> <a href="https://gradle.com/"><img src="https://cdn.discordapp.com/attachments/810799100940255260/955691550560636958/Gradle.svg" height=40/></a> <a href="https://developer.apple.com/swift/"><img src="https://github.com/tandpfun/skill-icons/raw/main/icons/Swift.svg" height=40/></a>
|
||||
|
||||
### ¿Porque estoy haciendo esto?
|
||||
|
||||
Bueno, esto ha estado en el servidor de Discord de Return YouTube Dislike durante bastante tiempo, ¡así que pensé que probablemente debería lanzarlo!
|
||||
|
||||
### ¿Quieres contribuir?
|
||||
|
||||
Por favor, lee en nuestro sitio web cómo hacerlo: www.vuetube.app/contributing
|
||||
|
||||
## Colaboradores
|
||||
|
||||
<a href="https://github.com/VueTubeApp/VueTube/graphs/contributors">
|
||||
<img src="https://contrib.rocks/image?repo=VueTubeApp/VueTube" />
|
||||
</a>
|
||||
|
||||
<sub>Hecho con [contrib.rocks](https://contrib.rocks). </sub>
|
||||
|
||||
## Agradecimientos
|
||||
|
||||
- Emojis por el [equipo de Twemoji](https://twemoji.twitter.com/), Con licencia [CC-BY 4.0](https://creativecommons.org/licenses/by/4.0/)
|
||||
- Logo de VueTube por [@afnzmn](https://github.com/afnzmn)
|
||||
|
||||
## Aviso legal
|
||||
|
||||
El proyecto VueTube y sus contenidos no están afiliados, financiados, autorizados, respaldados o asociados de ninguna manera con YouTube, Google LLC o cualquiera de sus filiales y subsidiarias. El sitio web oficial de YouTube se encuentra en [www.youtube.com](https://www.youtube.com).
|
||||
|
||||
Cualquier marca comercial, de servicio, nombre comercial u otros derechos de propiedad intelectual utilizados en el proyecto VueTube son propiedad de sus respectivos dueños.
|
||||
|
||||
En caso de conflicto entre las traducciones del aviso legal, tiene preferencia la versión en inglés.
|
Before Width: | Height: | Size: 949 B After Width: | Height: | Size: 949 B |
1
resources/Stable.svg
Normal file
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" fill="none" viewBox="0 0 512 512"><rect width="512" height="512" fill="url(#paint0_linear_6_11)"/><path fill="url(#paint1_linear_6_11)" fill-rule="evenodd" d="M327.768 269.391C338.454 263.236 338.454 247.814 327.766 241.66L327.761 241.658L291.527 220.81L331.399 202.218L345.714 210.453L345.736 210.466L386.392 233.857C403.102 243.471 403.102 267.582 386.392 277.196L345.743 300.582L345.714 300.599L328.778 310.343L288.905 291.75L327.761 269.395L327.768 269.391ZM206 290.605V311.783V311.79C206.005 324.092 219.314 331.788 229.979 325.652L252.89 312.471L292.763 331.063L247.932 356.856L247.903 356.873L207.467 380.137C190.801 389.725 170 377.695 170 358.467L170 311.783L170 273.818L206 290.605ZM295.384 181.497L255.512 200.09L229.979 185.4L229.964 185.392C219.3 179.27 206 186.968 206 199.269V223.177L170 239.965L170 199.269L170 152.585C170 133.357 190.801 121.327 207.467 130.915L247.917 154.187L247.932 154.196L295.384 181.497Z" clip-rule="evenodd"/><defs><linearGradient id="paint0_linear_6_11" x1="0" x2="512" y1="0" y2="512" gradientUnits="userSpaceOnUse"><stop stop-color="#1B3245"/><stop offset="1" stop-color="#0C2028"/></linearGradient><linearGradient id="paint1_linear_6_11" x1="170" x2="431" y1="128" y2="389" gradientUnits="userSpaceOnUse"><stop stop-color="#00E1C3"/><stop offset="1" stop-color="#00D1D5"/></linearGradient></defs></svg>
|
After Width: | Height: | Size: 1.4 KiB |
BIN
resources/getcanary.png
Normal file
After Width: | Height: | Size: 43 KiB |
BIN
resources/getstable.png
Normal file
After Width: | Height: | Size: 45 KiB |
BIN
resources/getunstable.png
Normal file
After Width: | Height: | Size: 40 KiB |
BIN
resources/install.png
Normal file
After Width: | Height: | Size: 39 KiB |
BIN
resources/installES.PNG
Normal file
After Width: | Height: | Size: 80 KiB |
BIN
resources/plans.png
Normal file
After Width: | Height: | Size: 36 KiB |
BIN
resources/plansES.PNG
Normal file
After Width: | Height: | Size: 81 KiB |
BIN
resources/vuetube.png
Normal file
After Width: | Height: | Size: 60 KiB |
BIN
resources/vuetubeES.PNG
Normal file
After Width: | Height: | Size: 109 KiB |