فهرست منبع

Merge branch 'staging' of https://github.com/Musare/Musare into staging

Kristian Vos 2 سال پیش
والد
کامیت
b4c14a599b

+ 1 - 0
.wiki/Configuration.md

@@ -73,6 +73,7 @@ Location: `frontend/dist/config/default.json`
 | `siteSettings.logo_blue` | Path to the blue logo image, by default it is `/assets/blue_wordmark.png`. |
 | `siteSettings.sitename` | Should be the name of the site. |
 | `siteSettings.github` | URL of GitHub repository, defaults to `https://github.com/Musare/MusareNode`. |
+| `siteSettings.mediasession` | Whether to enable mediasession functionality. |
 | `siteSettings.christmas` | Whether to enable christmas theming. |
 | `messages.accountRemoval` | Message to return to users on account removal. |
 | `shortcutOverrides` | Overwrite keyboard shortcuts, for example `"editSong.useAllDiscogs": { "keyCode": 68, "ctrl": true, "alt": true, "shift": false, "preventDefault": true }`. |

BIN
frontend/dist/assets/15-seconds-of-silence.mp3


+ 1 - 0
frontend/dist/config/template.json

@@ -23,6 +23,7 @@
 		"logo_blue": "/assets/blue_wordmark.png",
 		"sitename": "Musare",
 		"github": "https://github.com/Musare/Musare",
+		"mediasession": false,
 		"christmas": false
 	},
 	"messages": {

+ 2 - 0
frontend/src/components/Modal.vue

@@ -197,6 +197,8 @@ export default {
 			.delete.material-icons {
 				font-size: 28px;
 				cursor: pointer;
+				user-select: none;
+				-webkit-user-drag: none;
 				&:hover,
 				&:focus {
 					filter: brightness(90%);

+ 2 - 1
frontend/src/components/layout/MainFooter.vue

@@ -90,10 +90,11 @@ export default {
 		margin-right: auto;
 		width: 160px;
 		order: 1;
-		user-select: none;
 
 		img {
 			max-width: 100%;
+			user-select: none;
+			-webkit-user-drag: none;
 		}
 	}
 

+ 1 - 0
frontend/src/components/layout/MainHeader.vue

@@ -290,6 +290,7 @@ export default {
 				max-height: 38px;
 				color: var(--primary-color);
 				user-select: none;
+				-webkit-user-drag: none;
 			}
 		}
 	}

+ 3 - 0
frontend/src/main.js

@@ -5,6 +5,7 @@ import VueTippy, { Tippy } from "vue-tippy";
 import { createRouter, createWebHistory } from "vue-router";
 
 import ws from "@/ws";
+import ms from "@/ms";
 import store from "./store";
 
 import AppComponent from "./App.vue";
@@ -220,6 +221,8 @@ lofig.folder = "../config/default.json";
 	const websocketsDomain = await lofig.get("backend.websocketsDomain");
 	ws.init(websocketsDomain);
 
+	if (await lofig.get("siteSettings.mediasession")) ms.init();
+
 	ws.socket.on("ready", res => {
 		const { loggedIn, role, username, userId, email } = res.data;
 

+ 102 - 0
frontend/src/ms.js

@@ -0,0 +1,102 @@
+/* global MediaMetadata */
+
+export default {
+	mediaSessionData: {},
+	listeners: {},
+	audio: null,
+	ytReady: false,
+	playSuccessful: false,
+	loopInterval: null,
+	setYTReady(ytReady) {
+		if (ytReady)
+			setTimeout(() => {
+				this.ytReady = true;
+			}, 1000);
+		else this.ytReady = false;
+	},
+	setListeners(priority, listeners) {
+		this.listeners[priority] = listeners;
+	},
+	removeListeners(priority) {
+		delete this.listeners[priority];
+	},
+	setMediaSessionData(priority, playing, title, artist, album, artwork) {
+		this.mediaSessionData[priority] = {
+			playing, // True = playing, false = paused
+			mediaMetadata: new MediaMetadata({
+				title,
+				artist,
+				album,
+				artwork: [{ src: artwork }]
+			})
+		};
+	},
+	removeMediaSessionData(priority) {
+		delete this.mediaSessionData[priority];
+	},
+	// Gets the highest priority media session data and updates the media session
+	updateMediaSession() {
+		const highestPriority = this.getHighestPriority();
+
+		if (typeof highestPriority === "number") {
+			const mediaSessionDataObject =
+				this.mediaSessionData[highestPriority];
+			navigator.mediaSession.metadata =
+				mediaSessionDataObject.mediaMetadata;
+
+			if (
+				mediaSessionDataObject.playing ||
+				!this.ytReady ||
+				!this.playSuccessful
+			) {
+				navigator.mediaSession.playbackState = "playing";
+				this.audio
+					.play()
+					.then(() => {
+						if (this.audio.currentTime > 1.0) {
+							this.audio.muted = true;
+						}
+						this.playSuccessful = true;
+					})
+					.catch(() => {
+						this.playSuccessful = false;
+					});
+			} else {
+				this.audio.pause();
+				navigator.mediaSession.playbackState = "paused";
+			}
+		} else {
+			this.audio.pause();
+			navigator.mediaSession.playbackState = "none";
+			navigator.mediaSession.metadata = null;
+		}
+	},
+	getHighestPriority() {
+		return Object.keys(this.mediaSessionData)
+			.map(priority => Number(priority))
+			.sort((a, b) => a > b)
+			.reverse()[0];
+	},
+	init() {
+		this.audio = new Audio("/assets/15-seconds-of-silence.mp3");
+
+		this.audio.loop = true;
+		this.audio.volume = 0.1;
+
+		navigator.mediaSession.setActionHandler("play", () => {
+			this.listeners[this.getHighestPriority()].play();
+		});
+
+		navigator.mediaSession.setActionHandler("pause", () => {
+			this.listeners[this.getHighestPriority()].pause();
+		});
+
+		navigator.mediaSession.setActionHandler("nexttrack", () => {
+			this.listeners[this.getHighestPriority()].nexttrack();
+		});
+
+		this.loopInterval = setInterval(() => {
+			this.updateMediaSession();
+		}, 100);
+	}
+};

+ 6 - 1
frontend/src/pages/Home.vue

@@ -928,11 +928,13 @@ html {
 .header {
 	display: flex;
 	height: 35vh;
+	min-height: 300px;
 	margin-top: -64px;
 	border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 
 	img.background {
 		height: 35vh;
+		min-height: 300px;
 		width: 100%;
 		object-fit: cover;
 		object-position: center;
@@ -951,6 +953,7 @@ html {
 		);
 		position: absolute;
 		height: 35vh;
+		min-height: 300px;
 		width: 100%;
 		border-radius: 0% 0% 33% 33% / 0% 0% 7% 7%;
 		overflow: hidden;
@@ -962,8 +965,8 @@ html {
 		margin-left: auto;
 		margin-right: auto;
 		text-align: center;
-		height: 100%;
 		height: 35vh;
+		min-height: 300px;
 		.content {
 			position: absolute;
 			top: 50%;
@@ -1009,10 +1012,12 @@ html {
 	}
 	&.loggedIn {
 		height: 20vh;
+		min-height: 200px;
 		.overlay,
 		.content-container,
 		img.background {
 			height: 20vh;
+			min-height: 200px;
 		}
 	}
 }

+ 43 - 1
frontend/src/pages/Station/index.vue

@@ -858,6 +858,7 @@ import Toast from "toasters";
 import { ContentLoader } from "vue-content-loader";
 
 import aw from "@/aw";
+import ms from "@/ms";
 import ws from "@/ws";
 import keyboardShortcuts from "@/keyboardShortcuts";
 
@@ -939,6 +940,7 @@ export default {
 			persistentToastCheckerInterval: null,
 			persistentToasts: [],
 			partyPlaylistLock: false,
+			mediasession: false,
 			christmas: false
 		};
 	},
@@ -1071,7 +1073,7 @@ export default {
 		});
 
 		this.frontendDevMode = await lofig.get("mode");
-
+		this.mediasession = await lofig.get("siteSettings.mediasession");
 		this.christmas = await lofig.get("siteSettings.christmas");
 
 		this.socket.dispatch(
@@ -1094,6 +1096,21 @@ export default {
 			}
 		);
 
+		ms.setListeners(0, {
+			play: () => {
+				if (this.isOwnerOrAdmin()) this.resumeStation();
+				else this.resumeLocalStation();
+			},
+			pause: () => {
+				if (this.isOwnerOrAdmin()) this.pauseStation();
+				else this.pauseLocalStation();
+			},
+			nexttrack: () => {
+				if (this.isOwnerOrAdmin()) this.skipStation();
+				else this.voteSkipStation();
+			}
+		});
+
 		this.socket.on("event:station.nextSong", res => {
 			const { currentSong, startedAt, paused, timePaused } = res.data;
 
@@ -1285,6 +1302,11 @@ export default {
 	beforeUnmount() {
 		document.body.style.cssText = "";
 
+		if (this.mediasession) {
+			ms.removeListeners(0);
+			ms.removeMediaSessionData(0);
+		}
+
 		/** Reset Songslist */
 		this.updateSongsList([]);
 
@@ -1325,6 +1347,18 @@ export default {
 		isOwnerOrAdmin() {
 			return this.isOwnerOnly() || this.isAdminOnly();
 		},
+		updateMediaSessionData(currentSong) {
+			if (currentSong) {
+				ms.setMediaSessionData(
+					0,
+					!this.localPaused && !this.stationPaused, // This should be improved later
+					this.currentSong.title,
+					this.currentSong.artists.join(", "),
+					null,
+					this.currentSong.thumbnail
+				);
+			} else ms.removeMediaSessionData(0);
+		},
 		removeFromQueue(youtubeId) {
 			window.socket.dispatch(
 				"stations.removeFromQueue",
@@ -1399,6 +1433,8 @@ export default {
 
 			clearTimeout(window.stationNextSongTimeout);
 
+			if (this.mediasession) this.updateMediaSessionData(currentSong);
+
 			this.startedAt = startedAt;
 			this.updateStationPaused(paused);
 			this.timePaused = timePaused;
@@ -1511,6 +1547,7 @@ export default {
 		},
 		youtubeReady() {
 			if (!this.player) {
+				ms.setYTReady(false);
 				this.player = new window.YT.Player("stationPlayer", {
 					height: 270,
 					width: 480,
@@ -1530,6 +1567,7 @@ export default {
 					events: {
 						onReady: () => {
 							this.playerReady = true;
+							ms.setYTReady(true);
 
 							let volume = parseInt(
 								localStorage.getItem("volume")
@@ -1808,6 +1846,8 @@ export default {
 			this.pauseLocalPlayer();
 		},
 		resumeLocalPlayer() {
+			if (this.mediasession)
+				this.updateMediaSessionData(this.currentSong);
 			if (!this.noSong) {
 				if (this.playerReady) {
 					this.player.seekTo(
@@ -1819,6 +1859,8 @@ export default {
 			}
 		},
 		pauseLocalPlayer() {
+			if (this.mediasession)
+				this.updateMediaSessionData(this.currentSong);
 			if (!this.noSong) {
 				this.timeBeforePause = this.getTimeElapsed();
 				if (this.playerReady) this.player.pauseVideo();