mirror of
https://github.com/TeamPiped/Piped.git
synced 2025-08-09 20:24:09 +00:00
Merge remote-tracking branch 'origin/master' into toast
This commit is contained in:
@@ -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() {
|
||||
|
@@ -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",
|
||||
|
55
src/components/LoadingIndicatorPage.vue
Normal file
55
src/components/LoadingIndicatorPage.vue
Normal 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>
|
@@ -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();
|
||||
},
|
||||
|
@@ -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>
|
||||
|
@@ -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 {
|
||||
|
@@ -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 {
|
||||
|
@@ -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 -->
|
||||
|
@@ -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>
|
||||
|
@@ -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();
|
||||
}
|
||||
|
Reference in New Issue
Block a user