Merge remote-tracking branch 'origin/master' into toast

This commit is contained in:
Kavin
2023-03-17 04:13:04 +00:00
37 changed files with 1176 additions and 729 deletions

View File

@@ -1,43 +1,64 @@
<template>
<ErrorHandler v-if="channel && channel.error" :message="channel.message" :error="channel.error" />
<div v-if="channel" v-show="!channel.error">
<div class="flex justify-center place-items-center">
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
<h1 v-text="channel.name" />
<font-awesome-icon class="ml-1.5 !text-3xl" v-if="channel.verified" icon="check" />
<LoadingIndicatorPage :show-content="channel != null && !channel.error">
<img
v-if="channel.bannerUrl"
:src="channel.bannerUrl"
class="w-full py-1.5 h-30 md:h-50 object-cover"
loading="lazy"
/>
<div class="flex flex-col md:flex-row justify-between items-center">
<div class="flex place-items-center">
<img height="48" width="48" class="rounded-full m-1" :src="channel.avatarUrl" />
<div class="flex gap-1 items-center">
<h1 v-text="channel.name" class="!text-xl" />
<font-awesome-icon class="!text-xl" v-if="channel.verified" icon="check" />
</div>
</div>
<div class="flex gap-2">
<button
class="btn"
@click="subscribeHandler"
v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(channel.subscriberCount) },
}"
></button>
<!-- RSS Feed button -->
<a
aria-label="RSS feed"
title="RSS feed"
role="button"
v-if="channel.id"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
target="_blank"
class="btn flex-col"
>
<font-awesome-icon icon="rss" />
</a>
</div>
</div>
<img v-if="channel.bannerUrl" :src="channel.bannerUrl" class="w-full pb-1.5" loading="lazy" />
<!-- eslint-disable-next-line vue/no-v-html -->
<p class="whitespace-pre-wrap">
<span v-html="purifyHTML(rewriteDescription(channel.description))" />
</p>
<button
class="btn"
@click="subscribeHandler"
v-t="{
path: `actions.${subscribed ? 'unsubscribe' : 'subscribe'}`,
args: { count: numberFormat(channel.subscriberCount) },
}"
></button>
<!-- RSS Feed button -->
<a
aria-label="RSS feed"
title="RSS feed"
role="button"
v-if="channel.id"
:href="`${apiUrl()}/feed/unauthenticated/rss?channels=${channel.id}`"
target="_blank"
class="btn flex-col mx-3"
>
<font-awesome-icon icon="rss" />
</a>
<div v-if="channel.description" class="whitespace-pre-wrap py-2 mx-1">
<span v-if="fullDescription" v-html="purifyHTML(rewriteDescription(channel.description))" />
<span v-html="purifyHTML(rewriteDescription(channel.description.slice(0, 100)))" v-else />
<span v-if="channel.description.length > 100 && !fullDescription">...</span>
<button
v-if="channel.description.length > 100"
class="hover:underline font-semibold text-neutral-500 block whitespace-normal"
@click="fullDescription = !fullDescription"
>
[{{ fullDescription ? $t("actions.show_less") : $t("actions.show_more") }}]
</button>
</div>
<WatchOnYouTubeButton :link="`https://youtube.com/channel/${this.channel.id}`" />
<div class="flex mt-4 mb-2">
<div class="flex my-2 mx-1">
<button
v-for="(tab, index) in tabs"
:key="tab.name"
@@ -61,19 +82,21 @@
hide-channel
/>
</div>
</div>
</LoadingIndicatorPage>
</template>
<script>
import ErrorHandler from "./ErrorHandler.vue";
import ContentItem from "./ContentItem.vue";
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
export default {
components: {
ErrorHandler,
ContentItem,
WatchOnYouTubeButton,
LoadingIndicatorPage,
},
data() {
return {
@@ -82,6 +105,7 @@ export default {
tabs: [],
selectedTab: 0,
contentItems: [],
fullDescription: false,
};
},
mounted() {
@@ -121,7 +145,9 @@ export default {
});
},
async fetchChannel() {
const url = this.apiUrl() + "/" + this.$route.params.path + "/" + this.$route.params.channelId;
const url = this.$route.path.includes("@")
? this.apiUrl() + "/c/" + this.$route.params.channelId
: this.apiUrl() + "/" + this.$route.params.path + "/" + this.$route.params.channelId;
return await this.fetchJson(url);
},
async getChannelData() {

View File

@@ -24,27 +24,29 @@
<hr />
<div class="video-grid">
<LoadingIndicatorPage :show-content="videosStore != null" class="video-grid">
<template v-for="video in videos" :key="video.url">
<VideoItem v-if="shouldShowVideo(video)" :is-feed="true" :item="video" />
</template>
</div>
</LoadingIndicatorPage>
</template>
<script>
import VideoItem from "./VideoItem.vue";
import SortingSelector from "./SortingSelector.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
export default {
components: {
VideoItem,
SortingSelector,
LoadingIndicatorPage,
},
data() {
return {
currentVideoCount: 0,
videoStep: 100,
videosStore: [],
videosStore: null,
videos: [],
availableFilters: ["all", "shorts", "videos"],
selectedFilter: "all",

View File

@@ -0,0 +1,55 @@
<template>
<div v-if="!showContent" class="flex min-h-[75vh] w-full justify-center items-center">
<span id="spinner" />
</div>
<div v-else>
<slot />
</div>
</template>
<style>
#spinner:after {
--spinner-color: #000;
}
.dark #spinner:after {
--spinner-color: #fff;
}
#spinner {
display: inline-block;
width: 70px;
height: 70px;
}
#spinner:after {
content: " ";
display: block;
width: 54px;
height: 54px;
margin: 8px;
border-radius: 50%;
border: 4px solid var(--spinner-color);
border-color: var(--spinner-color) transparent var(--spinner-color) transparent;
animation: spinner 1.2s linear infinite;
}
@keyframes spinner {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
<script>
export default {
props: {
showContent: {
type: Boolean,
required: true,
},
},
};
</script>

View File

@@ -1,7 +1,7 @@
<template>
<ErrorHandler v-if="playlist && playlist.error" :message="playlist.message" :error="playlist.error" />
<div v-if="playlist" v-show="!playlist.error">
<LoadingIndicatorPage :show-content="playlist" v-show="!playlist.error">
<h1 class="text-center my-4" v-text="playlist.name" />
<div class="flex justify-between items-center">
@@ -46,11 +46,12 @@
width="168"
/>
</div>
</div>
</LoadingIndicatorPage>
</template>
<script>
import ErrorHandler from "./ErrorHandler.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import VideoItem from "./VideoItem.vue";
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
@@ -59,6 +60,7 @@ export default {
ErrorHandler,
VideoItem,
WatchOnYouTubeButton,
LoadingIndicatorPage,
},
data() {
return {
@@ -88,7 +90,7 @@ export default {
},
}).then(json => {
if (json.error) alert(json.error);
else if (json.filter(playlist => playlist.id === playlistId).length > 0) this.admin = true;
else if (json.some(playlist => playlist.id === playlistId)) this.admin = true;
});
this.isPlaylistBookmarked();
},

View File

@@ -376,6 +376,7 @@ export default {
languages: [
{ code: "ar", name: "Arabic" },
{ code: "az", name: "Azərbaycan" },
{ code: "bg", name: "Български" },
{ code: "bn", name: "বাংলা" },
{ code: "bs", name: "Bosanski" },
{ code: "ca", name: "Català" },
@@ -436,7 +437,7 @@ export default {
this.fetchJson("https://piped-instances.kavin.rocks/").then(resp => {
this.instances = resp;
if (this.instances.filter(instance => instance.api_url == this.apiUrl()).length == 0)
if (!this.instances.some(instance => instance.api_url == this.apiUrl()))
this.instances.push({
name: "Custom Instance",
api_url: this.apiUrl(),
@@ -615,4 +616,10 @@ export default {
.pref {
@apply flex justify-between items-center my-2 mx-[15vw] lt-md:mx-[2vw];
}
.pref:nth-child(odd) {
@apply bg-gray-200;
}
.dark .pref:nth-child(odd) {
@apply bg-dark-800;
}
</style>

View File

@@ -18,19 +18,21 @@
</i18n-t>
</div>
<div v-if="results" class="video-grid">
<LoadingIndicatorPage :show-content="results != null && results.items?.length" class="video-grid">
<template v-for="result in results.items" :key="result.url">
<ContentItem :item="result" height="94" width="168" />
</template>
</div>
</LoadingIndicatorPage>
</template>
<script>
import ContentItem from "./ContentItem.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
export default {
components: {
ContentItem,
LoadingIndicatorPage,
},
data() {
return {

View File

@@ -3,17 +3,19 @@
<hr />
<div class="video-grid">
<LoadingIndicatorPage :show-content="videos.length != 0" class="video-grid">
<VideoItem v-for="video in videos" :key="video.url" :item="video" height="118" width="210" />
</div>
</LoadingIndicatorPage>
</template>
<script>
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import VideoItem from "./VideoItem.vue";
export default {
components: {
VideoItem,
LoadingIndicatorPage,
},
data() {
return {

View File

@@ -12,10 +12,10 @@
>
<div class="w-full">
<img
class="w-full aspect-video"
class="w-full aspect-video object-contain"
:src="item.thumbnail"
:alt="item.title"
:class="{ 'shorts-img': item.isShort }"
:class="{ 'shorts-img': item.isShort, 'opacity-75': item.watched }"
loading="lazy"
/>
<!-- progress bar -->

View File

@@ -92,7 +92,7 @@ export default {
this.hotkeysPromise.then(() => {
var self = this;
this.$hotkeys(
"f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+n,shift+,,shift+.,return",
"f,m,j,k,l,c,space,up,down,left,right,0,1,2,3,4,5,6,7,8,9,shift+n,shift+,,shift+.,return,.,,",
function (e, handler) {
const videoEl = self.$refs.videoEl;
switch (handler.key) {
@@ -191,6 +191,14 @@ export default {
case "return":
self.skipSegment(videoEl);
break;
case ".":
videoEl.currentTime += 0.04;
e.preventDefault();
break;
case ",":
videoEl.currentTime -= 0.04;
e.preventDefault();
break;
}
},
);
@@ -206,6 +214,8 @@ export default {
},
methods: {
async loadVideo() {
this.updateSponsors();
const component = this;
const videoEl = this.$refs.videoEl;
@@ -263,9 +273,7 @@ export default {
const MseSupport = window.MediaSource !== undefined;
const lbry = this.getPreferenceBoolean("disableLBRY", false)
? null
: this.video.videoStreams.filter(stream => stream.quality === "LBRY")[0];
const lbry = null;
var uri;
var mime;
@@ -275,9 +283,10 @@ export default {
mime = "application/x-mpegURL";
} else if (this.video.audioStreams.length > 0 && !lbry && MseSupport) {
if (!this.video.dash) {
const dash = (
await import("@/utils/DashUtils.js").then(mod => mod.default)
).generate_dash_file_from_formats(streams, this.video.duration);
const dash = (await import("../utils/DashUtils.js")).generate_dash_file_from_formats(
streams,
this.video.duration,
);
uri = "data:application/dash+xml;charset=utf-8;base64," + btoa(dash);
} else {
@@ -313,7 +322,7 @@ export default {
uri = this.video.hls;
mime = "application/x-mpegURL";
} else {
uri = this.video.videoStreams.filter(stream => stream.codec == null).slice(-1)[0].url;
uri = this.video.videoStreams.findLast(stream => stream.codec == null).url;
mime = "video/mp4";
}
@@ -363,6 +372,9 @@ export default {
else this.setPlayerAttrs(this.$player, videoEl, uri, mime, this.$shaka);
if (noPrevPlayer) {
videoEl.addEventListener("loadeddata", () => {
if (document.pictureInPictureElement) videoEl.requestPictureInPicture();
});
videoEl.addEventListener("timeupdate", () => {
const time = videoEl.currentTime;
this.$emit("timeupdate", time);
@@ -647,22 +659,7 @@ export default {
if (markers) markers.style.background = `linear-gradient(${array.join(",")})`;
},
destroy(hotkeys) {
if (this.$ui) {
this.$ui.destroy();
this.$ui = undefined;
this.$player = undefined;
}
if (this.$player) {
this.$player.destroy();
this.$player = undefined;
}
if (hotkeys) this.$hotkeys?.unbind();
this.$refs.container?.querySelectorAll("div").forEach(node => node.remove());
},
},
watch: {
sponsors() {
updateSponsors() {
const skipOptions = this.getPreferenceJSON("skipOptions", {});
this.sponsors?.segments?.forEach(segment => {
const option = skipOptions[segment.category];
@@ -674,6 +671,19 @@ export default {
});
}
},
destroy(hotkeys) {
if (this.$ui && !document.pictureInPictureElement) {
this.$ui.destroy();
this.$ui = undefined;
this.$player = undefined;
}
if (this.$player) {
this.$player.destroy();
if (!document.pictureInPictureElement) this.$player = undefined;
}
if (hotkeys) this.$hotkeys?.unbind();
this.$refs.container?.querySelectorAll("div").forEach(node => node.remove());
},
},
};
</script>

View File

@@ -10,7 +10,7 @@
/>
</div>
<div v-if="video && !isEmbed" class="w-full">
<LoadingIndicatorPage :show-content="video && !isEmbed" class="w-full">
<ErrorHandler v-if="video && video.error" :message="video.message" :error="video.error" />
<Transition>
<ToastComponent v-if="shouldShowToast" @dismissed="dismiss">
@@ -20,15 +20,17 @@
<div v-show="!video.error">
<div :class="isMobile ? 'flex-col' : 'flex'">
<VideoPlayer
ref="videoPlayer"
:video="video"
:sponsors="sponsors"
:selected-auto-play="selectedAutoPlay"
:selected-auto-loop="selectedAutoLoop"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
/>
<keep-alive>
<VideoPlayer
ref="videoPlayer"
:video="video"
:sponsors="sponsors"
:selected-auto-play="selectedAutoPlay"
:selected-auto-loop="selectedAutoLoop"
@timeupdate="onTimeUpdate"
@ended="onVideoEnded"
/>
</keep-alive>
<ChaptersBar
:mobileLayout="isMobile"
v-if="video?.chapters?.length > 0 && showChapters"
@@ -219,7 +221,7 @@
<hr class="sm:hidden" />
</div>
</div>
</div>
</LoadingIndicatorPage>
</template>
<script>
@@ -232,6 +234,7 @@ import PlaylistAddModal from "./PlaylistAddModal.vue";
import ShareModal from "./ShareModal.vue";
import PlaylistVideos from "./PlaylistVideos.vue";
import WatchOnYouTubeButton from "./WatchOnYouTubeButton.vue";
import LoadingIndicatorPage from "./LoadingIndicatorPage.vue";
import ToastComponent from "./ToastComponent.vue";
export default {
@@ -246,14 +249,13 @@ export default {
ShareModal,
PlaylistVideos,
WatchOnYouTubeButton,
LoadingIndicatorPage,
ToastComponent,
},
data() {
const smallViewQuery = window.matchMedia("(max-width: 640px)");
return {
video: {
title: "Loading...",
},
video: null,
playlistId: null,
playlist: null,
index: null,
@@ -361,7 +363,7 @@ export default {
this.showDesc = !this.getPreferenceBoolean("minimizeDescription", false);
this.showRecs = !this.getPreferenceBoolean("minimizeRecommendations", false);
this.showChapters = !this.getPreferenceBoolean("minimizeChapters", false);
if (this.video.duration) {
if (this.video?.duration) {
document.title = this.video.title + " - Piped";
this.$refs.videoPlayer.loadVideo();
}