index.vue 68 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921
  1. <script setup lang="ts">
  2. import {
  3. defineAsyncComponent,
  4. ref,
  5. computed,
  6. watch,
  7. onMounted,
  8. onBeforeUnmount
  9. } from "vue";
  10. import { useRoute, useRouter } from "vue-router";
  11. import Toast from "toasters";
  12. import { storeToRefs } from "pinia";
  13. import { ContentLoader } from "vue-content-loader";
  14. import canAutoPlay from "can-autoplay";
  15. import { useWebsocketsStore } from "@/stores/websockets";
  16. import { useStationStore } from "@/stores/station";
  17. import { useUserAuthStore } from "@/stores/userAuth";
  18. import { useUserPreferencesStore } from "@/stores/userPreferences";
  19. import { useModalsStore } from "@/stores/modals";
  20. import aw from "@/aw";
  21. import ms from "@/ms";
  22. import ws from "@/ws";
  23. import keyboardShortcuts from "@/keyboardShortcuts";
  24. import utils from "@/utils";
  25. const MainHeader = defineAsyncComponent(
  26. () => import("@/components/MainHeader.vue")
  27. );
  28. const MainFooter = defineAsyncComponent(
  29. () => import("@/components/MainFooter.vue")
  30. );
  31. const FloatingBox = defineAsyncComponent(
  32. () => import("@/components/FloatingBox.vue")
  33. );
  34. const StationInfoBox = defineAsyncComponent(
  35. () => import("@/components/StationInfoBox.vue")
  36. );
  37. const AddToPlaylistDropdown = defineAsyncComponent(
  38. () => import("@/components/AddToPlaylistDropdown.vue")
  39. );
  40. const SongItem = defineAsyncComponent(
  41. () => import("@/components/SongItem.vue")
  42. );
  43. const Z404 = defineAsyncComponent(() => import("@/pages/404.vue"));
  44. const StationSidebar = defineAsyncComponent(
  45. () => import("./Sidebar/index.vue")
  46. );
  47. const route = useRoute();
  48. const router = useRouter();
  49. const { socket } = useWebsocketsStore();
  50. const stationStore = useStationStore();
  51. const userAuthStore = useUserAuthStore();
  52. const userPreferencesStore = useUserPreferencesStore();
  53. // TODO this might need a different place, like onMounted
  54. const isApple = ref(
  55. navigator.platform.match(/iPhone|iPod|iPad/) ||
  56. navigator.vendor === "Apple Computer, Inc."
  57. );
  58. const loading = ref(true);
  59. const exists = ref(true);
  60. const playerReady = ref(false);
  61. const player = ref(undefined);
  62. const timePaused = ref(0);
  63. const muted = ref(false);
  64. const timeElapsed = ref("0:00");
  65. const timeBeforePause = ref(0);
  66. const systemDifference = ref(0);
  67. const attemptsToPlayVideo = ref(0);
  68. const canAutoplay = ref(true);
  69. const lastTimeRequestedIfCanAutoplay = ref(0);
  70. const seeking = ref(false);
  71. const playbackRate = ref(1);
  72. const volumeSliderValue = ref(0);
  73. const showPlaylistDropdown = ref(false);
  74. const seekerbarPercentage = ref(0);
  75. const frontendDevMode = ref("production");
  76. const activityWatchVideoDataInterval = ref(null);
  77. const activityWatchVideoLastStatus = ref("");
  78. const activityWatchVideoLastYouTubeId = ref("");
  79. // const activityWatchVideoLastStartDuration = ref("");
  80. const nextCurrentSong = ref(null);
  81. const mediaModalWatcher = ref(null);
  82. const beforeMediaModalLocalPausedLock = ref(false);
  83. const beforeMediaModalLocalPaused = ref(null);
  84. const socketConnected = ref(null);
  85. const persistentToastCheckerInterval = ref(null);
  86. const persistentToasts = ref([]);
  87. const mediasession = ref(false);
  88. const christmas = ref(false);
  89. const sitename = ref("Musare");
  90. // NEW
  91. const videoLoading = ref();
  92. const startedAt = ref();
  93. const pausedAt = ref();
  94. const stationIdentifier = ref();
  95. // ENDNEW
  96. const playerDebugBox = ref();
  97. const keyboardShortcutsHelper = ref();
  98. const modalsStore = useModalsStore();
  99. const { activeModals } = storeToRefs(modalsStore);
  100. // TODO fix this if it still has some use, as this is no longer accurate
  101. // const video = computed(() => store.state.modals.editSong);
  102. const { loggedIn, userId } = storeToRefs(userAuthStore);
  103. const { nightmode, autoSkipDisliked } = storeToRefs(userPreferencesStore);
  104. const {
  105. station,
  106. currentSong,
  107. nextSong,
  108. songsList,
  109. stationPaused,
  110. localPaused,
  111. noSong,
  112. autoRequest,
  113. autoRequestLock
  114. } = storeToRefs(stationStore);
  115. const skipVotesLoaded = computed(
  116. () =>
  117. !noSong.value &&
  118. Number.isInteger(currentSong.value.skipVotes) &&
  119. currentSong.value.skipVotes >= 0
  120. );
  121. const ratingsLoaded = computed(
  122. () =>
  123. !noSong.value &&
  124. Number.isInteger(currentSong.value.likes) &&
  125. Number.isInteger(currentSong.value.dislikes) &&
  126. currentSong.value.likes >= 0 &&
  127. currentSong.value.dislikes >= 0
  128. );
  129. const ownRatingsLoaded = computed(
  130. () =>
  131. !noSong.value &&
  132. typeof currentSong.value.liked === "boolean" &&
  133. typeof currentSong.value.disliked === "boolean"
  134. );
  135. const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
  136. const currentUserQueueSongs = computed(
  137. () =>
  138. songsList.value.filter(
  139. queueSong => queueSong.requestedBy === userId.value
  140. ).length
  141. );
  142. const {
  143. joinStation,
  144. leaveStation,
  145. updateStation,
  146. updateUserCount,
  147. updateUsers,
  148. updateCurrentSong,
  149. updateNextSong,
  150. updateSongsList,
  151. repositionSongInList,
  152. updateStationPaused,
  153. updateLocalPaused,
  154. updateNoSong,
  155. updateIfStationIsFavorited,
  156. setAutofillPlaylists,
  157. setBlacklist,
  158. updateCurrentSongRatings,
  159. updateOwnCurrentSongRatings,
  160. updateCurrentSongSkipVotes,
  161. updateAutoRequestLock,
  162. hasPermission,
  163. addDj,
  164. removeDj,
  165. updatePermissions
  166. } = stationStore;
  167. // TODO fix this if it still has some use
  168. // const stopVideo = payload =>
  169. // store.dispatch("modals/editSong/stopVideo", payload);
  170. const updateMediaSessionData = song => {
  171. if (song) {
  172. ms.setMediaSessionData(
  173. 0,
  174. !localPaused.value && !stationPaused.value, // This should be improved later
  175. song.title,
  176. song.artists ? song.artists.join(", ") : null,
  177. null,
  178. song.thumbnail ||
  179. `https://img.youtube.com/vi/${song.youtubeId}/mqdefault.jpg`
  180. );
  181. } else ms.removeMediaSessionData(0);
  182. };
  183. const autoRequestSong = () => {
  184. if (
  185. !autoRequestLock.value &&
  186. songsList.value.length < 50 &&
  187. currentUserQueueSongs.value < station.value.requests.limit * 0.5 &&
  188. autoRequest.value.length > 0
  189. ) {
  190. const selectedPlaylist =
  191. autoRequest.value[
  192. Math.floor(Math.random() * autoRequest.value.length)
  193. ];
  194. if (selectedPlaylist._id && selectedPlaylist.songs.length > 0) {
  195. const selectedSong =
  196. selectedPlaylist.songs[
  197. Math.floor(Math.random() * selectedPlaylist.songs.length)
  198. ];
  199. if (selectedSong.youtubeId) {
  200. updateAutoRequestLock(true);
  201. socket.dispatch(
  202. "stations.addToQueue",
  203. station.value._id,
  204. selectedSong.youtubeId,
  205. data => {
  206. updateAutoRequestLock(false);
  207. if (data.status !== "success") {
  208. setTimeout(
  209. () => {
  210. autoRequestSong();
  211. },
  212. data.message ===
  213. "That song is already in the queue."
  214. ? 5000
  215. : 1000
  216. );
  217. }
  218. }
  219. );
  220. }
  221. }
  222. }
  223. };
  224. const dateCurrently = () => new Date().getTime() + systemDifference.value;
  225. const getTimeElapsed = () => {
  226. if (currentSong.value) {
  227. if (stationPaused.value)
  228. timePaused.value += dateCurrently() - pausedAt.value;
  229. return dateCurrently() - startedAt.value - timePaused.value;
  230. }
  231. return 0;
  232. };
  233. const getTimeRemaining = () => {
  234. if (currentSong.value) {
  235. return currentSong.value.duration * 1000 - getTimeElapsed();
  236. }
  237. return 0;
  238. };
  239. const skipSong = () => {
  240. if (nextCurrentSong.value && nextCurrentSong.value.currentSong) {
  241. const _songsList = songsList.value.concat([]);
  242. if (
  243. _songsList.length > 0 &&
  244. _songsList[0].youtubeId ===
  245. nextCurrentSong.value.currentSong.youtubeId
  246. ) {
  247. _songsList.splice(0, 1);
  248. updateSongsList(_songsList);
  249. }
  250. // TODO fix
  251. // eslint-disable-next-line
  252. setCurrentSong(nextCurrentSong.value);
  253. } else {
  254. // TODO fix
  255. // eslint-disable-next-line
  256. setCurrentSong({
  257. currentSong: null,
  258. startedAt: 0,
  259. paused: stationPaused.value,
  260. timePaused: 0,
  261. pausedAt: 0
  262. });
  263. }
  264. };
  265. const setNextCurrentSong = (_nextCurrentSong, skipSkipCheck = false) => {
  266. nextCurrentSong.value = _nextCurrentSong;
  267. // If skipSkipCheck is true, it won't try to skip the song
  268. if (getTimeRemaining() <= 0 && !skipSkipCheck) {
  269. skipSong();
  270. }
  271. };
  272. const resizeSeekerbar = () => {
  273. seekerbarPercentage.value =
  274. (getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
  275. };
  276. const calculateTimeElapsed = () => {
  277. if (
  278. playerReady.value &&
  279. !noSong.value &&
  280. currentSong.value &&
  281. player.value.getPlayerState() === -1
  282. ) {
  283. if (!canAutoplay.value) {
  284. if (Date.now() - lastTimeRequestedIfCanAutoplay.value > 2000) {
  285. lastTimeRequestedIfCanAutoplay.value = Date.now();
  286. canAutoPlay.video().then(({ result }) => {
  287. if (result) {
  288. attemptsToPlayVideo.value = 0;
  289. canAutoplay.value = true;
  290. } else {
  291. canAutoplay.value = false;
  292. }
  293. });
  294. }
  295. } else {
  296. player.value.playVideo();
  297. attemptsToPlayVideo.value += 1;
  298. }
  299. }
  300. if (
  301. !stationPaused.value &&
  302. !localPaused.value &&
  303. playerReady.value &&
  304. !isApple.value
  305. ) {
  306. const timeElapsed = getTimeElapsed();
  307. const currentPlayerTime =
  308. Math.max(
  309. player.value.getCurrentTime() - currentSong.value.skipDuration,
  310. 0
  311. ) * 1000;
  312. const difference = timeElapsed - currentPlayerTime;
  313. let _playbackRate = 1;
  314. if (difference < -2000) {
  315. if (!seeking.value) {
  316. seeking.value = true;
  317. player.value.seekTo(
  318. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  319. );
  320. }
  321. } else if (difference < -200) {
  322. _playbackRate = 0.8;
  323. } else if (difference < -50) {
  324. _playbackRate = 0.9;
  325. } else if (difference < -25) {
  326. _playbackRate = 0.95;
  327. } else if (difference > 2000) {
  328. if (!seeking.value) {
  329. seeking.value = true;
  330. player.value.seekTo(
  331. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  332. );
  333. }
  334. } else if (difference > 200) {
  335. _playbackRate = 1.2;
  336. } else if (difference > 50) {
  337. _playbackRate = 1.1;
  338. } else if (difference > 25) {
  339. _playbackRate = 1.05;
  340. } else if (player.value.getPlaybackRate !== 1.0) {
  341. player.value.setPlaybackRate(1.0);
  342. }
  343. if (playbackRate.value !== _playbackRate) {
  344. player.value.setPlaybackRate(_playbackRate);
  345. playbackRate.value = _playbackRate;
  346. }
  347. }
  348. if (stationPaused.value)
  349. timePaused.value += dateCurrently() - pausedAt.value;
  350. const duration =
  351. (dateCurrently() - startedAt.value - timePaused.value) / 1000;
  352. const songDuration = currentSong.value.duration;
  353. if (playerReady.value && songDuration <= duration)
  354. player.value.pauseVideo();
  355. if (duration <= songDuration)
  356. timeElapsed.value = utils.formatTime(duration) || "0";
  357. };
  358. const playVideo = () => {
  359. if (playerReady.value) {
  360. videoLoading.value = true;
  361. player.value.loadVideoById(
  362. currentSong.value.youtubeId,
  363. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  364. );
  365. if (window.stationInterval !== 0) clearInterval(window.stationInterval);
  366. window.stationInterval = setInterval(() => {
  367. if (!stationPaused.value) {
  368. resizeSeekerbar();
  369. calculateTimeElapsed();
  370. }
  371. }, 150);
  372. }
  373. };
  374. const toggleSkipVote = (message?) => {
  375. socket.dispatch("stations.toggleSkipVote", station.value._id, data => {
  376. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  377. else
  378. new Toast(
  379. message || "Successfully toggled vote to skip the current song."
  380. );
  381. });
  382. };
  383. const youtubeReady = () => {
  384. if (!player.value) {
  385. ms.setYTReady(false);
  386. player.value = new window.YT.Player("stationPlayer", {
  387. height: 270,
  388. width: 480,
  389. videoId: currentSong.value.youtubeId,
  390. host: "https://www.youtube-nocookie.com",
  391. startSeconds:
  392. getTimeElapsed() / 1000 + currentSong.value.skipDuration,
  393. playerVars: {
  394. controls: 0,
  395. iv_load_policy: 3,
  396. rel: 0,
  397. showinfo: 0,
  398. disablekb: 1,
  399. playsinline: 1
  400. },
  401. events: {
  402. onReady: () => {
  403. playerReady.value = true;
  404. ms.setYTReady(true);
  405. let volume = parseFloat(localStorage.getItem("volume"));
  406. volume = typeof volume === "number" ? volume : 20;
  407. player.value.setVolume(volume);
  408. if (volume > 0) player.value.unMute();
  409. if (muted.value) player.value.mute();
  410. playVideo();
  411. const duration =
  412. (dateCurrently() - startedAt.value - timePaused.value) /
  413. 1000;
  414. const songDuration = currentSong.value.duration;
  415. if (songDuration <= duration) player.value.pauseVideo();
  416. // on ios, playback will be forcibly paused locally
  417. if (isApple.value) {
  418. updateLocalPaused(true);
  419. new Toast(
  420. `Please click play manually to use ${sitename.value} on iOS.`
  421. );
  422. }
  423. },
  424. onError: err => {
  425. console.log("error with youtube video", err);
  426. if (err.data === 150 && loggedIn.value) {
  427. if (
  428. !(localPaused.value || stationPaused.value) &&
  429. !currentSong.value.voted
  430. ) {
  431. // automatically vote to skip
  432. toggleSkipVote(
  433. "Automatically voted to skip as this song isn't available for you."
  434. );
  435. }
  436. // persistent message while song is playing
  437. const persistentToast = new Toast({
  438. content:
  439. "This song is unavailable for you, but is playing for everyone else.",
  440. persistent: true
  441. });
  442. // save current song id
  443. const erroredYoutubeId = currentSong.value.youtubeId;
  444. persistentToasts.value.push({
  445. toast: persistentToast,
  446. checkIfCanRemove: () => {
  447. if (
  448. currentSong.value.youtubeId !==
  449. erroredYoutubeId
  450. ) {
  451. persistentToast.destroy();
  452. return true;
  453. }
  454. return false;
  455. }
  456. });
  457. } else {
  458. new Toast(
  459. "There has been an error with the YouTube Embed"
  460. );
  461. }
  462. },
  463. onStateChange: event => {
  464. if (
  465. event.data === window.YT.PlayerState.PLAYING &&
  466. videoLoading.value === true
  467. ) {
  468. videoLoading.value = false;
  469. player.value.seekTo(
  470. getTimeElapsed() / 1000 +
  471. currentSong.value.skipDuration,
  472. true
  473. );
  474. canAutoplay.value = true;
  475. if (localPaused.value || stationPaused.value)
  476. player.value.pauseVideo();
  477. } else if (
  478. event.data === window.YT.PlayerState.PLAYING &&
  479. (localPaused.value || stationPaused.value)
  480. ) {
  481. player.value.seekTo(timeBeforePause.value / 1000, true);
  482. player.value.pauseVideo();
  483. } else if (
  484. event.data === window.YT.PlayerState.PLAYING &&
  485. seeking.value === true
  486. )
  487. seeking.value = false;
  488. if (
  489. event.data === window.YT.PlayerState.PAUSED &&
  490. !localPaused.value &&
  491. !stationPaused.value &&
  492. !noSong.value &&
  493. player.value.getDuration() / 1000 <
  494. currentSong.value.duration
  495. ) {
  496. player.value.seekTo(
  497. getTimeElapsed() / 1000 +
  498. currentSong.value.skipDuration,
  499. true
  500. );
  501. player.value.playVideo();
  502. }
  503. }
  504. }
  505. });
  506. }
  507. };
  508. const setCurrentSong = data => {
  509. const {
  510. currentSong: _currentSong,
  511. startedAt: _startedAt,
  512. paused: _paused,
  513. timePaused: _timePaused,
  514. pausedAt: _pausedAt
  515. } = data;
  516. if (_currentSong) {
  517. if (!_currentSong.skipDuration || _currentSong.skipDuration < 0)
  518. _currentSong.skipDuration = 0;
  519. if (!_currentSong.duration || _currentSong.duration < 0)
  520. _currentSong.duration = 0;
  521. }
  522. updateCurrentSong(_currentSong || {});
  523. let nextSong = null;
  524. if (songsList.value[0])
  525. nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
  526. updateNextSong(nextSong);
  527. setNextCurrentSong(
  528. {
  529. currentSong: null,
  530. startedAt: 0,
  531. _paused,
  532. timePaused: 0,
  533. pausedAt: 0
  534. },
  535. true
  536. );
  537. clearTimeout(window.stationNextSongTimeout);
  538. if (mediasession.value) updateMediaSessionData(_currentSong);
  539. startedAt.value = _startedAt;
  540. updateStationPaused(_paused);
  541. timePaused.value = _timePaused;
  542. pausedAt.value = _pausedAt;
  543. if (_currentSong) {
  544. updateNoSong(false);
  545. if (!playerReady.value) youtubeReady();
  546. else playVideo();
  547. // If the station is playing and the backend is not connected, set the next song to skip to after this song and set a timer to skip
  548. if (!stationPaused.value && !socketConnected.value) {
  549. if (nextSong)
  550. setNextCurrentSong(
  551. {
  552. currentSong: nextSong,
  553. startedAt: Date.now() + getTimeRemaining(),
  554. paused: false,
  555. timePaused: 0
  556. },
  557. true
  558. );
  559. else
  560. setNextCurrentSong(
  561. {
  562. currentSong: null,
  563. startedAt: 0,
  564. paused: false,
  565. timePaused: 0,
  566. pausedAt: 0
  567. },
  568. true
  569. );
  570. window.stationNextSongTimeout = setTimeout(() => {
  571. if (
  572. !noSong.value &&
  573. _currentSong.value._id === _currentSong._id
  574. )
  575. skipSong();
  576. }, getTimeRemaining());
  577. }
  578. const currentSongId = currentSong.value._id;
  579. socket.dispatch(
  580. "stations.getSkipVotes",
  581. station.value._id,
  582. currentSongId,
  583. res => {
  584. if (res.status === "success") {
  585. const { skipVotes, skipVotesCurrent, voted } = res.data;
  586. if (
  587. !noSong.value &&
  588. currentSong.value._id === currentSongId
  589. ) {
  590. updateCurrentSongSkipVotes({
  591. skipVotes,
  592. skipVotesCurrent,
  593. voted
  594. });
  595. }
  596. }
  597. }
  598. );
  599. socket.dispatch("media.getRatings", _currentSong.youtubeId, res => {
  600. if (_currentSong.youtubeId === currentSong.value.youtubeId) {
  601. const { likes, dislikes } = res.data;
  602. updateCurrentSongRatings({ likes, dislikes });
  603. }
  604. });
  605. if (loggedIn.value) {
  606. socket.dispatch(
  607. "media.getOwnRatings",
  608. _currentSong.youtubeId,
  609. res => {
  610. console.log("getOwnSongRatings", res);
  611. if (
  612. res.status === "success" &&
  613. currentSong.value.youtubeId === res.data.youtubeId
  614. ) {
  615. updateOwnCurrentSongRatings(res.data);
  616. if (
  617. autoSkipDisliked.value &&
  618. res.data.disliked === true &&
  619. !(localPaused.value || stationPaused.value) &&
  620. !currentSong.value.voted
  621. ) {
  622. toggleSkipVote(
  623. "Automatically voted to skip disliked song."
  624. );
  625. }
  626. }
  627. }
  628. );
  629. }
  630. } else {
  631. if (playerReady.value) player.value.stopVideo();
  632. updateNoSong(true);
  633. }
  634. calculateTimeElapsed();
  635. resizeSeekerbar();
  636. };
  637. const changeVolume = () => {
  638. const volume = volumeSliderValue.value;
  639. localStorage.setItem("volume", `${volume}`);
  640. if (playerReady.value) {
  641. player.value.setVolume(volume);
  642. if (volume > 0) {
  643. player.value.unMute();
  644. localStorage.setItem("muted", "false");
  645. muted.value = false;
  646. }
  647. }
  648. };
  649. const resumeLocalPlayer = () => {
  650. if (mediasession.value) updateMediaSessionData(currentSong.value);
  651. if (!noSong.value) {
  652. if (playerReady.value) {
  653. player.value.seekTo(
  654. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  655. );
  656. player.value.playVideo();
  657. }
  658. }
  659. };
  660. const pauseLocalPlayer = () => {
  661. if (mediasession.value) updateMediaSessionData(currentSong.value);
  662. if (!noSong.value) {
  663. timeBeforePause.value = getTimeElapsed();
  664. if (playerReady.value) player.value.pauseVideo();
  665. }
  666. };
  667. const resumeLocalStation = () => {
  668. updateLocalPaused(false);
  669. if (!stationPaused.value) resumeLocalPlayer();
  670. };
  671. const pauseLocalStation = () => {
  672. updateLocalPaused(true);
  673. pauseLocalPlayer();
  674. };
  675. const skipStation = () => {
  676. socket.dispatch("stations.forceSkip", station.value._id, data => {
  677. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  678. else new Toast("Successfully skipped the station's current song.");
  679. });
  680. };
  681. const resumeStation = () => {
  682. socket.dispatch("stations.resume", station.value._id, data => {
  683. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  684. else new Toast("Successfully resumed the station.");
  685. });
  686. };
  687. const pauseStation = () => {
  688. socket.dispatch("stations.pause", station.value._id, data => {
  689. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  690. else new Toast("Successfully paused the station.");
  691. });
  692. };
  693. const toggleMute = () => {
  694. if (playerReady.value) {
  695. const previousVolume = parseFloat(localStorage.getItem("volume"));
  696. const volume = player.value.getVolume() <= 0 ? previousVolume : 0;
  697. muted.value = !muted.value;
  698. localStorage.setItem("muted", `${muted.value}`);
  699. volumeSliderValue.value = volume;
  700. player.value.setVolume(volume);
  701. if (!muted.value) localStorage.setItem("volume", `${volume}`);
  702. }
  703. };
  704. const increaseVolume = () => {
  705. if (playerReady.value) {
  706. const previousVolume = parseFloat(localStorage.getItem("volume"));
  707. let volume = previousVolume + 5;
  708. if (previousVolume === 0) {
  709. muted.value = false;
  710. localStorage.setItem("muted", "false");
  711. }
  712. if (volume > 100) volume = 100;
  713. volumeSliderValue.value = volume;
  714. player.value.setVolume(volume);
  715. localStorage.setItem("volume", `${volume}`);
  716. }
  717. };
  718. const toggleLike = () => {
  719. if (currentSong.value.liked)
  720. socket.dispatch("media.unlike", currentSong.value.youtubeId, res => {
  721. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  722. });
  723. else
  724. socket.dispatch("media.like", currentSong.value.youtubeId, res => {
  725. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  726. });
  727. };
  728. const toggleDislike = () => {
  729. if (currentSong.value.disliked)
  730. return socket.dispatch(
  731. "media.undislike",
  732. currentSong.value.youtubeId,
  733. res => {
  734. if (res.status !== "success")
  735. new Toast(`Error: ${res.message}`);
  736. }
  737. );
  738. return socket.dispatch(
  739. "media.dislike",
  740. currentSong.value.youtubeId,
  741. res => {
  742. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  743. }
  744. );
  745. };
  746. const togglePlayerDebugBox = () => {
  747. playerDebugBox.value.toggleBox();
  748. };
  749. const resetPlayerDebugBox = () => {
  750. playerDebugBox.value.resetBox();
  751. };
  752. const toggleKeyboardShortcutsHelper = () => {
  753. keyboardShortcutsHelper.value.toggleBox();
  754. };
  755. const resetKeyboardShortcutsHelper = () => {
  756. keyboardShortcutsHelper.value.resetBox();
  757. };
  758. const join = () => {
  759. socket.dispatch("stations.join", stationIdentifier.value, async res => {
  760. if (res.status === "success") {
  761. setTimeout(() => {
  762. loading.value = false;
  763. }, 1000); // prevents popping in of youtube embed etc.
  764. const {
  765. _id,
  766. displayName,
  767. name,
  768. description,
  769. privacy,
  770. owner,
  771. autofill,
  772. blacklist,
  773. type,
  774. isFavorited,
  775. theme,
  776. requests,
  777. djs
  778. } = res.data;
  779. // change url to use station name instead of station id
  780. if (name !== stationIdentifier.value) {
  781. // eslint-disable-next-line no-restricted-globals
  782. router.replace(name);
  783. }
  784. joinStation({
  785. _id,
  786. name,
  787. displayName,
  788. description,
  789. privacy,
  790. owner,
  791. autofill,
  792. blacklist,
  793. type,
  794. isFavorited,
  795. theme,
  796. requests,
  797. djs
  798. });
  799. document.getElementsByTagName(
  800. "html"
  801. )[0].style.cssText = `--primary-color: var(--${res.data.theme})`;
  802. setCurrentSong({
  803. currentSong: res.data.currentSong,
  804. startedAt: res.data.startedAt,
  805. paused: res.data.paused,
  806. timePaused: res.data.timePaused,
  807. pausedAt: res.data.pausedAt
  808. });
  809. updateUserCount(res.data.userCount);
  810. updateUsers(res.data.users);
  811. await updatePermissions();
  812. socket.dispatch(
  813. "stations.getStationAutofillPlaylistsById",
  814. station.value._id,
  815. res => {
  816. if (res.status === "success") {
  817. setAutofillPlaylists(res.data.playlists);
  818. }
  819. }
  820. );
  821. socket.dispatch(
  822. "stations.getStationBlacklistById",
  823. station.value._id,
  824. res => {
  825. if (res.status === "success") {
  826. setBlacklist(res.data.playlists);
  827. }
  828. }
  829. );
  830. socket.dispatch("stations.getQueue", _id, res => {
  831. if (res.status === "success") {
  832. const { queue } = res.data;
  833. updateSongsList(queue);
  834. const [nextSong] = queue;
  835. updateNextSong(nextSong);
  836. }
  837. });
  838. if (hasPermission("stations.playback.toggle"))
  839. keyboardShortcuts.registerShortcut("station.pauseResume", {
  840. keyCode: 32, // Spacebar
  841. shift: false,
  842. ctrl: true,
  843. preventDefault: true,
  844. handler: () => {
  845. if (aModalIsOpen.value) return;
  846. if (stationPaused.value) resumeStation();
  847. else pauseStation();
  848. }
  849. });
  850. if (hasPermission("stations.skip"))
  851. keyboardShortcuts.registerShortcut("station.skipStation", {
  852. keyCode: 39, // Right arrow key
  853. shift: false,
  854. ctrl: true,
  855. preventDefault: true,
  856. handler: () => {
  857. if (aModalIsOpen.value) return;
  858. skipStation();
  859. }
  860. });
  861. keyboardShortcuts.registerShortcut("station.lowerVolumeLarge", {
  862. keyCode: 40, // Down arrow key
  863. shift: false,
  864. ctrl: true,
  865. preventDefault: true,
  866. handler: () => {
  867. if (aModalIsOpen.value) return;
  868. volumeSliderValue.value -= 10;
  869. changeVolume();
  870. }
  871. });
  872. keyboardShortcuts.registerShortcut("station.lowerVolumeSmall", {
  873. keyCode: 40, // Down arrow key
  874. shift: true,
  875. ctrl: true,
  876. preventDefault: true,
  877. handler: () => {
  878. if (aModalIsOpen.value) return;
  879. volumeSliderValue.value -= 1;
  880. changeVolume();
  881. }
  882. });
  883. keyboardShortcuts.registerShortcut("station.increaseVolumeLarge", {
  884. keyCode: 38, // Up arrow key
  885. shift: false,
  886. ctrl: true,
  887. preventDefault: true,
  888. handler: () => {
  889. if (aModalIsOpen.value) return;
  890. volumeSliderValue.value += 10;
  891. changeVolume();
  892. }
  893. });
  894. keyboardShortcuts.registerShortcut("station.increaseVolumeSmall", {
  895. keyCode: 38, // Up arrow key
  896. shift: true,
  897. ctrl: true,
  898. preventDefault: true,
  899. handler: () => {
  900. if (aModalIsOpen.value) return;
  901. volumeSliderValue.value += 1;
  902. changeVolume();
  903. }
  904. });
  905. keyboardShortcuts.registerShortcut("station.toggleDebug", {
  906. keyCode: 68, // D key
  907. shift: false,
  908. ctrl: true,
  909. preventDefault: true,
  910. handler: () => {
  911. if (aModalIsOpen.value) return;
  912. togglePlayerDebugBox();
  913. }
  914. });
  915. keyboardShortcuts.registerShortcut(
  916. "station.toggleKeyboardShortcutsHelper",
  917. {
  918. keyCode: 191, // '/' key
  919. ctrl: true,
  920. preventDefault: true,
  921. handler: () => {
  922. if (aModalIsOpen.value) return;
  923. toggleKeyboardShortcutsHelper();
  924. }
  925. }
  926. );
  927. keyboardShortcuts.registerShortcut(
  928. "station.resetKeyboardShortcutsHelper",
  929. {
  930. keyCode: 191, // '/' key
  931. ctrl: true,
  932. shift: true,
  933. preventDefault: true,
  934. handler: () => {
  935. if (aModalIsOpen.value) return;
  936. resetKeyboardShortcutsHelper();
  937. }
  938. }
  939. );
  940. // UNIX client time before ping
  941. const beforePing = Date.now();
  942. socket.dispatch("apis.ping", res => {
  943. if (res.status === "success") {
  944. // UNIX client time after ping
  945. const afterPing = Date.now();
  946. // Average time in MS it took between the server responding and the client receiving
  947. const connectionLatency = (afterPing - beforePing) / 2;
  948. console.log(connectionLatency, beforePing - afterPing);
  949. // UNIX server time
  950. const serverDate = res.data.date;
  951. // Difference between the server UNIX time and the client UNIX time after ping, with the connectionLatency added to the server UNIX time
  952. const difference =
  953. serverDate + connectionLatency - afterPing;
  954. console.log("Difference: ", difference);
  955. if (difference > 3000 || difference < -3000) {
  956. console.log(
  957. "System time difference is bigger than 3 seconds."
  958. );
  959. }
  960. systemDifference.value = difference;
  961. }
  962. });
  963. } else {
  964. loading.value = false;
  965. exists.value = false;
  966. }
  967. });
  968. };
  969. const sendActivityWatchVideoData = () => {
  970. if (!stationPaused.value && !localPaused.value && !noSong.value) {
  971. if (activityWatchVideoLastStatus.value !== "playing") {
  972. activityWatchVideoLastStatus.value = "playing";
  973. activityWatchVideoLastStatus.value = `${
  974. currentSong.value.skipDuration + getTimeElapsed()
  975. }`;
  976. }
  977. if (
  978. activityWatchVideoLastYouTubeId.value !==
  979. currentSong.value.youtubeId
  980. ) {
  981. activityWatchVideoLastYouTubeId.value = currentSong.value.youtubeId;
  982. activityWatchVideoLastStatus.value = `${
  983. currentSong.value.skipDuration + getTimeElapsed()
  984. }`;
  985. }
  986. const videoData = {
  987. title: currentSong.value ? currentSong.value.title : null,
  988. artists:
  989. currentSong.value && currentSong.value.artists
  990. ? currentSong.value.artists.join(", ")
  991. : null,
  992. youtubeId: currentSong.value.youtubeId,
  993. muted: muted.value,
  994. volume: volumeSliderValue.value,
  995. startedDuration:
  996. Number(activityWatchVideoLastStatus.value) <= 0
  997. ? 0
  998. : Math.floor(
  999. Number(activityWatchVideoLastStatus.value) / 1000
  1000. ),
  1001. source: `station#${station.value.name}`,
  1002. hostname: window.location.hostname
  1003. };
  1004. aw.sendVideoData(videoData);
  1005. } else {
  1006. activityWatchVideoLastStatus.value = "not_playing";
  1007. }
  1008. };
  1009. watch(
  1010. () => autoRequest.value.length,
  1011. () => {
  1012. autoRequestSong();
  1013. }
  1014. );
  1015. onMounted(async () => {
  1016. mediaModalWatcher.value = stationStore.$onAction(({ name, args }) => {
  1017. if (name === "updateMediaModalPlayingAudio") {
  1018. const [mediaModalPlayingAudio] = args;
  1019. if (mediaModalPlayingAudio) {
  1020. if (!beforeMediaModalLocalPausedLock.value) {
  1021. beforeMediaModalLocalPausedLock.value = true;
  1022. beforeMediaModalLocalPaused.value = localPaused.value;
  1023. pauseLocalStation();
  1024. }
  1025. } else {
  1026. beforeMediaModalLocalPausedLock.value = false;
  1027. if (!beforeMediaModalLocalPaused.value) resumeLocalStation();
  1028. }
  1029. }
  1030. });
  1031. window.scrollTo(0, 0);
  1032. stationIdentifier.value = route.params.id;
  1033. window.stationInterval = 0;
  1034. activityWatchVideoDataInterval.value = setInterval(() => {
  1035. sendActivityWatchVideoData();
  1036. }, 1000);
  1037. persistentToastCheckerInterval.value = setInterval(() => {
  1038. persistentToasts.value.filter(
  1039. persistentToast => !persistentToast.checkIfCanRemove()
  1040. );
  1041. }, 1000);
  1042. if (socket.readyState === 1) join();
  1043. ws.onConnect(() => {
  1044. socketConnected.value = true;
  1045. clearTimeout(window.stationNextSongTimeout);
  1046. join();
  1047. });
  1048. ws.onDisconnect(true, () => {
  1049. socketConnected.value = false;
  1050. const _currentSong = currentSong.value;
  1051. if (nextSong.value)
  1052. setNextCurrentSong(
  1053. {
  1054. currentSong: nextSong.value,
  1055. startedAt: Date.now() + getTimeRemaining(),
  1056. paused: false,
  1057. timePaused: 0
  1058. },
  1059. true
  1060. );
  1061. else
  1062. setNextCurrentSong(
  1063. {
  1064. currentSong: null,
  1065. startedAt: 0,
  1066. paused: false,
  1067. timePaused: 0,
  1068. pausedAt: 0
  1069. },
  1070. true
  1071. );
  1072. window.stationNextSongTimeout = setTimeout(() => {
  1073. if (!noSong.value && currentSong.value._id === _currentSong._id)
  1074. skipSong();
  1075. }, getTimeRemaining());
  1076. });
  1077. frontendDevMode.value = await lofig.get("mode");
  1078. mediasession.value = await lofig.get("siteSettings.mediasession");
  1079. christmas.value = await lofig.get("siteSettings.christmas");
  1080. sitename.value = await lofig.get("siteSettings.sitename");
  1081. socket.dispatch("stations.existsByName", stationIdentifier.value, res => {
  1082. if (res.status === "error" || !res.data.exists) {
  1083. // station identifier may be using stationid instead
  1084. socket.dispatch(
  1085. "stations.existsById",
  1086. stationIdentifier.value,
  1087. res => {
  1088. if (res.status === "error" || !res.data.exists) {
  1089. loading.value = false;
  1090. exists.value = false;
  1091. }
  1092. }
  1093. );
  1094. }
  1095. });
  1096. ms.setListeners(0, {
  1097. play: () => {
  1098. if (hasPermission("stations.playback.toggle")) resumeStation();
  1099. else resumeLocalStation();
  1100. },
  1101. pause: () => {
  1102. if (hasPermission("stations.playback.toggle")) pauseStation();
  1103. else pauseLocalStation();
  1104. },
  1105. nexttrack: () => {
  1106. if (hasPermission("stations.skip")) skipStation();
  1107. else if (!currentSong.value.voted) toggleSkipVote();
  1108. }
  1109. });
  1110. socket.on("event:station.nextSong", res => {
  1111. const { currentSong, startedAt, paused, timePaused } = res.data;
  1112. setCurrentSong({
  1113. currentSong,
  1114. startedAt,
  1115. paused,
  1116. timePaused,
  1117. pausedAt: 0
  1118. });
  1119. });
  1120. socket.on("event:station.pause", res => {
  1121. pausedAt.value = res.data.pausedAt;
  1122. updateStationPaused(true);
  1123. pauseLocalPlayer();
  1124. clearTimeout(window.stationNextSongTimeout);
  1125. });
  1126. socket.on("event:station.resume", res => {
  1127. timePaused.value = res.data.timePaused;
  1128. updateStationPaused(false);
  1129. if (!localPaused.value) resumeLocalPlayer();
  1130. autoRequestSong();
  1131. });
  1132. socket.on("event:station.deleted", () => {
  1133. router.push({
  1134. path: "/",
  1135. query: {
  1136. toast: "The station you were in was deleted."
  1137. }
  1138. });
  1139. });
  1140. socket.on("event:ratings.liked", res => {
  1141. if (!noSong.value) {
  1142. if (res.data.youtubeId === currentSong.value.youtubeId) {
  1143. updateCurrentSongRatings(res.data);
  1144. }
  1145. }
  1146. });
  1147. socket.on("event:ratings.disliked", res => {
  1148. if (!noSong.value) {
  1149. if (res.data.youtubeId === currentSong.value.youtubeId) {
  1150. updateCurrentSongRatings(res.data);
  1151. }
  1152. }
  1153. });
  1154. socket.on("event:ratings.unliked", res => {
  1155. if (!noSong.value) {
  1156. if (res.data.youtubeId === currentSong.value.youtubeId) {
  1157. updateCurrentSongRatings(res.data);
  1158. }
  1159. }
  1160. });
  1161. socket.on("event:ratings.undisliked", res => {
  1162. if (!noSong.value) {
  1163. if (res.data.youtubeId === currentSong.value.youtubeId) {
  1164. updateCurrentSongRatings(res.data);
  1165. }
  1166. }
  1167. });
  1168. socket.on("event:ratings.updated", res => {
  1169. if (!noSong.value) {
  1170. if (res.data.youtubeId === currentSong.value.youtubeId) {
  1171. updateOwnCurrentSongRatings(res.data);
  1172. }
  1173. }
  1174. });
  1175. socket.on("event:station.queue.updated", res => {
  1176. updateSongsList(res.data.queue);
  1177. let nextSong = null;
  1178. if (songsList.value[0])
  1179. nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
  1180. updateNextSong(nextSong);
  1181. autoRequestSong();
  1182. });
  1183. socket.on("event:station.queue.song.repositioned", res => {
  1184. repositionSongInList(res.data.song);
  1185. let nextSong = null;
  1186. if (songsList.value[0])
  1187. nextSong = songsList.value[0].youtubeId ? songsList.value[0] : null;
  1188. updateNextSong(nextSong);
  1189. });
  1190. socket.on("event:station.toggleSkipVote", res => {
  1191. if (currentSong.value)
  1192. updateCurrentSongSkipVotes({
  1193. skipVotes: res.data.voted
  1194. ? currentSong.value.skipVotes + 1
  1195. : currentSong.value.skipVotes - 1,
  1196. skipVotesCurrent: null,
  1197. voted:
  1198. res.data.userId === userId.value
  1199. ? res.data.voted
  1200. : currentSong.value.voted
  1201. });
  1202. });
  1203. socket.on("event:station.updated", async res => {
  1204. const { name, theme, privacy } = res.data.station;
  1205. if (!hasPermission("stations.view") && privacy === "private") {
  1206. router.push({
  1207. path: "/",
  1208. query: {
  1209. toast: "The station you were in was made private."
  1210. }
  1211. });
  1212. } else {
  1213. if (station.value.name !== name) {
  1214. await router.push(
  1215. `${name}?${Object.keys(route.query)
  1216. .map(
  1217. key =>
  1218. `${encodeURIComponent(
  1219. key
  1220. )}=${encodeURIComponent(
  1221. JSON.stringify(route.query[key])
  1222. )}`
  1223. )
  1224. .join("&")}`
  1225. );
  1226. // eslint-disable-next-line no-restricted-globals
  1227. history.replaceState({ ...history.state, ...{} }, null);
  1228. }
  1229. if (station.value.theme !== theme)
  1230. document.getElementsByTagName(
  1231. "html"
  1232. )[0].style.cssText = `--primary-color: var(--${theme})`;
  1233. updateStation(res.data.station);
  1234. }
  1235. });
  1236. socket.on("event:station.users.updated", res =>
  1237. updateUsers(res.data.users)
  1238. );
  1239. socket.on("event:station.userCount.updated", res =>
  1240. updateUserCount(res.data.userCount)
  1241. );
  1242. socket.on("event:user.station.favorited", res => {
  1243. if (res.data.stationId === station.value._id)
  1244. updateIfStationIsFavorited({ isFavorited: true });
  1245. });
  1246. socket.on("event:user.station.unfavorited", res => {
  1247. if (res.data.stationId === station.value._id)
  1248. updateIfStationIsFavorited({ isFavorited: false });
  1249. });
  1250. socket.on("event:station.djs.added", res => {
  1251. if (res.data.user._id === userId.value)
  1252. updatePermissions().then(() => {
  1253. if (
  1254. !hasPermission("stations.view") &&
  1255. station.value.privacy === "private"
  1256. )
  1257. router.push({
  1258. path: "/",
  1259. query: {
  1260. toast: "You no longer have access to the station you were in."
  1261. }
  1262. });
  1263. });
  1264. addDj(res.data.user);
  1265. });
  1266. socket.on("event:station.djs.removed", res => {
  1267. if (res.data.user._id === userId.value)
  1268. updatePermissions().then(() => {
  1269. if (
  1270. !hasPermission("stations.view") &&
  1271. station.value.privacy === "private"
  1272. )
  1273. router.push({
  1274. path: "/",
  1275. query: {
  1276. toast: "You no longer have access to the station you were in."
  1277. }
  1278. });
  1279. });
  1280. removeDj(res.data.user);
  1281. });
  1282. socket.on("keep.event:user.role.updated", () => {
  1283. updatePermissions().then(() => {
  1284. if (
  1285. !hasPermission("stations.view") &&
  1286. station.value.privacy === "private"
  1287. )
  1288. router.push({
  1289. path: "/",
  1290. query: {
  1291. toast: "You no longer have access to the station you were in."
  1292. }
  1293. });
  1294. });
  1295. });
  1296. if (JSON.parse(localStorage.getItem("muted"))) {
  1297. muted.value = true;
  1298. player.value.setVolume(0);
  1299. volumeSliderValue.value = 0;
  1300. } else {
  1301. let volume = parseFloat(localStorage.getItem("volume"));
  1302. volume =
  1303. typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
  1304. localStorage.setItem("volume", `${volume}`);
  1305. volumeSliderValue.value = volume;
  1306. }
  1307. });
  1308. onBeforeUnmount(() => {
  1309. document.getElementsByTagName("html")[0].style.cssText = "";
  1310. if (mediasession.value) {
  1311. ms.removeListeners(0);
  1312. ms.removeMediaSessionData(0);
  1313. }
  1314. /** Reset Songslist */
  1315. updateSongsList([]);
  1316. const shortcutNames = [
  1317. "station.pauseResume",
  1318. "station.skipStation",
  1319. "station.lowerVolumeLarge",
  1320. "station.lowerVolumeSmall",
  1321. "station.increaseVolumeLarge",
  1322. "station.increaseVolumeSmall",
  1323. "station.toggleDebug"
  1324. ];
  1325. shortcutNames.forEach(shortcutName => {
  1326. keyboardShortcuts.unregisterShortcut(shortcutName);
  1327. });
  1328. mediaModalWatcher.value(); // removes the watcher
  1329. clearInterval(activityWatchVideoDataInterval.value);
  1330. clearTimeout(window.stationNextSongTimeout);
  1331. clearTimeout(persistentToastCheckerInterval.value);
  1332. persistentToasts.value.forEach(persistentToast => {
  1333. persistentToast.toast.destroy();
  1334. });
  1335. socket.dispatch("stations.leave", station.value._id, () => {});
  1336. leaveStation();
  1337. });
  1338. </script>
  1339. <template>
  1340. <div>
  1341. <page-metadata
  1342. v-if="exists && !loading"
  1343. :title="`${station.displayName}`"
  1344. />
  1345. <page-metadata v-else-if="!exists && !loading" :title="`Not found`" />
  1346. <div id="page-loader-container" v-if="loading">
  1347. <content-loader
  1348. width="1920"
  1349. height="1080"
  1350. :primary-color="nightmode ? '#222' : '#fff'"
  1351. :secondary-color="nightmode ? '#444' : '#ddd'"
  1352. preserve-aspect-ratio="none"
  1353. id="page-loader-content"
  1354. >
  1355. <rect x="55" y="105" rx="5" ry="5" width="670" height="149" />
  1356. <rect x="55" y="283" rx="5" ry="5" width="670" height="640" />
  1357. <rect x="745" y="108" rx="5" ry="5" width="1120" height="672" />
  1358. <rect x="745" y="810" rx="5" ry="5" width="1120" height="110" />
  1359. </content-loader>
  1360. <content-loader
  1361. width="1920"
  1362. height="1080"
  1363. :primary-color="nightmode ? '#222' : '#fff'"
  1364. :secondary-color="nightmode ? '#444' : '#ddd'"
  1365. preserve-aspect-ratio="none"
  1366. id="page-loader-layout"
  1367. >
  1368. <rect x="0" y="0" rx="0" ry="0" width="1920" height="64" />
  1369. <rect x="0" y="980" rx="0" ry="0" width="1920" height="100" />
  1370. </content-loader>
  1371. </div>
  1372. <!-- More simplistic loading animation for mobile users -->
  1373. <div v-show="loading" id="mobile-progress-animation" />
  1374. <ul
  1375. v-if="
  1376. currentSong &&
  1377. (currentSong.youtubeId === 'l9PxOanFjxQ' ||
  1378. currentSong.youtubeId === 'xKVcVSYmesU' ||
  1379. currentSong.youtubeId === '60ItHLz5WEA' ||
  1380. currentSong.youtubeId === 'e6vkFbtSGm0')
  1381. "
  1382. class="bg-bubbles"
  1383. >
  1384. <li></li>
  1385. <li></li>
  1386. <li></li>
  1387. <li></li>
  1388. <li></li>
  1389. <li></li>
  1390. <li></li>
  1391. <li></li>
  1392. <li></li>
  1393. <li></li>
  1394. </ul>
  1395. <div v-show="!loading && exists">
  1396. <main-header />
  1397. <div id="station-outer-container">
  1398. <div
  1399. id="station-inner-container"
  1400. :class="{ 'nothing-here': noSong }"
  1401. >
  1402. <div id="station-left-column" class="column">
  1403. <!-- div with quadrant class -->
  1404. <div class="quadrant">
  1405. <station-info-box
  1406. :station="station"
  1407. :station-paused="stationPaused"
  1408. :show-manage-station="true"
  1409. />
  1410. </div>
  1411. <div id="sidebar-container" class="quadrant">
  1412. <station-sidebar />
  1413. </div>
  1414. </div>
  1415. <div id="station-right-column" class="column">
  1416. <div class="player-container quadrant" v-show="!noSong">
  1417. <div id="video-container">
  1418. <div
  1419. id="stationPlayer"
  1420. style="
  1421. width: 100%;
  1422. height: 100%;
  1423. min-height: 200px;
  1424. "
  1425. />
  1426. <div
  1427. class="player-cannot-autoplay"
  1428. v-if="!canAutoplay"
  1429. @click="increaseVolume()"
  1430. >
  1431. <p>
  1432. Please click anywhere on the screen for
  1433. the video to start
  1434. </p>
  1435. </div>
  1436. </div>
  1437. <div id="seeker-bar-container">
  1438. <div
  1439. id="seeker-bar"
  1440. :class="{
  1441. 'christmas-seeker': christmas,
  1442. nyan:
  1443. currentSong &&
  1444. currentSong.youtubeId ===
  1445. 'QH2-TGUlwu4'
  1446. }"
  1447. />
  1448. <div
  1449. class="seeker-bar-cover"
  1450. :style="{
  1451. width: `calc(100% - ${seekerbarPercentage}%)`
  1452. }"
  1453. ></div>
  1454. <img
  1455. v-if="
  1456. currentSong &&
  1457. currentSong.youtubeId === 'QH2-TGUlwu4'
  1458. "
  1459. src="https://freepngimg.com/thumb/nyan_cat/1-2-nyan-cat-free-download-png.png"
  1460. :style="{
  1461. position: 'absolute',
  1462. top: `-10px`,
  1463. left: `${seekerbarPercentage}%`,
  1464. width: '50px'
  1465. }"
  1466. />
  1467. <img
  1468. v-if="
  1469. currentSong &&
  1470. (currentSong.youtubeId ===
  1471. 'DtVBCG6ThDk' ||
  1472. currentSong.youtubeId ===
  1473. 'sI66hcu9fIs' ||
  1474. currentSong.youtubeId ===
  1475. 'iYYRH4apXDo' ||
  1476. currentSong.youtubeId ===
  1477. 'tRcPA7Fzebw')
  1478. "
  1479. src="/assets/rocket.svg"
  1480. :style="{
  1481. position: 'absolute',
  1482. top: `-21px`,
  1483. left: `calc(${seekerbarPercentage}% - 35px)`,
  1484. width: '50px',
  1485. transform: 'rotate(45deg)'
  1486. }"
  1487. />
  1488. <img
  1489. v-if="
  1490. currentSong &&
  1491. currentSong.youtubeId === 'jofNR_WkoCE'
  1492. "
  1493. src="/assets/fox.svg"
  1494. :style="{
  1495. position: 'absolute',
  1496. top: `-21px`,
  1497. left: `calc(${seekerbarPercentage}% - 35px)`,
  1498. width: '50px',
  1499. transform: 'scaleX(-1)',
  1500. opacity: 1
  1501. }"
  1502. />
  1503. <img
  1504. v-if="
  1505. currentSong &&
  1506. (currentSong.youtubeId ===
  1507. 'l9PxOanFjxQ' ||
  1508. currentSong.youtubeId ===
  1509. 'xKVcVSYmesU' ||
  1510. currentSong.youtubeId ===
  1511. '60ItHLz5WEA' ||
  1512. currentSong.youtubeId ===
  1513. 'e6vkFbtSGm0')
  1514. "
  1515. src="/assets/old_logo.png"
  1516. :style="{
  1517. position: 'absolute',
  1518. top: `-9px`,
  1519. left: `calc(${seekerbarPercentage}% - 22px)`,
  1520. 'background-color': 'rgb(96, 199, 169)',
  1521. width: '25px',
  1522. height: '25px',
  1523. 'border-radius': '25px'
  1524. }"
  1525. />
  1526. <img
  1527. v-if="
  1528. christmas &&
  1529. currentSong &&
  1530. ![
  1531. 'QH2-TGUlwu4',
  1532. 'DtVBCG6ThDk',
  1533. 'sI66hcu9fIs',
  1534. 'iYYRH4apXDo',
  1535. 'tRcPA7Fzebw',
  1536. 'jofNR_WkoCE',
  1537. 'l9PxOanFjxQ',
  1538. 'xKVcVSYmesU',
  1539. '60ItHLz5WEA',
  1540. 'e6vkFbtSGm0'
  1541. ].includes(currentSong.youtubeId)
  1542. "
  1543. src="/assets/santa.png"
  1544. :style="{
  1545. position: 'absolute',
  1546. top: `-30px`,
  1547. left: `calc(${seekerbarPercentage}% - 25px)`,
  1548. height: '50px',
  1549. transform: 'scaleX(-1)'
  1550. }"
  1551. />
  1552. </div>
  1553. <div id="control-bar-container">
  1554. <div id="left-buttons">
  1555. <!-- Debug Box -->
  1556. <button
  1557. v-if="frontendDevMode === 'development'"
  1558. class="button is-primary"
  1559. @click="togglePlayerDebugBox()"
  1560. @dblclick="resetPlayerDebugBox()"
  1561. content="Debug"
  1562. v-tippy
  1563. >
  1564. <i
  1565. class="material-icons icon-with-button"
  1566. >
  1567. bug_report
  1568. </i>
  1569. </button>
  1570. <!-- Local Pause/Resume Button -->
  1571. <button
  1572. class="button is-primary"
  1573. @click="resumeLocalStation()"
  1574. id="local-resume"
  1575. v-if="localPaused"
  1576. content="Unpause Playback"
  1577. v-tippy
  1578. >
  1579. <i class="material-icons">play_arrow</i>
  1580. </button>
  1581. <button
  1582. class="button is-primary"
  1583. @click="pauseLocalStation()"
  1584. id="local-pause"
  1585. v-else
  1586. content="Pause Playback"
  1587. v-tippy
  1588. >
  1589. <i class="material-icons">pause</i>
  1590. </button>
  1591. <!-- Vote to Skip Button -->
  1592. <button
  1593. v-if="!skipVotesLoaded"
  1594. class="button is-primary disabled"
  1595. content="Skip votes have not been loaded yet"
  1596. v-tippy
  1597. >
  1598. <i
  1599. class="material-icons icon-with-button"
  1600. >skip_next</i
  1601. >
  1602. </button>
  1603. <button
  1604. v-else-if="loggedIn"
  1605. :class="[
  1606. 'button',
  1607. 'is-primary',
  1608. { voted: currentSong.voted }
  1609. ]"
  1610. @click="toggleSkipVote()"
  1611. :content="`${
  1612. currentSong.voted
  1613. ? 'Remove vote'
  1614. : 'Vote'
  1615. } to Skip Song`"
  1616. v-tippy
  1617. >
  1618. <i
  1619. class="material-icons icon-with-button"
  1620. >skip_next</i
  1621. >
  1622. {{ currentSong.skipVotes }}
  1623. </button>
  1624. <button
  1625. v-else
  1626. class="button is-primary disabled"
  1627. content="Log in to vote to skip songs"
  1628. v-tippy="{ theme: 'info' }"
  1629. >
  1630. <i
  1631. class="material-icons icon-with-button"
  1632. >skip_next</i
  1633. >
  1634. {{ currentSong.skipVotes }}
  1635. </button>
  1636. </div>
  1637. <div id="duration">
  1638. <p>
  1639. {{ timeElapsed }} /
  1640. {{
  1641. utils.formatTime(
  1642. currentSong.duration
  1643. )
  1644. }}
  1645. </p>
  1646. </div>
  1647. <p id="volume-control" v-if="!isApple">
  1648. <i
  1649. class="material-icons"
  1650. @click="toggleMute()"
  1651. :content="`${
  1652. muted ? 'Unmute' : 'Mute'
  1653. }`"
  1654. v-tippy
  1655. >{{
  1656. muted
  1657. ? "volume_mute"
  1658. : volumeSliderValue >= 50
  1659. ? "volume_up"
  1660. : "volume_down"
  1661. }}</i
  1662. >
  1663. <input
  1664. v-model="volumeSliderValue"
  1665. type="range"
  1666. min="0"
  1667. max="100"
  1668. class="volume-slider active"
  1669. @change="changeVolume()"
  1670. @input="changeVolume()"
  1671. />
  1672. </p>
  1673. <div id="right-buttons" v-if="loggedIn">
  1674. <!-- Ratings (Like/Dislike) Buttons -->
  1675. <div
  1676. id="ratings"
  1677. v-if="ratingsLoaded && ownRatingsLoaded"
  1678. :class="{
  1679. liked: currentSong.liked,
  1680. disliked: currentSong.disliked
  1681. }"
  1682. >
  1683. <!-- Like Song Button -->
  1684. <button
  1685. class="button is-success like-song"
  1686. id="like-song"
  1687. @click="toggleLike()"
  1688. content="Like Song"
  1689. v-tippy
  1690. >
  1691. <i
  1692. class="material-icons icon-with-button"
  1693. :class="{
  1694. liked: currentSong.liked
  1695. }"
  1696. >thumb_up_alt</i
  1697. >{{ currentSong.likes }}
  1698. </button>
  1699. <!-- Dislike Song Button -->
  1700. <button
  1701. class="button is-danger dislike-song"
  1702. id="dislike-song"
  1703. @click="toggleDislike()"
  1704. content="Dislike Song"
  1705. v-tippy
  1706. >
  1707. <i
  1708. class="material-icons icon-with-button"
  1709. :class="{
  1710. disliked:
  1711. currentSong.disliked
  1712. }"
  1713. >thumb_down_alt</i
  1714. >{{ currentSong.dislikes }}
  1715. </button>
  1716. </div>
  1717. <div id="ratings" class="disabled" v-else>
  1718. <!-- Like Song Button -->
  1719. <button
  1720. class="button is-success like-song disabled"
  1721. id="like-song"
  1722. content="Ratings have not been loaded yet"
  1723. v-tippy
  1724. >
  1725. <i
  1726. class="material-icons icon-with-button"
  1727. >thumb_up_alt</i
  1728. >
  1729. </button>
  1730. <!-- Dislike Song Button -->
  1731. <button
  1732. class="button is-danger dislike-song disabled"
  1733. id="dislike-song"
  1734. content="Ratings have not been loaded yet"
  1735. v-tippy
  1736. >
  1737. <i
  1738. class="material-icons icon-with-button"
  1739. >thumb_down_alt</i
  1740. >
  1741. </button>
  1742. </div>
  1743. <!-- Add Song To Playlist Button & Dropdown -->
  1744. <add-to-playlist-dropdown
  1745. :song="currentSong"
  1746. placement="top-end"
  1747. >
  1748. <template #button>
  1749. <div
  1750. id="add-song-to-playlist"
  1751. content="Add Song to Playlist"
  1752. v-tippy
  1753. >
  1754. <div class="control has-addons">
  1755. <button
  1756. class="button is-primary"
  1757. >
  1758. <i
  1759. class="material-icons"
  1760. >
  1761. playlist_add
  1762. </i>
  1763. </button>
  1764. <button
  1765. class="button"
  1766. id="dropdown-toggle"
  1767. >
  1768. <i
  1769. class="material-icons"
  1770. >
  1771. {{
  1772. showPlaylistDropdown
  1773. ? "expand_more"
  1774. : "expand_less"
  1775. }}
  1776. </i>
  1777. </button>
  1778. </div>
  1779. </div>
  1780. </template>
  1781. </add-to-playlist-dropdown>
  1782. </div>
  1783. <div id="right-buttons" v-else>
  1784. <!-- Disabled Ratings (Like/Dislike) Buttons -->
  1785. <div id="ratings" v-if="ratingsLoaded">
  1786. <!-- Disabled Like Song Button -->
  1787. <button
  1788. class="button is-success disabled"
  1789. id="like-song"
  1790. content="Log in to like songs"
  1791. v-tippy="{ theme: 'info' }"
  1792. >
  1793. <i
  1794. class="material-icons icon-with-button"
  1795. >thumb_up_alt</i
  1796. >{{ currentSong.likes }}
  1797. </button>
  1798. <!-- Disabled Dislike Song Button -->
  1799. <button
  1800. class="button is-danger disabled"
  1801. id="dislike-song"
  1802. content="Log in to dislike songs"
  1803. v-tippy="{ theme: 'info' }"
  1804. >
  1805. <i
  1806. class="material-icons icon-with-button"
  1807. >thumb_down_alt</i
  1808. >{{ currentSong.dislikes }}
  1809. </button>
  1810. </div>
  1811. <div id="ratings" v-else>
  1812. <!-- Disabled Like Song Button -->
  1813. <button
  1814. class="button is-success disabled"
  1815. id="like-song"
  1816. content="Ratings have not been loaded yet"
  1817. v-tippy="{ theme: 'info' }"
  1818. >
  1819. <i
  1820. class="material-icons icon-with-button"
  1821. >thumb_up_alt</i
  1822. >
  1823. </button>
  1824. <!-- Disabled Dislike Song Button -->
  1825. <button
  1826. class="button is-danger disabled"
  1827. id="dislike-song"
  1828. content="Ratings have not been loaded yet"
  1829. v-tippy="{ theme: 'info' }"
  1830. >
  1831. <i
  1832. class="material-icons icon-with-button"
  1833. >thumb_down_alt</i
  1834. >
  1835. </button>
  1836. </div>
  1837. <!-- Disabled Add Song To Playlist Button & Dropdown -->
  1838. <div id="add-song-to-playlist">
  1839. <div class="control has-addons">
  1840. <button
  1841. class="button is-primary disabled"
  1842. content="Log in to add songs to playlist"
  1843. v-tippy="{ theme: 'info' }"
  1844. >
  1845. <i class="material-icons"
  1846. >queue</i
  1847. >
  1848. </button>
  1849. </div>
  1850. </div>
  1851. </div>
  1852. </div>
  1853. </div>
  1854. <p
  1855. class="player-container nothing-here-text"
  1856. v-if="noSong"
  1857. >
  1858. No song is currently playing
  1859. </p>
  1860. <div v-if="!noSong" id="current-next-row">
  1861. <div
  1862. id="currently-playing-container"
  1863. class="quadrant"
  1864. :class="{ 'no-currently-playing': noSong }"
  1865. >
  1866. <song-item
  1867. :key="`songItem-currentSong-${currentSong._id}`"
  1868. :song="currentSong"
  1869. :duration="false"
  1870. :requested-by="true"
  1871. header="Currently Playing.."
  1872. />
  1873. </div>
  1874. <div
  1875. v-if="nextSong"
  1876. id="next-up-container"
  1877. class="quadrant"
  1878. >
  1879. <song-item
  1880. :key="`songItem-nextSong-${nextSong._id}`"
  1881. :song="nextSong"
  1882. :duration="false"
  1883. :requested-by="true"
  1884. header="Next Up.."
  1885. />
  1886. </div>
  1887. </div>
  1888. </div>
  1889. </div>
  1890. </div>
  1891. <main-footer />
  1892. </div>
  1893. <floating-box
  1894. id="player-debug-box"
  1895. ref="playerDebugBox"
  1896. title="Station Debug"
  1897. >
  1898. <template #body>
  1899. <span><b>No song</b>: {{ noSong }}</span>
  1900. <span><b>Song id</b>: {{ currentSong._id }}</span>
  1901. <span><b>YouTube id</b>: {{ currentSong.youtubeId }}</span>
  1902. <span><b>Duration</b>: {{ currentSong.duration }}</span>
  1903. <span
  1904. ><b>Skip duration</b>: {{ currentSong.skipDuration }}</span
  1905. >
  1906. <span><b>Loading</b>: {{ loading }}</span>
  1907. <span><b>Can autoplay</b>: {{ canAutoplay }}</span>
  1908. <span><b>Player ready</b>: {{ playerReady }}</span>
  1909. <span
  1910. ><b>Attempts to play video</b>:
  1911. {{ attemptsToPlayVideo }}</span
  1912. >
  1913. <span
  1914. ><b>Last time requested if can autoplay</b>:
  1915. {{ lastTimeRequestedIfCanAutoplay }}</span
  1916. >
  1917. <span><b>Seeking</b>: {{ seeking }}</span>
  1918. <span><b>Playback rate</b>: {{ playbackRate }}</span>
  1919. <span><b>System difference</b>: {{ systemDifference }}</span>
  1920. <span><b>Time before paused</b>: {{ timeBeforePause }}</span>
  1921. <span><b>Time paused</b>: {{ timePaused }}</span>
  1922. <span><b>Time elapsed</b>: {{ timeElapsed }}</span>
  1923. <span><b>Volume slider value</b>: {{ volumeSliderValue }}</span>
  1924. <span><b>Local paused</b>: {{ localPaused }}</span>
  1925. <span><b>Station paused</b>: {{ stationPaused }}</span>
  1926. <span
  1927. ><b>Requests enabled</b>:
  1928. {{ station.requests.enabled }}</span
  1929. >
  1930. <span
  1931. ><b>Requests access</b>: {{ station.requests.access }}</span
  1932. >
  1933. <span><b>Requests limit</b>: {{ station.requests.limit }}</span>
  1934. <span
  1935. ><b>Auto requesting playlists</b>:
  1936. {{
  1937. autoRequest.map(playlist => playlist._id).join(", ")
  1938. }}</span
  1939. >
  1940. <span
  1941. ><b>Autofill enabled</b>:
  1942. {{ station.autofill.enabled }}</span
  1943. >
  1944. <span><b>Autofill limit</b>: {{ station.autofill.limit }}</span>
  1945. <span><b>Autofill mode</b>: {{ station.autofill.mode }}</span>
  1946. <span><b>Skip votes loaded</b>: {{ skipVotesLoaded }}</span>
  1947. <span
  1948. ><b>Skip votes current</b>:
  1949. {{
  1950. currentSong.skipVotesCurrent
  1951. ? currentSong.skipVotesCurrent
  1952. : "N/A"
  1953. }}</span
  1954. >
  1955. <span
  1956. ><b>Skip votes</b>:
  1957. {{ skipVotesLoaded ? currentSong.skipVotes : "N/A" }}</span
  1958. >
  1959. <span><b>Ratings loaded</b>: {{ ratingsLoaded }}</span>
  1960. <span
  1961. ><b>Ratings</b>:
  1962. {{
  1963. ratingsLoaded
  1964. ? `${currentSong.likes} / ${currentSong.dislikes}`
  1965. : "N/A"
  1966. }}</span
  1967. >
  1968. <span><b>Own ratings loaded</b>: {{ ownRatingsLoaded }}</span>
  1969. <span
  1970. ><b>Own ratings</b>:
  1971. {{
  1972. ownRatingsLoaded
  1973. ? `${currentSong.liked} / ${currentSong.disliked}`
  1974. : "N/A"
  1975. }}</span
  1976. >
  1977. </template>
  1978. </floating-box>
  1979. <floating-box
  1980. id="keyboardShortcutsHelper"
  1981. ref="keyboardShortcutsHelper"
  1982. title="Station Keyboard Shortcuts"
  1983. >
  1984. <template #body>
  1985. <div>
  1986. <div
  1987. v-if="
  1988. hasPermission('stations.playback.toggle') ||
  1989. hasPermission('stations.skip')
  1990. "
  1991. >
  1992. <span class="biggest"><b>Admin/owner</b></span>
  1993. <span><b>Ctrl + Space</b> - Pause/resume station</span>
  1994. <span><b>Ctrl + Numpad right</b> - Skip station</span>
  1995. </div>
  1996. <hr
  1997. v-if="
  1998. hasPermission('stations.playback.toggle') ||
  1999. hasPermission('stations.skip')
  2000. "
  2001. />
  2002. <div>
  2003. <span class="biggest"><b>Volume</b></span>
  2004. <span
  2005. ><b>Ctrl + Numpad up/down</b> - Volume up/down
  2006. 10%</span
  2007. >
  2008. <span
  2009. ><b>Ctrl + Shift + Numpad up/down</b> - Volume
  2010. up/down 10%</span
  2011. >
  2012. </div>
  2013. <hr />
  2014. <div>
  2015. <span class="biggest"><b>Misc</b></span>
  2016. <span><b>Ctrl + D</b> - Toggles debug box</span>
  2017. <span><b>Ctrl + Shift + D</b> - Resets debug box</span>
  2018. <span
  2019. ><b>Ctrl + /</b> - Toggles keyboard shortcuts
  2020. box</span
  2021. >
  2022. <span
  2023. ><b>Ctrl + Shift + /</b> - Resets keyboard shortcuts
  2024. box</span
  2025. >
  2026. </div>
  2027. </div>
  2028. </template>
  2029. </floating-box>
  2030. <Z404 v-if="!exists"></Z404>
  2031. </div>
  2032. </template>
  2033. <style lang="less">
  2034. #stationPlayer {
  2035. position: absolute;
  2036. top: 0;
  2037. left: 0;
  2038. width: 100%;
  2039. height: 100%;
  2040. }
  2041. #currently-playing-container,
  2042. #next-up-container {
  2043. .song-item {
  2044. .thumbnail {
  2045. min-width: 130px;
  2046. width: 130px;
  2047. height: 130px;
  2048. }
  2049. }
  2050. }
  2051. #control-bar-container
  2052. #right-buttons
  2053. .tippy-box[data-theme~="dropdown"]
  2054. .nav-dropdown-items {
  2055. padding-bottom: 0 !important;
  2056. }
  2057. </style>
  2058. <style lang="less" scoped>
  2059. #page-loader-container {
  2060. height: inherit;
  2061. #page-loader-content {
  2062. height: inherit;
  2063. position: absolute;
  2064. max-width: 100%;
  2065. width: 1800px;
  2066. transform: translateX(-50%);
  2067. left: 50%;
  2068. }
  2069. #page-loader-layout {
  2070. height: inherit;
  2071. width: 100%;
  2072. }
  2073. }
  2074. #mobile-progress-animation {
  2075. width: 50px;
  2076. animation: rotate 0.8s infinite linear;
  2077. border: 8px solid var(--primary-color);
  2078. border-right-color: transparent;
  2079. border-radius: 50%;
  2080. height: 50px;
  2081. position: absolute;
  2082. top: 50%;
  2083. left: 50%;
  2084. display: none;
  2085. }
  2086. @keyframes rotate {
  2087. 0% {
  2088. transform: rotate(0deg);
  2089. }
  2090. 100% {
  2091. transform: rotate(360deg);
  2092. }
  2093. }
  2094. .nav,
  2095. .button.is-primary {
  2096. background-color: var(--primary-color) !important;
  2097. }
  2098. .button.is-primary:hover,
  2099. .button.is-primary:focus {
  2100. filter: brightness(90%);
  2101. }
  2102. .night-mode {
  2103. #currently-playing-container,
  2104. #next-up-container,
  2105. #control-bar-container,
  2106. .quadrant:not(#sidebar-container),
  2107. .player-container {
  2108. background-color: var(--dark-grey-3) !important;
  2109. }
  2110. #video-container,
  2111. #control-bar-container,
  2112. .quadrant:not(#sidebar-container),
  2113. .player-container {
  2114. border: 0 !important;
  2115. }
  2116. #seeker-bar-container {
  2117. background-color: var(--dark-grey-3) !important;
  2118. }
  2119. #dropdown-toggle {
  2120. background-color: var(--dark-grey-2) !important;
  2121. border: 0;
  2122. i {
  2123. color: var(--white);
  2124. }
  2125. }
  2126. }
  2127. #station-outer-container {
  2128. margin: 0 auto;
  2129. padding: 20px 40px;
  2130. min-height: calc(100vh - 64px);
  2131. width: 100%;
  2132. max-width: 1800px;
  2133. display: flex;
  2134. #station-inner-container {
  2135. width: 100%;
  2136. min-height: calc(100vh - 428px);
  2137. display: flex;
  2138. flex-direction: row;
  2139. flex-wrap: wrap;
  2140. .row {
  2141. display: flex;
  2142. flex-direction: row;
  2143. max-width: 100%;
  2144. }
  2145. .column {
  2146. display: flex;
  2147. flex-direction: column;
  2148. }
  2149. .quadrant {
  2150. border-radius: @border-radius;
  2151. margin: 10px;
  2152. overflow: hidden;
  2153. }
  2154. .quadrant:not(#sidebar-container) {
  2155. background-color: var(--white);
  2156. border: 1px solid var(--light-grey-3);
  2157. }
  2158. #station-left-column,
  2159. #station-right-column {
  2160. padding: 0;
  2161. }
  2162. #current-next-row {
  2163. display: flex;
  2164. flex-direction: row;
  2165. #currently-playing-container,
  2166. #next-up-container {
  2167. overflow: hidden;
  2168. flex-basis: 50%;
  2169. .song-item {
  2170. border: unset;
  2171. }
  2172. .nothing-here-text {
  2173. height: 100%;
  2174. }
  2175. }
  2176. > div:only-child {
  2177. flex: 1 !important;
  2178. flex-basis: 100% !important;
  2179. }
  2180. }
  2181. .player-container {
  2182. height: inherit;
  2183. background-color: var(--white);
  2184. display: flex;
  2185. flex-direction: column;
  2186. border: 1px solid var(--light-grey-3);
  2187. border-radius: @border-radius;
  2188. overflow: hidden;
  2189. &.nothing-here-text {
  2190. margin: 10px;
  2191. flex: 1;
  2192. min-height: 487px;
  2193. }
  2194. #video-container {
  2195. position: relative;
  2196. padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
  2197. height: 0;
  2198. overflow: hidden;
  2199. .player-cannot-autoplay {
  2200. position: relative;
  2201. width: 100%;
  2202. height: 100%;
  2203. bottom: calc(100% + 5px);
  2204. background: var(--primary-color);
  2205. display: flex;
  2206. align-items: center;
  2207. justify-content: center;
  2208. p {
  2209. color: var(--white);
  2210. font-size: 26px;
  2211. text-align: center;
  2212. }
  2213. }
  2214. }
  2215. #seeker-bar-container {
  2216. background-color: var(--white);
  2217. position: relative;
  2218. height: 7px;
  2219. display: block;
  2220. width: 100%;
  2221. #seeker-bar {
  2222. background-color: var(--primary-color);
  2223. top: 0;
  2224. left: 0;
  2225. bottom: 0;
  2226. position: absolute;
  2227. width: 100%;
  2228. }
  2229. .seeker-bar-cover {
  2230. position: absolute;
  2231. top: 0;
  2232. right: 0;
  2233. bottom: 0;
  2234. background-color: inherit;
  2235. }
  2236. }
  2237. #control-bar-container {
  2238. display: flex;
  2239. justify-content: space-around;
  2240. padding: 10px 0;
  2241. width: 100%;
  2242. background: var(--white);
  2243. flex-direction: column;
  2244. flex-flow: wrap;
  2245. .button:not(#dropdown-toggle) {
  2246. width: 75px;
  2247. }
  2248. #left-buttons,
  2249. #right-buttons {
  2250. margin: 3px;
  2251. }
  2252. #left-buttons {
  2253. display: flex;
  2254. .button:not(:first-of-type) {
  2255. margin-left: 5px;
  2256. }
  2257. .disabled {
  2258. filter: grayscale(0.4);
  2259. }
  2260. }
  2261. #duration {
  2262. margin: 3px;
  2263. display: flex;
  2264. align-items: center;
  2265. p {
  2266. font-size: 22px;
  2267. /** prevents duration width slightly varying and shifting other controls slightly */
  2268. width: 150px;
  2269. text-align: center;
  2270. }
  2271. }
  2272. #volume-control {
  2273. margin: 3px;
  2274. margin-top: 0;
  2275. display: flex;
  2276. align-items: center;
  2277. cursor: pointer;
  2278. .volume-slider {
  2279. width: 100%;
  2280. padding: 0 15px;
  2281. background: transparent;
  2282. min-width: 100px;
  2283. }
  2284. input[type="range"] {
  2285. -webkit-appearance: none;
  2286. margin: 7.3px 0;
  2287. }
  2288. input[type="range"]:focus {
  2289. outline: none;
  2290. }
  2291. input[type="range"]::-webkit-slider-runnable-track {
  2292. width: 100%;
  2293. height: 5.2px;
  2294. cursor: pointer;
  2295. box-shadow: 0;
  2296. background: var(--light-grey-3);
  2297. border-radius: @border-radius;
  2298. border: 0;
  2299. }
  2300. input[type="range"]::-webkit-slider-thumb {
  2301. box-shadow: 0;
  2302. border: 0;
  2303. height: 19px;
  2304. width: 19px;
  2305. border-radius: 100%;
  2306. background: var(--primary-color);
  2307. cursor: pointer;
  2308. -webkit-appearance: none;
  2309. margin-top: -6.5px;
  2310. }
  2311. input[type="range"]::-moz-range-track {
  2312. width: 100%;
  2313. height: 5.2px;
  2314. cursor: pointer;
  2315. box-shadow: 0;
  2316. background: var(--light-grey-3);
  2317. border-radius: @border-radius;
  2318. border: 0;
  2319. }
  2320. input[type="range"]::-moz-range-thumb {
  2321. box-shadow: 0;
  2322. border: 0;
  2323. height: 19px;
  2324. width: 19px;
  2325. border-radius: 100%;
  2326. background: var(--primary-color);
  2327. cursor: pointer;
  2328. -webkit-appearance: none;
  2329. margin-top: -6.5px;
  2330. }
  2331. input[type="range"]::-ms-track {
  2332. width: 100%;
  2333. height: 5.2px;
  2334. cursor: pointer;
  2335. box-shadow: 0;
  2336. background: var(--light-grey-3);
  2337. border-radius: @border-radius;
  2338. }
  2339. input[type="range"]::-ms-fill-lower {
  2340. background: var(--light-grey-3);
  2341. border: 0;
  2342. border-radius: 0;
  2343. box-shadow: 0;
  2344. }
  2345. input[type="range"]::-ms-fill-upper {
  2346. background: var(--light-grey-3);
  2347. border: 0;
  2348. border-radius: 0;
  2349. box-shadow: 0;
  2350. }
  2351. input[type="range"]::-ms-thumb {
  2352. box-shadow: 0;
  2353. border: 0;
  2354. height: 15px;
  2355. width: 15px;
  2356. border-radius: 100%;
  2357. background: var(--primary-color);
  2358. cursor: pointer;
  2359. -webkit-appearance: none;
  2360. margin-top: 1.5px;
  2361. }
  2362. }
  2363. #right-buttons {
  2364. display: flex;
  2365. #dropdown-toggle {
  2366. width: 35px;
  2367. }
  2368. #dislike-song,
  2369. #add-song-to-playlist .button:not(#dropdown-toggle) {
  2370. margin-left: 5px;
  2371. }
  2372. #ratings {
  2373. display: flex;
  2374. &.liked #dislike-song,
  2375. &.disliked #like-song {
  2376. background-color: var(--grey) !important;
  2377. }
  2378. #like-song.disabled,
  2379. #dislike-song.disabled {
  2380. filter: grayscale(0.4);
  2381. }
  2382. }
  2383. #add-song-to-playlist {
  2384. display: flex;
  2385. flex-direction: column-reverse;
  2386. #nav-dropdown {
  2387. position: absolute;
  2388. margin-left: 4px;
  2389. margin-bottom: 36px;
  2390. .nav-dropdown-items {
  2391. position: relative;
  2392. right: calc(100% - 110px);
  2393. }
  2394. }
  2395. .control {
  2396. width: fit-content;
  2397. margin-bottom: 0 !important;
  2398. button.disabled {
  2399. filter: grayscale(0.4);
  2400. border-radius: @border-radius;
  2401. &::after {
  2402. margin-right: 100%;
  2403. }
  2404. }
  2405. }
  2406. }
  2407. }
  2408. }
  2409. }
  2410. #sidebar-container {
  2411. border-top: 0;
  2412. position: relative;
  2413. height: inherit;
  2414. flex-grow: 1;
  2415. min-height: 350px;
  2416. }
  2417. }
  2418. }
  2419. .footer {
  2420. margin-top: 30px;
  2421. }
  2422. .nyan {
  2423. background: linear-gradient(
  2424. 90deg,
  2425. magenta 0%,
  2426. red 15%,
  2427. orange 30%,
  2428. yellow 45%,
  2429. lime 60%,
  2430. cyan 75%,
  2431. blue 90%,
  2432. magenta 100%
  2433. );
  2434. background-size: 200%;
  2435. animation: nyanMoving 4s linear infinite;
  2436. }
  2437. @keyframes nyanMoving {
  2438. 0% {
  2439. background-position: 0% 0%;
  2440. }
  2441. 100% {
  2442. background-position: -200% 0%;
  2443. }
  2444. }
  2445. .christmas-seeker {
  2446. background: repeating-linear-gradient(
  2447. -45deg,
  2448. var(--white) 0 1rem,
  2449. var(--dark-red) 1rem 2rem
  2450. );
  2451. background-size: 200% 200%;
  2452. animation: christmas 20s linear infinite;
  2453. }
  2454. @keyframes christmas {
  2455. 100% {
  2456. background-position: 80% 100%;
  2457. }
  2458. }
  2459. .bg-bubbles {
  2460. top: 0;
  2461. left: 0;
  2462. width: 100%;
  2463. height: 100%;
  2464. position: absolute;
  2465. z-index: -1;
  2466. margin: 0px;
  2467. pointer-events: none;
  2468. }
  2469. .bg-bubbles li {
  2470. position: absolute;
  2471. list-style: none;
  2472. display: block;
  2473. width: 40px;
  2474. height: 40px;
  2475. border-radius: 100px;
  2476. background-color: var(--primary-color);
  2477. opacity: 0.15;
  2478. bottom: 0px;
  2479. -webkit-animation: square 25s infinite;
  2480. animation: square 25s infinite;
  2481. -webkit-transition-timing-function: linear;
  2482. transition-timing-function: linear;
  2483. }
  2484. .bg-bubbles li:nth-child(1) {
  2485. left: 10%;
  2486. }
  2487. .bg-bubbles li:nth-child(2) {
  2488. left: 20%;
  2489. width: 80px;
  2490. height: 80px;
  2491. -webkit-animation-delay: 2s;
  2492. animation-delay: 2s;
  2493. -webkit-animation-duration: 17s;
  2494. animation-duration: 17s;
  2495. }
  2496. .bg-bubbles li:nth-child(3) {
  2497. left: 25%;
  2498. -webkit-animation-delay: 4s;
  2499. animation-delay: 4s;
  2500. }
  2501. .bg-bubbles li:nth-child(4) {
  2502. left: 40%;
  2503. width: 60px;
  2504. height: 60px;
  2505. -webkit-animation-duration: 22s;
  2506. animation-duration: 22s;
  2507. background-color: var(--primary-color);
  2508. opacity: 0.25;
  2509. }
  2510. .bg-bubbles li:nth-child(5) {
  2511. left: 70%;
  2512. }
  2513. .bg-bubbles li:nth-child(6) {
  2514. left: 80%;
  2515. width: 120px;
  2516. height: 120px;
  2517. -webkit-animation-delay: 3s;
  2518. animation-delay: 3s;
  2519. background-color: var(--primary-color);
  2520. opacity: 0.2;
  2521. }
  2522. .bg-bubbles li:nth-child(7) {
  2523. left: 32%;
  2524. width: 160px;
  2525. height: 160px;
  2526. -webkit-animation-delay: 7s;
  2527. animation-delay: 7s;
  2528. }
  2529. .bg-bubbles li:nth-child(8) {
  2530. left: 55%;
  2531. width: 20px;
  2532. height: 20px;
  2533. -webkit-animation-delay: 15s;
  2534. animation-delay: 15s;
  2535. -webkit-animation-duration: 40s;
  2536. animation-duration: 40s;
  2537. }
  2538. .bg-bubbles li:nth-child(9) {
  2539. left: 25%;
  2540. width: 10px;
  2541. height: 10px;
  2542. -webkit-animation-delay: 2s;
  2543. animation-delay: 2s;
  2544. -webkit-animation-duration: 40s;
  2545. animation-duration: 40s;
  2546. background-color: var(--primary-color);
  2547. opacity: 0.3;
  2548. }
  2549. .bg-bubbles li:nth-child(10) {
  2550. left: 80%;
  2551. width: 160px;
  2552. height: 160px;
  2553. -webkit-animation-delay: 11s;
  2554. animation-delay: 11s;
  2555. }
  2556. /* Tablet view fix */
  2557. @media (max-width: 768px) {
  2558. .bg-bubbles li:nth-child(10) {
  2559. display: none;
  2560. }
  2561. }
  2562. @-webkit-keyframes square {
  2563. 0% {
  2564. -webkit-transform: translateY(0);
  2565. transform: translateY(0);
  2566. }
  2567. 100% {
  2568. -webkit-transform: translateY(-700px) rotate(600deg);
  2569. transform: translateY(-700px) rotate(600deg);
  2570. }
  2571. }
  2572. @keyframes square {
  2573. 0% {
  2574. -webkit-transform: translateY(0);
  2575. transform: translateY(0);
  2576. }
  2577. 100% {
  2578. -webkit-transform: translateY(-700px) rotate(600deg);
  2579. transform: translateY(-700px) rotate(600deg);
  2580. }
  2581. }
  2582. :deep(.nothing-here-text) {
  2583. display: flex;
  2584. align-items: center;
  2585. justify-content: center;
  2586. }
  2587. @media (min-width: 1500px) {
  2588. #station-left-column {
  2589. max-width: 650px;
  2590. }
  2591. #station-right-column {
  2592. max-width: calc(100% - 650px);
  2593. }
  2594. }
  2595. @media (max-width: 1700px) {
  2596. #current-next-row {
  2597. flex-direction: column !important;
  2598. > div {
  2599. flex: 1 !important;
  2600. }
  2601. }
  2602. }
  2603. @media (max-width: 1500px) {
  2604. #mobile-progress-animation {
  2605. display: block;
  2606. }
  2607. #page-loader-container {
  2608. display: none;
  2609. }
  2610. #station-outer-container {
  2611. max-width: 1500px;
  2612. #station-inner-container {
  2613. flex-direction: row;
  2614. #station-left-column {
  2615. #about-station-container #admin-buttons {
  2616. flex-wrap: wrap;
  2617. }
  2618. #sidebar-container {
  2619. min-height: 350px;
  2620. }
  2621. }
  2622. #station-right-column {
  2623. #current-next-row {
  2624. flex-direction: column;
  2625. }
  2626. #control-bar-container {
  2627. #duration,
  2628. #volume-control,
  2629. #right-buttons,
  2630. #left-buttons {
  2631. margin-bottom: 5px;
  2632. justify-content: center;
  2633. }
  2634. #duration {
  2635. order: 1;
  2636. }
  2637. #volume-control {
  2638. order: 2;
  2639. max-width: 400px;
  2640. }
  2641. #right-buttons {
  2642. order: 3;
  2643. flex-wrap: wrap;
  2644. #ratings {
  2645. flex-wrap: wrap;
  2646. }
  2647. }
  2648. #left-buttons {
  2649. order: 4;
  2650. flex-wrap: wrap;
  2651. }
  2652. }
  2653. }
  2654. }
  2655. }
  2656. }
  2657. @media (max-width: 1200px) {
  2658. #station-outer-container {
  2659. max-width: 900px;
  2660. padding: 0;
  2661. #station-inner-container {
  2662. flex-direction: column-reverse;
  2663. flex-wrap: nowrap;
  2664. }
  2665. }
  2666. }
  2667. @media (max-width: 990px) {
  2668. #station-outer-container {
  2669. min-height: calc(
  2670. 100vh - 256px
  2671. ); // Height of nav (64px) + height of footer (190px)
  2672. }
  2673. }
  2674. </style>