index.vue 89 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540254125422543254425452546254725482549255025512552255325542555255625572558255925602561256225632564256525662567256825692570257125722573257425752576257725782579258025812582258325842585258625872588258925902591259225932594259525962597259825992600260126022603260426052606260726082609261026112612261326142615261626172618261926202621262226232624262526262627262826292630263126322633263426352636263726382639264026412642264326442645264626472648264926502651265226532654265526562657265826592660266126622663266426652666266726682669267026712672267326742675267626772678267926802681268226832684268526862687268826892690269126922693269426952696269726982699270027012702270327042705270627072708270927102711271227132714271527162717271827192720272127222723272427252726272727282729273027312732273327342735273627372738273927402741274227432744274527462747274827492750275127522753275427552756275727582759276027612762276327642765276627672768276927702771277227732774277527762777277827792780278127822783278427852786278727882789279027912792279327942795279627972798279928002801280228032804280528062807280828092810281128122813281428152816281728182819282028212822282328242825282628272828282928302831283228332834283528362837283828392840284128422843284428452846284728482849285028512852285328542855285628572858285928602861286228632864286528662867286828692870287128722873287428752876287728782879288028812882288328842885288628872888288928902891289228932894289528962897289828992900290129022903290429052906290729082909291029112912291329142915291629172918291929202921292229232924292529262927292829292930293129322933293429352936293729382939294029412942294329442945294629472948294929502951295229532954295529562957295829592960296129622963296429652966296729682969297029712972297329742975297629772978297929802981298229832984298529862987298829892990299129922993299429952996299729982999300030013002300330043005300630073008300930103011301230133014301530163017301830193020302130223023302430253026302730283029303030313032303330343035303630373038303930403041304230433044304530463047304830493050305130523053305430553056305730583059306030613062306330643065306630673068306930703071307230733074307530763077307830793080308130823083308430853086308730883089309030913092309330943095309630973098309931003101310231033104310531063107310831093110311131123113311431153116311731183119312031213122312331243125312631273128312931303131313231333134313531363137313831393140314131423143314431453146314731483149315031513152315331543155315631573158315931603161316231633164316531663167316831693170317131723173317431753176317731783179318031813182318331843185318631873188318931903191319231933194319531963197319831993200320132023203320432053206320732083209321032113212321332143215321632173218321932203221322232233224322532263227322832293230323132323233323432353236323732383239324032413242324332443245324632473248324932503251325232533254325532563257325832593260326132623263326432653266326732683269327032713272327332743275327632773278327932803281328232833284328532863287328832893290329132923293329432953296329732983299330033013302330333043305330633073308330933103311331233133314331533163317331833193320332133223323332433253326332733283329333033313332333333343335333633373338333933403341334233433344334533463347334833493350335133523353335433553356335733583359336033613362336333643365336633673368336933703371337233733374337533763377337833793380338133823383338433853386338733883389339033913392339333943395339633973398339934003401340234033404340534063407340834093410341134123413341434153416341734183419342034213422342334243425342634273428342934303431343234333434343534363437343834393440344134423443344434453446344734483449345034513452345334543455345634573458345934603461346234633464346534663467346834693470347134723473347434753476347734783479348034813482348334843485348634873488348934903491349234933494349534963497349834993500350135023503350435053506350735083509351035113512351335143515351635173518351935203521352235233524352535263527352835293530353135323533353435353536353735383539354035413542354335443545354635473548354935503551355235533554355535563557355835593560356135623563356435653566356735683569357035713572357335743575357635773578357935803581358235833584358535863587358835893590359135923593359435953596359735983599360036013602360336043605360636073608360936103611361236133614361536163617361836193620362136223623362436253626362736283629363036313632363336343635363636373638363936403641364236433644364536463647364836493650365136523653365436553656365736583659366036613662366336643665366636673668366936703671367236733674367536763677367836793680368136823683368436853686368736883689369036913692369336943695369636973698369937003701370237033704370537063707370837093710371137123713371437153716371737183719372037213722372337243725372637273728372937303731373237333734373537363737373837393740374137423743374437453746374737483749375037513752375337543755375637573758
  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 { useSoundcloudPlayer } from "@/composables/useSoundcloudPlayer";
  16. import { useWebsocketsStore } from "@/stores/websockets";
  17. import { useConfigStore } from "@/stores/config";
  18. import { useStationStore } from "@/stores/station";
  19. import { useUserAuthStore } from "@/stores/userAuth";
  20. import { useUserPreferencesStore } from "@/stores/userPreferences";
  21. import { useModalsStore } from "@/stores/modals";
  22. import aw from "@/aw";
  23. import ms from "@/ms";
  24. import keyboardShortcuts from "@/keyboardShortcuts";
  25. import utils from "@/utils";
  26. const TAG = "[STATION]";
  27. const MainHeader = defineAsyncComponent(
  28. () => import("@/components/MainHeader.vue")
  29. );
  30. const MainFooter = defineAsyncComponent(
  31. () => import("@/components/MainFooter.vue")
  32. );
  33. const FloatingBox = defineAsyncComponent(
  34. () => import("@/components/FloatingBox.vue")
  35. );
  36. const StationInfoBox = defineAsyncComponent(
  37. () => import("@/components/StationInfoBox.vue")
  38. );
  39. const AddToPlaylistDropdown = defineAsyncComponent(
  40. () => import("@/components/AddToPlaylistDropdown.vue")
  41. );
  42. const MediaItem = defineAsyncComponent(
  43. () => import("@/components/MediaItem.vue")
  44. );
  45. const Z404 = defineAsyncComponent(() => import("@/pages/404.vue"));
  46. const StationSidebar = defineAsyncComponent(
  47. () => import("./Sidebar/index.vue")
  48. );
  49. const route = useRoute();
  50. const router = useRouter();
  51. const { socket } = useWebsocketsStore();
  52. const configStore = useConfigStore();
  53. const { experimental, primaryColor, sitename, christmas } =
  54. storeToRefs(configStore);
  55. const stationStore = useStationStore();
  56. const userAuthStore = useUserAuthStore();
  57. const userPreferencesStore = useUserPreferencesStore();
  58. const {
  59. soundcloudIframeElement,
  60. soundcloudLoadTrack,
  61. soundcloudSeekTo,
  62. soundcloudPlay,
  63. soundcloudPause,
  64. soundcloudSetVolume,
  65. soundcloudGetPosition,
  66. soundcloudGetTrackState,
  67. soundcloudBindListener,
  68. soundcloudOnTrackStateChange,
  69. soundcloudDestroy,
  70. soundcloudUnload
  71. } = useSoundcloudPlayer();
  72. // TODO this might need a different place, like onMounted
  73. const isApple = ref(
  74. navigator.platform.match(/iPhone|iPod|iPad/) ||
  75. navigator.vendor === "Apple Computer, Inc."
  76. );
  77. const loading = ref(true);
  78. const exists = ref(true);
  79. const youtubePlayerReady = ref(false);
  80. const youtubePlayer = ref(undefined);
  81. const timePaused = ref(0);
  82. const muted = ref(false);
  83. const timeElapsed = ref("0:00");
  84. const timeBeforePause = ref(0);
  85. const systemDifference = ref(0);
  86. const attemptsToPlayVideo = ref(0);
  87. const canAutoplay = ref(true);
  88. const lastTimeRequestedIfCanAutoplay = ref(0);
  89. const seeking = ref(false);
  90. const playbackRate = ref(1);
  91. const volumeSliderValue = ref(0);
  92. const showPlaylistDropdown = ref(false);
  93. const seekerbarPercentage = ref(0);
  94. const frontendDevMode = ref("production");
  95. const activityWatchMediaDataInterval = ref(null);
  96. const activityWatchMediaLastStatus = ref("");
  97. const activityWatchMediaLastMediaSource = ref("");
  98. const activityWatchMediaLastStartDuration = ref(0);
  99. const reportStationStateInterval = ref(null);
  100. const nextCurrentSong = ref(null);
  101. const mediaModalWatcher = ref(null);
  102. const beforeMediaModalLocalPausedLock = ref(false);
  103. const beforeMediaModalLocalPaused = ref(null);
  104. const persistentToastCheckerInterval = ref(null);
  105. const persistentToasts = ref([]);
  106. // Experimental options
  107. const experimentalChangableListenModeEnabled = ref(false);
  108. const experimentalChangableListenMode = ref("listen_and_participate"); // Can be either listen_and_participate or participate
  109. // End experimental options
  110. const videoLoading = ref();
  111. const startedAt = ref();
  112. const pausedAt = ref();
  113. const stationIdentifier = ref();
  114. const calculateTimeDifferenceTimeout = ref();
  115. const playerDebugBox = ref();
  116. const keyboardShortcutsHelper = ref();
  117. const autoPaused = ref(false);
  118. const modalsStore = useModalsStore();
  119. const { activeModals } = storeToRefs(modalsStore);
  120. // TODO fix this if it still has some use, as this is no longer accurate
  121. // const video = computed(() => store.state.modals.editSong);
  122. const { loggedIn, userId } = storeToRefs(userAuthStore);
  123. const { nightmode, autoSkipDisliked } = storeToRefs(userPreferencesStore);
  124. const {
  125. station,
  126. currentSong,
  127. nextSong,
  128. songsList,
  129. stationPaused,
  130. localPaused,
  131. noSong,
  132. autoRequest,
  133. autoRequestLock,
  134. autorequestExcludedMediaSources
  135. } = storeToRefs(stationStore);
  136. const youtubePlayerState = ref<
  137. null | "UNSTARTED" | "ENDED" | "PLAYING" | "PAUSED" | "BUFFERING" | "CUED"
  138. >(null);
  139. const skipVotesLoaded = computed(
  140. () =>
  141. !noSong.value &&
  142. Number.isInteger(currentSong.value.skipVotes) &&
  143. currentSong.value.skipVotes >= 0
  144. );
  145. const ratingsLoaded = computed(
  146. () =>
  147. !noSong.value &&
  148. Number.isInteger(currentSong.value.likes) &&
  149. Number.isInteger(currentSong.value.dislikes) &&
  150. currentSong.value.likes >= 0 &&
  151. currentSong.value.dislikes >= 0
  152. );
  153. const ownRatingsLoaded = computed(
  154. () =>
  155. !noSong.value &&
  156. typeof currentSong.value.liked === "boolean" &&
  157. typeof currentSong.value.disliked === "boolean"
  158. );
  159. const aModalIsOpen = computed(() => Object.keys(activeModals.value).length > 0);
  160. const currentUserQueueSongs = computed(
  161. () =>
  162. songsList.value.filter(
  163. queueSong => queueSong.requestedBy === userId.value
  164. ).length
  165. );
  166. const currentSongMediaType = computed(() => {
  167. if (
  168. !currentSong.value ||
  169. !currentSong.value.mediaSource ||
  170. currentSong.value.mediaSource.indexOf(":") === -1
  171. )
  172. return "none";
  173. return currentSong.value.mediaSource.split(":")[0];
  174. });
  175. const currentSongMediaValue = computed(() => {
  176. if (
  177. !currentSong.value ||
  178. !currentSong.value.mediaSource ||
  179. currentSong.value.mediaSource.indexOf(":") === -1
  180. )
  181. return null;
  182. return currentSong.value.mediaSource.split(":")[1];
  183. });
  184. const currentYoutubeId = computed(() => {
  185. if (
  186. !currentSong.value ||
  187. !currentSong.value.mediaSource.startsWith("youtube:")
  188. )
  189. return null;
  190. return currentSong.value.mediaSource.split(":")[1];
  191. });
  192. const stationState = computed(() => {
  193. if (noSong.value) return "no_song";
  194. if (stationPaused.value) return "station_paused";
  195. if (
  196. experimentalChangableListenModeEnabled.value &&
  197. experimentalChangableListenMode.value === "participate"
  198. )
  199. return "participate";
  200. if (localPaused.value) return "local_paused";
  201. if (volumeSliderValue.value === 0 || muted.value) return "muted";
  202. if (currentSongMediaType.value === "youtube" && youtubePlayerReady.value) {
  203. if (youtubePlayerState.value === "PLAYING") return "playing";
  204. if (youtubePlayerState.value === "BUFFERING") return "buffering";
  205. }
  206. if (
  207. currentSongMediaType.value === "soundcloud" &&
  208. soundcloudGetTrackState() === "playing"
  209. )
  210. return "playing";
  211. if (autoPaused.value) return "unavailable";
  212. return "unknown";
  213. });
  214. const {
  215. joinStation,
  216. leaveStation,
  217. updateStation,
  218. updateUserCount,
  219. updateUsers,
  220. updateCurrentSong,
  221. updateNextSong,
  222. updateSongsList,
  223. reorderSongsList,
  224. updateStationPaused,
  225. updateLocalPaused,
  226. updateNoSong,
  227. updateIfStationIsFavorited,
  228. setAutofillPlaylists,
  229. setBlacklist,
  230. updateCurrentSongRatings,
  231. updateOwnCurrentSongRatings,
  232. updateCurrentSongSkipVotes,
  233. updateAutoRequestLock,
  234. updateAutorequestLocalStorage,
  235. hasPermission,
  236. addDj,
  237. removeDj,
  238. updatePermissions,
  239. addHistoryItem,
  240. setHistory
  241. } = stationStore;
  242. // TODO fix this if it still has some use
  243. // const stopVideo = payload =>
  244. // store.dispatch("modals/editSong/stopVideo", payload);
  245. const calculateTimeDifference = (firstRun = false) => {
  246. if (localStorage.getItem("stationNoSystemTimeDifference") === "true") {
  247. console.log(
  248. "Not calculating time different because 'stationNoSystemTimeDifference' is 'true' in localStorage"
  249. );
  250. return;
  251. }
  252. if (!station.value._id) return;
  253. if (calculateTimeDifferenceTimeout.value)
  254. clearTimeout(calculateTimeDifferenceTimeout.value);
  255. // Store the current time in ms before we send a ping to the backend
  256. const beforePing = Date.now();
  257. socket.dispatch("apis.ping", res => {
  258. if (res.status === "success") {
  259. // Store the current time in ms after we receive a pong from the backend
  260. const afterPing = Date.now();
  261. // Calculate the approximate latency between the client and the backend, by taking the time the request took and dividing it in 2
  262. // This is not perfect, as the request could take longer to get to the server than be sent back, or the other way around
  263. let connectionLatency = (afterPing - beforePing) / 2;
  264. console.log(
  265. `Latency between client and server: ${connectionLatency}ms`,
  266. beforePing,
  267. afterPing
  268. );
  269. // If we have a station latency in localStorage, use that. Can be used for debugging.
  270. if (localStorage.getItem("stationLatency")) {
  271. connectionLatency = parseInt(
  272. localStorage.getItem("stationLatency")
  273. );
  274. console.log(
  275. `Using latency from local storage: ${connectionLatency}ms`
  276. );
  277. }
  278. // Store the server time in ms that the server had before sending the pong
  279. const serverDate = res.data.date;
  280. // Calculates the approximate different in system time that the current client has, compared to the system time of the backend
  281. // Takes into account the approximate latency, so if it took approximately 500ms between the backend sending the pong, and the client receiving the pong,
  282. // the system time from the backend has to have 500ms added for it to be correct
  283. const difference = serverDate + connectionLatency - afterPing;
  284. console.log(
  285. `Difference in system time compared to server: ${difference}ms`
  286. );
  287. if (Math.abs(difference) > 3000) {
  288. console.log("System time difference is bigger than 3 seconds.");
  289. }
  290. // Gets how many ms. difference there is between the last time this function was called and now
  291. const differenceBetweenLastTime = Math.abs(
  292. systemDifference.value - difference
  293. );
  294. systemDifference.value = difference;
  295. // By default, we want to re-run this function every 5 minutes
  296. let timeoutTime = 1000 * 300;
  297. // If this is the first time this command is called, we want to re-run this function after 15 seconds
  298. // Also, if the system time difference is more than 500ms different from last time, we also want to re-run after 15 seconds
  299. if (firstRun || differenceBetweenLastTime > 500) {
  300. timeoutTime = 1000 * 15;
  301. }
  302. console.log(
  303. `Will attempt to get system time difference again in ${
  304. timeoutTime / 1000
  305. } seconds.`
  306. );
  307. if (calculateTimeDifferenceTimeout.value)
  308. clearTimeout(calculateTimeDifferenceTimeout.value);
  309. calculateTimeDifferenceTimeout.value = setTimeout(() => {
  310. calculateTimeDifference();
  311. }, timeoutTime);
  312. }
  313. });
  314. };
  315. const updateMediaSessionData = song => {
  316. if (song) {
  317. ms.setMediaSessionData(
  318. 0,
  319. !localPaused.value && !stationPaused.value && !autoPaused.value, // This should be improved later
  320. song.title,
  321. song.artists ? song.artists.join(", ") : null,
  322. null,
  323. song.thumbnail ||
  324. `https://img.youtube.com/vi/${song.youtubeId}/mqdefault.jpg`
  325. );
  326. } else ms.removeMediaSessionData(0);
  327. };
  328. const autoRequestSong = () => {
  329. const { limit, allowAutorequest, autorequestLimit } =
  330. station.value.requests;
  331. if (autoRequestLock.value) return;
  332. if (!allowAutorequest) return;
  333. if (autoRequest.value.length === 0) return;
  334. updateAutorequestLocalStorage();
  335. if (currentUserQueueSongs.value >= limit) return;
  336. if (currentUserQueueSongs.value >= autorequestLimit) return;
  337. if (songsList.value.length >= 50) return;
  338. const uniqueMediaSources = new Set();
  339. autoRequest.value.forEach(playlist => {
  340. playlist.songs.forEach(song => {
  341. if (
  342. autorequestExcludedMediaSources.value.indexOf(
  343. song.mediaSource
  344. ) !== -1
  345. )
  346. return;
  347. if (song.mediaSource.startsWith("spotify:")) return;
  348. if (
  349. !experimental.value.soundcloud &&
  350. song.mediaSource.startsWith("soundcloud:")
  351. )
  352. return;
  353. uniqueMediaSources.add(song.mediaSource);
  354. });
  355. });
  356. if (uniqueMediaSources.size > 0) {
  357. const mediaSource = Array.from(uniqueMediaSources.values())[
  358. Math.floor(Math.random() * uniqueMediaSources.size)
  359. ];
  360. updateAutoRequestLock(true);
  361. socket.dispatch(
  362. "stations.addToQueue",
  363. station.value._id,
  364. mediaSource,
  365. "autorequest",
  366. data => {
  367. updateAutoRequestLock(false);
  368. setTimeout(
  369. () => {
  370. autoRequestSong();
  371. },
  372. data.message === "That song is already in the queue."
  373. ? 5000
  374. : 1000
  375. );
  376. }
  377. );
  378. }
  379. };
  380. const dateCurrently = () => new Date().getTime() + systemDifference.value;
  381. const getTimeElapsed = () => {
  382. if (currentSong.value) {
  383. let localTimePaused = timePaused.value;
  384. if (stationPaused.value)
  385. localTimePaused += dateCurrently() - pausedAt.value;
  386. return dateCurrently() - startedAt.value - localTimePaused;
  387. }
  388. return 0;
  389. };
  390. const getTimeRemaining = () => {
  391. if (currentSong.value) {
  392. return currentSong.value.duration * 1000 - getTimeElapsed();
  393. }
  394. return 0;
  395. };
  396. const getCurrentPlayerTime = () =>
  397. new Promise<number>(resolve => {
  398. if (
  399. currentSongMediaType.value === "youtube" &&
  400. youtubePlayerReady.value
  401. ) {
  402. resolve(
  403. Math.max(
  404. youtubePlayer.value.getCurrentTime() -
  405. currentSong.value.skipDuration,
  406. 0
  407. ) * 1000
  408. );
  409. return;
  410. }
  411. if (currentSongMediaType.value === "soundcloud") {
  412. soundcloudGetPosition(position => {
  413. resolve(
  414. Math.max(
  415. position / 1000 - currentSong.value.skipDuration,
  416. 0
  417. ) * 1000
  418. );
  419. });
  420. return;
  421. }
  422. resolve(0);
  423. });
  424. const skipSong = () => {
  425. if (nextCurrentSong.value && nextCurrentSong.value.currentSong) {
  426. const _songsList = songsList.value.concat([]);
  427. if (
  428. _songsList.length > 0 &&
  429. _songsList[0].mediaSource ===
  430. nextCurrentSong.value.currentSong.mediaSource
  431. ) {
  432. _songsList.splice(0, 1);
  433. updateSongsList(_songsList);
  434. }
  435. // TODO fix
  436. // eslint-disable-next-line
  437. setCurrentSong(nextCurrentSong.value);
  438. } else {
  439. // TODO fix
  440. // eslint-disable-next-line
  441. setCurrentSong({
  442. currentSong: null,
  443. startedAt: 0,
  444. paused: stationPaused.value,
  445. timePaused: 0,
  446. pausedAt: 0
  447. });
  448. }
  449. };
  450. const setNextCurrentSong = (_nextCurrentSong, skipSkipCheck = false) => {
  451. nextCurrentSong.value = _nextCurrentSong;
  452. // If skipSkipCheck is true, it won't try to skip the song
  453. if (getTimeRemaining() <= 0 && !skipSkipCheck) {
  454. skipSong();
  455. }
  456. };
  457. const resizeSeekerbar = () => {
  458. seekerbarPercentage.value =
  459. (getTimeElapsed() / 1000 / currentSong.value.duration) * 100;
  460. };
  461. const playerSeekTo = position => {
  462. console.debug("PLAYER SEEK TO", position);
  463. // Position is in seconds
  464. if (currentSongMediaType.value === "youtube" && youtubePlayerReady.value) {
  465. youtubePlayer.value.seekTo(position);
  466. }
  467. if (currentSongMediaType.value === "soundcloud") {
  468. soundcloudSeekTo(position * 1000);
  469. }
  470. };
  471. const playerPause = () => {
  472. if (youtubePlayerReady.value) {
  473. youtubePlayer.value.pauseVideo();
  474. }
  475. soundcloudPause();
  476. };
  477. const calculateTimeElapsed = async () => {
  478. if (experimentalChangableListenMode.value === "participate") return;
  479. if (
  480. youtubePlayerReady.value &&
  481. currentSongMediaType.value === "youtube" &&
  482. !noSong.value &&
  483. currentSong.value &&
  484. youtubePlayer.value?.getPlayerState() === -1
  485. ) {
  486. if (!canAutoplay.value) {
  487. if (Date.now() - lastTimeRequestedIfCanAutoplay.value > 2000) {
  488. lastTimeRequestedIfCanAutoplay.value = Date.now();
  489. canAutoPlay.video().then(({ result }) => {
  490. if (result) {
  491. attemptsToPlayVideo.value = 0;
  492. canAutoplay.value = true;
  493. } else {
  494. canAutoplay.value = false;
  495. }
  496. });
  497. }
  498. } else if (!stationPaused.value && !localPaused.value) {
  499. youtubePlayer.value.playVideo();
  500. attemptsToPlayVideo.value += 1;
  501. }
  502. }
  503. if (
  504. !stationPaused.value &&
  505. !localPaused.value &&
  506. !autoPaused.value &&
  507. !isApple.value
  508. ) {
  509. const timeElapsed = getTimeElapsed();
  510. const currentPlayerTime = await getCurrentPlayerTime();
  511. const difference = timeElapsed - currentPlayerTime;
  512. let _playbackRate = 1;
  513. if (difference < -2000) {
  514. if (!seeking.value) {
  515. seeking.value = true;
  516. playerSeekTo(
  517. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  518. );
  519. }
  520. } else if (difference < -200) {
  521. _playbackRate = 0.8;
  522. } else if (difference < -50) {
  523. _playbackRate = 0.9;
  524. } else if (difference < -25) {
  525. _playbackRate = 0.95;
  526. } else if (difference > 2000) {
  527. if (!seeking.value) {
  528. seeking.value = true;
  529. playerSeekTo(
  530. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  531. );
  532. }
  533. } else if (difference > 200) {
  534. _playbackRate = 1.2;
  535. } else if (difference > 50) {
  536. _playbackRate = 1.1;
  537. } else if (difference > 25) {
  538. _playbackRate = 1.05;
  539. } else if (
  540. currentSongMediaType.value === "youtube" &&
  541. youtubePlayerReady.value &&
  542. youtubePlayer.value.getPlaybackRate !== 1.0
  543. ) {
  544. youtubePlayer.value.setPlaybackRate(1.0);
  545. }
  546. if (
  547. currentSongMediaType.value === "youtube" &&
  548. youtubePlayerReady.value &&
  549. playbackRate.value !== _playbackRate
  550. ) {
  551. youtubePlayer.value.setPlaybackRate(_playbackRate);
  552. playbackRate.value = _playbackRate;
  553. }
  554. }
  555. let localTimePaused = timePaused.value;
  556. if (stationPaused.value)
  557. localTimePaused += dateCurrently() - pausedAt.value;
  558. const duration =
  559. (dateCurrently() - startedAt.value - localTimePaused) / 1000;
  560. const songDuration = currentSong.value.duration;
  561. // TODO: we should really move this out of calculateTimeElapsed in the future
  562. if (
  563. youtubePlayerReady.value &&
  564. songDuration <= duration &&
  565. currentSongMediaType.value === "youtube"
  566. )
  567. playerPause();
  568. if (duration <= songDuration)
  569. timeElapsed.value =
  570. typeof duration === "number" ? utils.formatTime(duration) : "0";
  571. };
  572. const playVideo = () => {
  573. console.debug(TAG, "Play video start");
  574. if (currentSongMediaType.value === "youtube") {
  575. if (youtubePlayerReady.value) {
  576. videoLoading.value = true;
  577. if (stationPaused.value || localPaused.value) {
  578. youtubePlayer.value.cueVideoById(
  579. currentYoutubeId.value,
  580. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  581. );
  582. } else {
  583. youtubePlayer.value.loadVideoById(
  584. currentYoutubeId.value,
  585. getTimeElapsed() / 1000 + currentSong.value.skipDuration
  586. );
  587. }
  588. }
  589. } else if (currentSongMediaType.value === "soundcloud") {
  590. const soundcloudId = currentSongMediaValue.value;
  591. soundcloudLoadTrack(
  592. soundcloudId,
  593. getTimeElapsed() / 1000 + currentSong.value.skipDuration,
  594. localPaused.value || stationPaused.value
  595. );
  596. }
  597. if (window.stationInterval !== 0) clearInterval(window.stationInterval);
  598. window.stationInterval = window.setInterval(() => {
  599. if (!stationPaused.value) {
  600. resizeSeekerbar();
  601. calculateTimeElapsed();
  602. }
  603. }, 150);
  604. console.debug(TAG, "Play video end");
  605. };
  606. const changeSoundcloudPlayerVolume = () => {
  607. if (muted.value) soundcloudSetVolume(0);
  608. else soundcloudSetVolume(volumeSliderValue.value);
  609. };
  610. const changePlayerVolume = () => {
  611. if (youtubePlayerReady.value) {
  612. youtubePlayer.value.setVolume(volumeSliderValue.value);
  613. if (muted.value) youtubePlayer.value.mute();
  614. else youtubePlayer.value.unMute();
  615. }
  616. changeSoundcloudPlayerVolume();
  617. };
  618. const playerPlay = () => {
  619. if (stationPaused.value || localPaused.value) return;
  620. console.debug(
  621. TAG,
  622. "PLAYER PLAY",
  623. currentSongMediaType.value,
  624. youtubePlayerReady.value
  625. );
  626. if (currentSongMediaType.value === "youtube" && youtubePlayerReady.value) {
  627. youtubePlayer.value.playVideo();
  628. }
  629. if (currentSongMediaType.value === "soundcloud") {
  630. soundcloudPlay();
  631. }
  632. };
  633. const playerStop = () => {
  634. if (youtubePlayerReady.value) {
  635. youtubePlayer.value.stopVideo();
  636. }
  637. soundcloudDestroy();
  638. };
  639. const toggleSkipVote = (message?) => {
  640. socket.dispatch("stations.toggleSkipVote", station.value._id, data => {
  641. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  642. else
  643. new Toast(
  644. message || "Successfully toggled vote to skip the current song."
  645. );
  646. });
  647. };
  648. const autoSkipVote = () => {
  649. if (
  650. !(localPaused.value || stationPaused.value) &&
  651. !currentSong.value.voted
  652. ) {
  653. // automatically vote to skip
  654. toggleSkipVote(
  655. "Automatically voted to skip as this song isn't available for you."
  656. );
  657. }
  658. // persistent message while song is playing
  659. const persistentToast = new Toast({
  660. content:
  661. "This song is unavailable for you, but may be working fine for everyone else.",
  662. persistent: true
  663. });
  664. // save current song id
  665. const erroredMediaSource = currentSong.value.mediaSource;
  666. const self = {
  667. toast: persistentToast,
  668. checkIfCanRemove: () => {
  669. if (currentSong.value.mediaSource !== erroredMediaSource) {
  670. persistentToast.destroy();
  671. persistentToasts.value.splice(
  672. persistentToasts.value.indexOf(self),
  673. 1
  674. );
  675. return true;
  676. }
  677. return false;
  678. }
  679. };
  680. persistentToasts.value.push(self);
  681. };
  682. const resumeLocalPlayer = () => {
  683. if (experimental.value.media_session)
  684. updateMediaSessionData(currentSong.value);
  685. if (!noSong.value) {
  686. playerSeekTo(getTimeElapsed() / 1000 + currentSong.value.skipDuration);
  687. playerPlay();
  688. }
  689. };
  690. const resumeLocalStation = () => {
  691. updateLocalPaused(false);
  692. if (!stationPaused.value) resumeLocalPlayer();
  693. };
  694. const pauseLocalPlayer = () => {
  695. if (experimental.value.media_session)
  696. updateMediaSessionData(currentSong.value);
  697. if (!noSong.value) {
  698. timeBeforePause.value = getTimeElapsed();
  699. playerPause();
  700. }
  701. };
  702. const pauseLocalStation = () => {
  703. updateLocalPaused(true);
  704. pauseLocalPlayer();
  705. };
  706. const youtubeReady = () => {
  707. if (experimentalChangableListenMode.value === "participate") return;
  708. if (!youtubePlayer.value) {
  709. ms.setYTReady(false);
  710. youtubePlayer.value = new window.YT.Player("youtubeStationPlayer", {
  711. height: 270,
  712. width: 480,
  713. // TODO CHECK TYPE
  714. videoId: currentYoutubeId.value,
  715. host: "https://www.youtube-nocookie.com",
  716. startSeconds:
  717. getTimeElapsed() / 1000 + currentSong.value.skipDuration,
  718. playerVars: {
  719. controls: 0,
  720. iv_load_policy: 3,
  721. rel: 0,
  722. showinfo: 0,
  723. disablekb: 1,
  724. playsinline: 1
  725. },
  726. events: {
  727. onReady: () => {
  728. youtubePlayerReady.value = true;
  729. ms.setYTReady(true);
  730. changePlayerVolume();
  731. playVideo();
  732. const duration =
  733. (dateCurrently() - startedAt.value - timePaused.value) /
  734. 1000;
  735. const songDuration = currentSong.value.duration;
  736. if (songDuration <= duration)
  737. youtubePlayer.value.pauseVideo();
  738. // on ios, playback will be forcibly paused locally
  739. if (isApple.value) {
  740. updateLocalPaused(true);
  741. new Toast(
  742. `Please click play manually to use ${sitename.value} on iOS.`
  743. );
  744. }
  745. },
  746. onError: err => {
  747. console.log("error with youtube video", err);
  748. if (err.data >= 100 && loggedIn.value) autoSkipVote();
  749. else
  750. new Toast(
  751. "There has been an error with the YouTube Embed"
  752. );
  753. autoPaused.value = true;
  754. },
  755. onStateChange: event => {
  756. switch (event.data) {
  757. case window.YT.PlayerState.UNSTARTED:
  758. youtubePlayerState.value = "UNSTARTED";
  759. break;
  760. case window.YT.PlayerState.ENDED:
  761. youtubePlayerState.value = "ENDED";
  762. break;
  763. case window.YT.PlayerState.PLAYING:
  764. youtubePlayerState.value = "PLAYING";
  765. break;
  766. case window.YT.PlayerState.PAUSED:
  767. youtubePlayerState.value = "PAUSED";
  768. break;
  769. case window.YT.PlayerState.BUFFERING:
  770. youtubePlayerState.value = "BUFFERING";
  771. break;
  772. case window.YT.PlayerState.CUED:
  773. youtubePlayerState.value = "CUED";
  774. break;
  775. default:
  776. youtubePlayerState.value = null;
  777. }
  778. if (event.data === window.YT.PlayerState.PLAYING)
  779. autoPaused.value = false;
  780. if (
  781. event.data === window.YT.PlayerState.PLAYING &&
  782. (noSong.value ||
  783. currentSongMediaType.value !== "youtube")
  784. ) {
  785. youtubePlayer.value.stopVideo();
  786. } else if (
  787. event.data === window.YT.PlayerState.PLAYING &&
  788. videoLoading.value === true
  789. ) {
  790. videoLoading.value = false;
  791. youtubePlayer.value.seekTo(
  792. getTimeElapsed() / 1000 +
  793. currentSong.value.skipDuration,
  794. true
  795. );
  796. canAutoplay.value = true;
  797. if (stationPaused.value || localPaused.value)
  798. youtubePlayer.value.pauseVideo();
  799. } else if (
  800. event.data === window.YT.PlayerState.PLAYING &&
  801. (localPaused.value || stationPaused.value)
  802. ) {
  803. youtubePlayer.value.seekTo(
  804. timeBeforePause.value / 1000,
  805. true
  806. );
  807. if (stationPaused.value)
  808. youtubePlayer.value.pauseVideo();
  809. else resumeLocalStation();
  810. } else if (
  811. event.data === window.YT.PlayerState.PLAYING &&
  812. seeking.value === true
  813. )
  814. seeking.value = false;
  815. if (
  816. event.data === window.YT.PlayerState.PAUSED &&
  817. !localPaused.value &&
  818. !stationPaused.value &&
  819. !noSong.value &&
  820. currentSongMediaType.value === "youtube" &&
  821. getTimeRemaining() > 0
  822. ) {
  823. youtubePlayer.value.seekTo(
  824. getTimeElapsed() / 1000 +
  825. currentSong.value.skipDuration,
  826. true
  827. );
  828. pauseLocalStation();
  829. }
  830. }
  831. }
  832. });
  833. }
  834. };
  835. const setCurrentSong = data => {
  836. console.debug(TAG, "Set current song start");
  837. const {
  838. currentSong: _currentSong,
  839. startedAt: _startedAt,
  840. paused: _paused,
  841. timePaused: _timePaused,
  842. pausedAt: _pausedAt
  843. } = data;
  844. if (_currentSong) {
  845. if (!_currentSong.skipDuration || _currentSong.skipDuration < 0)
  846. _currentSong.skipDuration = 0;
  847. if (!_currentSong.duration || _currentSong.duration < 0)
  848. _currentSong.duration = 0;
  849. }
  850. updateCurrentSong(_currentSong || {});
  851. let nextSong = null;
  852. if (songsList.value[0])
  853. nextSong = songsList.value[0].mediaSource ? songsList.value[0] : null;
  854. updateNextSong(nextSong);
  855. setNextCurrentSong(
  856. {
  857. currentSong: null,
  858. startedAt: 0,
  859. _paused,
  860. timePaused: 0,
  861. pausedAt: 0
  862. },
  863. true
  864. );
  865. clearTimeout(window.stationNextSongTimeout);
  866. if (experimental.value.media_session) updateMediaSessionData(_currentSong);
  867. startedAt.value = _startedAt;
  868. updateStationPaused(_paused);
  869. timePaused.value = _timePaused;
  870. pausedAt.value = _pausedAt;
  871. playerStop();
  872. if (_currentSong) {
  873. updateNoSong(false);
  874. if (currentSongMediaType.value === "youtube") {
  875. if (!youtubePlayerReady.value) youtubeReady();
  876. else playVideo();
  877. } else if (currentSongMediaType.value === "soundcloud") {
  878. playVideo();
  879. }
  880. // 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
  881. if (!stationPaused.value && !socket.ready) {
  882. if (nextSong)
  883. setNextCurrentSong(
  884. {
  885. currentSong: nextSong,
  886. startedAt: Date.now() + getTimeRemaining(),
  887. paused: false,
  888. timePaused: 0
  889. },
  890. true
  891. );
  892. else
  893. setNextCurrentSong(
  894. {
  895. currentSong: null,
  896. startedAt: 0,
  897. paused: false,
  898. timePaused: 0,
  899. pausedAt: 0
  900. },
  901. true
  902. );
  903. window.stationNextSongTimeout = setTimeout(() => {
  904. if (
  905. !noSong.value &&
  906. _currentSong.value._id === _currentSong._id
  907. )
  908. skipSong();
  909. }, getTimeRemaining());
  910. }
  911. const currentSongId = currentSong.value._id;
  912. socket.dispatch(
  913. "stations.getSkipVotes",
  914. station.value._id,
  915. currentSongId,
  916. res => {
  917. if (res.status === "success") {
  918. const { skipVotes, skipVotesCurrent, voted } = res.data;
  919. if (
  920. !noSong.value &&
  921. currentSong.value._id === currentSongId
  922. ) {
  923. updateCurrentSongSkipVotes({
  924. skipVotes,
  925. skipVotesCurrent,
  926. voted
  927. });
  928. }
  929. }
  930. }
  931. );
  932. socket.dispatch("media.getRatings", _currentSong.mediaSource, res => {
  933. if (_currentSong.mediaSource === currentSong.value.mediaSource) {
  934. const { likes, dislikes } = res.data;
  935. updateCurrentSongRatings({ likes, dislikes });
  936. }
  937. });
  938. if (loggedIn.value) {
  939. socket.dispatch(
  940. "media.getOwnRatings",
  941. _currentSong.mediaSource,
  942. res => {
  943. console.log("getOwnSongRatings", res);
  944. if (
  945. res.status === "success" &&
  946. currentSong.value.mediaSource === res.data.mediaSource
  947. ) {
  948. updateOwnCurrentSongRatings(res.data);
  949. if (
  950. autoSkipDisliked.value &&
  951. res.data.disliked === true &&
  952. !(
  953. localPaused.value ||
  954. stationPaused.value ||
  955. autoPaused.value
  956. ) &&
  957. !currentSong.value.voted
  958. ) {
  959. toggleSkipVote(
  960. "Automatically voted to skip disliked song."
  961. );
  962. }
  963. }
  964. }
  965. );
  966. }
  967. } else {
  968. updateNoSong(true);
  969. }
  970. calculateTimeElapsed();
  971. resizeSeekerbar();
  972. console.debug(TAG, "Set current song end");
  973. };
  974. const changeVolume = () => {
  975. const volume = volumeSliderValue.value;
  976. localStorage.setItem("volume", `${volume}`);
  977. muted.value = volume <= 0;
  978. localStorage.setItem("muted", `${muted.value}`);
  979. changePlayerVolume();
  980. };
  981. const skipStation = () => {
  982. socket.dispatch("stations.forceSkip", station.value._id, data => {
  983. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  984. else new Toast("Successfully skipped the station's current song.");
  985. });
  986. };
  987. const resumeStation = () => {
  988. socket.dispatch("stations.resume", station.value._id, data => {
  989. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  990. else new Toast("Successfully resumed the station.");
  991. });
  992. };
  993. const pauseStation = () => {
  994. socket.dispatch("stations.pause", station.value._id, data => {
  995. if (data.status !== "success") new Toast(`Error: ${data.message}`);
  996. else new Toast("Successfully paused the station.");
  997. });
  998. };
  999. const toggleMute = () => {
  1000. muted.value = !muted.value;
  1001. changePlayerVolume();
  1002. };
  1003. const toggleLike = () => {
  1004. if (currentSong.value.liked)
  1005. socket.dispatch("media.unlike", currentSong.value.mediaSource, res => {
  1006. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  1007. });
  1008. else
  1009. socket.dispatch("media.like", currentSong.value.mediaSource, res => {
  1010. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  1011. });
  1012. };
  1013. const toggleDislike = () => {
  1014. if (currentSong.value.disliked)
  1015. return socket.dispatch(
  1016. "media.undislike",
  1017. currentSong.value.mediaSource,
  1018. res => {
  1019. if (res.status !== "success")
  1020. new Toast(`Error: ${res.message}`);
  1021. }
  1022. );
  1023. return socket.dispatch(
  1024. "media.dislike",
  1025. currentSong.value.mediaSource,
  1026. res => {
  1027. if (res.status !== "success") new Toast(`Error: ${res.message}`);
  1028. }
  1029. );
  1030. };
  1031. const togglePlayerDebugBox = () => {
  1032. playerDebugBox.value.toggleBox();
  1033. };
  1034. const resetPlayerDebugBox = () => {
  1035. playerDebugBox.value.resetBox();
  1036. };
  1037. const toggleKeyboardShortcutsHelper = () => {
  1038. keyboardShortcutsHelper.value.toggleBox();
  1039. };
  1040. const resetKeyboardShortcutsHelper = () => {
  1041. keyboardShortcutsHelper.value.resetBox();
  1042. };
  1043. const sendActivityWatchMediaData = () => {
  1044. // TODO have this support soundcloud
  1045. if (
  1046. !autoPaused.value &&
  1047. !stationPaused.value &&
  1048. (!localPaused.value ||
  1049. experimentalChangableListenMode.value === "participate") &&
  1050. !noSong.value &&
  1051. (experimentalChangableListenMode.value === "participate" ||
  1052. currentSongMediaType.value !== "youtube" ||
  1053. (typeof youtubePlayer.value?.getPlayerState === "function" &&
  1054. youtubePlayer.value?.getPlayerState() ===
  1055. window.YT.PlayerState.PLAYING))
  1056. ) {
  1057. if (activityWatchMediaLastStatus.value !== "playing") {
  1058. activityWatchMediaLastStatus.value = "playing";
  1059. activityWatchMediaLastStartDuration.value =
  1060. currentSong.value.skipDuration + getTimeElapsed();
  1061. }
  1062. if (
  1063. activityWatchMediaLastMediaSource.value !==
  1064. currentSong.value.mediaSource
  1065. ) {
  1066. activityWatchMediaLastMediaSource.value =
  1067. currentSong.value.mediaSource;
  1068. activityWatchMediaLastStartDuration.value =
  1069. currentSong.value.skipDuration + getTimeElapsed();
  1070. }
  1071. const videoData = {
  1072. title: currentSong.value ? currentSong.value.title : null,
  1073. artists:
  1074. currentSong.value && currentSong.value.artists
  1075. ? currentSong.value.artists.join(", ")
  1076. : null,
  1077. mediaSource: currentSong.value.mediaSource,
  1078. muted: muted.value,
  1079. volume: volumeSliderValue.value,
  1080. startedDuration:
  1081. activityWatchMediaLastStartDuration.value <= 0
  1082. ? 0
  1083. : Math.floor(
  1084. activityWatchMediaLastStartDuration.value / 1000
  1085. ),
  1086. source: `station#${station.value.name}`,
  1087. hostname: window.location.hostname,
  1088. experimentalChangableListenMode:
  1089. experimentalChangableListenMode.value,
  1090. playerState: "",
  1091. playbackRate: -1
  1092. };
  1093. if (currentSongMediaType.value === "youtube") {
  1094. videoData.playerState =
  1095. experimentalChangableListenMode.value === "participate"
  1096. ? "none"
  1097. : Object.keys(window.YT.PlayerState).find(
  1098. key =>
  1099. window.YT.PlayerState[key] ===
  1100. youtubePlayer.value?.getPlayerState()
  1101. );
  1102. videoData.playbackRate = playbackRate.value;
  1103. } else {
  1104. delete videoData.playerState;
  1105. delete videoData.playbackRate;
  1106. }
  1107. const success = aw.sendMediaData(videoData);
  1108. if (!success) pauseLocalStation();
  1109. } else {
  1110. activityWatchMediaLastStatus.value = "not_playing";
  1111. }
  1112. };
  1113. const experimentalChangableListenModeChange = newMode => {
  1114. experimentalChangableListenMode.value = newMode;
  1115. localStorage.setItem(
  1116. `experimental_changeable_listen_mode_${station.value._id}`,
  1117. newMode
  1118. );
  1119. if (newMode === "participate") {
  1120. // Destroy the YouTube player
  1121. if (youtubePlayer.value) {
  1122. youtubePlayer.value.destroy();
  1123. youtubePlayer.value = null;
  1124. youtubePlayerReady.value = false;
  1125. youtubePlayerState.value = null;
  1126. }
  1127. soundcloudDestroy();
  1128. } else {
  1129. // Recreate the YouTube player
  1130. youtubeReady();
  1131. }
  1132. };
  1133. watch(
  1134. () => autoRequest.value.length,
  1135. () => {
  1136. autoRequestSong();
  1137. }
  1138. );
  1139. onMounted(async () => {
  1140. console.debug(TAG, "On mounted start");
  1141. mediaModalWatcher.value = stationStore.$onAction(({ name, args }) => {
  1142. if (name === "updateMediaModalPlayingAudio") {
  1143. const [mediaModalPlayingAudio] = args;
  1144. if (mediaModalPlayingAudio) {
  1145. if (!beforeMediaModalLocalPausedLock.value) {
  1146. beforeMediaModalLocalPausedLock.value = true;
  1147. beforeMediaModalLocalPaused.value = localPaused.value;
  1148. pauseLocalStation();
  1149. }
  1150. } else {
  1151. beforeMediaModalLocalPausedLock.value = false;
  1152. if (!beforeMediaModalLocalPaused.value) resumeLocalStation();
  1153. }
  1154. }
  1155. });
  1156. document.body.scrollTo(0, 0);
  1157. stationIdentifier.value = route.params.id;
  1158. window.stationInterval = 0;
  1159. activityWatchMediaDataInterval.value = setInterval(() => {
  1160. sendActivityWatchMediaData();
  1161. }, 1000);
  1162. reportStationStateInterval.value = setInterval(() => {
  1163. socket.dispatch(
  1164. "stations.setStationState",
  1165. stationState.value,
  1166. () => {}
  1167. );
  1168. }, 5000);
  1169. persistentToastCheckerInterval.value = setInterval(() => {
  1170. persistentToasts.value.filter(
  1171. persistentToast => !persistentToast.checkIfCanRemove()
  1172. );
  1173. }, 1000);
  1174. socket.onConnect(() => {
  1175. console.debug(TAG, "On socked connect start");
  1176. clearTimeout(window.stationNextSongTimeout);
  1177. socket.dispatch("stations.join", stationIdentifier.value, async res => {
  1178. if (res.status === "success") {
  1179. console.debug(TAG, "Station join start");
  1180. setTimeout(() => {
  1181. loading.value = false;
  1182. }, 1000); // prevents popping in of youtube embed etc.
  1183. const {
  1184. _id,
  1185. displayName,
  1186. name,
  1187. description,
  1188. privacy,
  1189. owner,
  1190. autofill,
  1191. blacklist,
  1192. type,
  1193. isFavorited,
  1194. theme,
  1195. requests,
  1196. djs
  1197. } = res.data;
  1198. if (experimental.value.changable_listen_mode) {
  1199. if (experimental.value.changable_listen_mode === true)
  1200. experimentalChangableListenModeEnabled.value = true;
  1201. else if (
  1202. Array.isArray(
  1203. experimental.value.changable_listen_mode
  1204. ) &&
  1205. experimental.value.changable_listen_mode.indexOf(
  1206. _id
  1207. ) !== -1
  1208. )
  1209. experimentalChangableListenModeEnabled.value = true;
  1210. }
  1211. if (experimentalChangableListenModeEnabled.value) {
  1212. console.log(
  1213. `Experimental changeable listen mode is enabled`
  1214. );
  1215. const experimentalChangeableListenModeLS =
  1216. localStorage.getItem(
  1217. `experimental_changeable_listen_mode_${_id}`
  1218. );
  1219. if (experimentalChangeableListenModeLS)
  1220. experimentalChangableListenMode.value =
  1221. experimentalChangeableListenModeLS;
  1222. }
  1223. // change url to use station name instead of station id
  1224. if (name !== stationIdentifier.value) {
  1225. // eslint-disable-next-line no-restricted-globals
  1226. router.replace(name);
  1227. }
  1228. joinStation({
  1229. _id,
  1230. name,
  1231. displayName,
  1232. description,
  1233. privacy,
  1234. owner,
  1235. autofill,
  1236. blacklist,
  1237. type,
  1238. isFavorited,
  1239. theme,
  1240. requests,
  1241. djs
  1242. });
  1243. document.getElementsByTagName("html")[0].style.cssText =
  1244. `--primary-color: var(--${res.data.theme})`;
  1245. setCurrentSong({
  1246. currentSong: res.data.currentSong,
  1247. startedAt: res.data.startedAt,
  1248. paused: res.data.paused,
  1249. timePaused: res.data.timePaused,
  1250. pausedAt: res.data.pausedAt
  1251. });
  1252. updateUserCount(res.data.userCount);
  1253. updateUsers(res.data.users);
  1254. await updatePermissions();
  1255. socket.dispatch(
  1256. "stations.getStationAutofillPlaylistsById",
  1257. station.value._id,
  1258. res => {
  1259. if (res.status === "success") {
  1260. setAutofillPlaylists(res.data.playlists);
  1261. }
  1262. }
  1263. );
  1264. socket.dispatch(
  1265. "stations.getStationBlacklistById",
  1266. station.value._id,
  1267. res => {
  1268. if (res.status === "success") {
  1269. setBlacklist(res.data.playlists);
  1270. }
  1271. }
  1272. );
  1273. socket.dispatch("stations.getQueue", _id, res => {
  1274. if (res.status === "success") {
  1275. const { queue } = res.data;
  1276. updateSongsList(queue);
  1277. const [nextSong] = queue;
  1278. updateNextSong(nextSong);
  1279. }
  1280. });
  1281. if (experimental.value.station_history)
  1282. socket.dispatch("stations.getHistory", _id, res => {
  1283. if (res.status === "success") {
  1284. const { history } = res.data;
  1285. setHistory(history);
  1286. }
  1287. });
  1288. if (hasPermission("stations.playback.toggle"))
  1289. keyboardShortcuts.registerShortcut("station.pauseResume", {
  1290. keyCode: 32, // Spacebar
  1291. shift: false,
  1292. ctrl: true,
  1293. preventDefault: true,
  1294. handler: () => {
  1295. if (aModalIsOpen.value) return;
  1296. if (stationPaused.value) resumeStation();
  1297. else pauseStation();
  1298. }
  1299. });
  1300. if (hasPermission("stations.skip"))
  1301. keyboardShortcuts.registerShortcut("station.skipStation", {
  1302. keyCode: 39, // Right arrow key
  1303. shift: false,
  1304. ctrl: true,
  1305. preventDefault: true,
  1306. handler: () => {
  1307. if (aModalIsOpen.value) return;
  1308. skipStation();
  1309. }
  1310. });
  1311. keyboardShortcuts.registerShortcut("station.lowerVolumeLarge", {
  1312. keyCode: 40, // Down arrow key
  1313. shift: false,
  1314. ctrl: true,
  1315. preventDefault: true,
  1316. handler: () => {
  1317. if (aModalIsOpen.value) return;
  1318. volumeSliderValue.value -= 10;
  1319. changeVolume();
  1320. }
  1321. });
  1322. keyboardShortcuts.registerShortcut("station.lowerVolumeSmall", {
  1323. keyCode: 40, // Down arrow key
  1324. shift: true,
  1325. ctrl: true,
  1326. preventDefault: true,
  1327. handler: () => {
  1328. if (aModalIsOpen.value) return;
  1329. volumeSliderValue.value -= 1;
  1330. changeVolume();
  1331. }
  1332. });
  1333. keyboardShortcuts.registerShortcut(
  1334. "station.increaseVolumeLarge",
  1335. {
  1336. keyCode: 38, // Up arrow key
  1337. shift: false,
  1338. ctrl: true,
  1339. preventDefault: true,
  1340. handler: () => {
  1341. if (aModalIsOpen.value) return;
  1342. volumeSliderValue.value += 10;
  1343. changeVolume();
  1344. }
  1345. }
  1346. );
  1347. keyboardShortcuts.registerShortcut(
  1348. "station.increaseVolumeSmall",
  1349. {
  1350. keyCode: 38, // Up arrow key
  1351. shift: true,
  1352. ctrl: true,
  1353. preventDefault: true,
  1354. handler: () => {
  1355. if (aModalIsOpen.value) return;
  1356. volumeSliderValue.value += 1;
  1357. changeVolume();
  1358. }
  1359. }
  1360. );
  1361. keyboardShortcuts.registerShortcut("station.toggleDebug", {
  1362. keyCode: 68, // D key
  1363. shift: false,
  1364. ctrl: true,
  1365. preventDefault: true,
  1366. handler: () => {
  1367. if (aModalIsOpen.value) return;
  1368. togglePlayerDebugBox();
  1369. }
  1370. });
  1371. keyboardShortcuts.registerShortcut(
  1372. "station.toggleKeyboardShortcutsHelper",
  1373. {
  1374. keyCode: 191, // '/' key
  1375. ctrl: true,
  1376. preventDefault: true,
  1377. handler: () => {
  1378. if (aModalIsOpen.value) return;
  1379. toggleKeyboardShortcutsHelper();
  1380. }
  1381. }
  1382. );
  1383. keyboardShortcuts.registerShortcut(
  1384. "station.resetKeyboardShortcutsHelper",
  1385. {
  1386. keyCode: 191, // '/' key
  1387. ctrl: true,
  1388. shift: true,
  1389. preventDefault: true,
  1390. handler: () => {
  1391. if (aModalIsOpen.value) return;
  1392. resetKeyboardShortcutsHelper();
  1393. }
  1394. }
  1395. );
  1396. keyboardShortcuts.registerShortcut(
  1397. "station.recalculateSystemTimeDifference",
  1398. {
  1399. keyCode: 82, // R key
  1400. shift: true,
  1401. alt: true,
  1402. preventDefault: true,
  1403. handler: () => {
  1404. calculateTimeDifference();
  1405. }
  1406. }
  1407. );
  1408. calculateTimeDifference(true);
  1409. console.debug(TAG, "Station join end");
  1410. } else {
  1411. loading.value = false;
  1412. exists.value = false;
  1413. }
  1414. });
  1415. socket.dispatch(
  1416. "stations.existsByName",
  1417. stationIdentifier.value,
  1418. res => {
  1419. if (res.status === "error" || !res.data.exists) {
  1420. // station identifier may be using stationid instead
  1421. socket.dispatch(
  1422. "stations.existsById",
  1423. stationIdentifier.value,
  1424. res => {
  1425. if (res.status === "error" || !res.data.exists) {
  1426. loading.value = false;
  1427. exists.value = false;
  1428. }
  1429. }
  1430. );
  1431. }
  1432. }
  1433. );
  1434. console.debug(TAG, "On socked connect end");
  1435. });
  1436. socket.onDisconnect(() => {
  1437. const _currentSong = currentSong.value;
  1438. if (nextSong.value)
  1439. setNextCurrentSong(
  1440. {
  1441. currentSong: nextSong.value,
  1442. startedAt: Date.now() + getTimeRemaining(),
  1443. paused: false,
  1444. timePaused: 0
  1445. },
  1446. true
  1447. );
  1448. else
  1449. setNextCurrentSong(
  1450. {
  1451. currentSong: null,
  1452. startedAt: 0,
  1453. paused: false,
  1454. timePaused: 0,
  1455. pausedAt: 0
  1456. },
  1457. true
  1458. );
  1459. window.stationNextSongTimeout = setTimeout(() => {
  1460. if (!noSong.value && currentSong.value._id === _currentSong._id)
  1461. skipSong();
  1462. }, getTimeRemaining());
  1463. clearTimeout(calculateTimeDifferenceTimeout.value);
  1464. }, true);
  1465. socket.on("event:station.nextSong", res => {
  1466. const { currentSong, startedAt, paused, timePaused } = res.data;
  1467. setCurrentSong({
  1468. currentSong,
  1469. startedAt,
  1470. paused,
  1471. timePaused,
  1472. pausedAt: 0
  1473. });
  1474. });
  1475. socket.on("event:station.pause", res => {
  1476. pausedAt.value = res.data.pausedAt;
  1477. updateStationPaused(true);
  1478. pauseLocalPlayer();
  1479. clearTimeout(window.stationNextSongTimeout);
  1480. });
  1481. socket.on("event:station.resume", res => {
  1482. timePaused.value = res.data.timePaused;
  1483. updateStationPaused(false);
  1484. if (!localPaused.value) resumeLocalPlayer();
  1485. autoRequestSong();
  1486. });
  1487. socket.on("event:station.deleted", () => {
  1488. router.push({
  1489. path: "/",
  1490. query: {
  1491. toast: "The station you were in was deleted."
  1492. }
  1493. });
  1494. });
  1495. socket.on("event:ratings.liked", res => {
  1496. if (!noSong.value) {
  1497. if (res.data.mediaSource === currentSong.value.mediaSource) {
  1498. updateCurrentSongRatings(res.data);
  1499. }
  1500. }
  1501. });
  1502. socket.on("event:ratings.disliked", res => {
  1503. if (!noSong.value) {
  1504. if (res.data.mediaSource === currentSong.value.mediaSource) {
  1505. updateCurrentSongRatings(res.data);
  1506. }
  1507. }
  1508. });
  1509. socket.on("event:ratings.unliked", res => {
  1510. if (!noSong.value) {
  1511. if (res.data.mediaSource === currentSong.value.mediaSource) {
  1512. updateCurrentSongRatings(res.data);
  1513. }
  1514. }
  1515. });
  1516. socket.on("event:ratings.undisliked", res => {
  1517. if (!noSong.value) {
  1518. if (res.data.mediaSource === currentSong.value.mediaSource) {
  1519. updateCurrentSongRatings(res.data);
  1520. }
  1521. }
  1522. });
  1523. socket.on("event:ratings.updated", res => {
  1524. if (!noSong.value) {
  1525. if (res.data.mediaSource === currentSong.value.mediaSource) {
  1526. updateOwnCurrentSongRatings(res.data);
  1527. }
  1528. }
  1529. });
  1530. socket.on("event:station.queue.updated", res => {
  1531. updateSongsList(res.data.queue);
  1532. let nextSong = null;
  1533. if (songsList.value[0])
  1534. nextSong = songsList.value[0].mediaSource
  1535. ? songsList.value[0]
  1536. : null;
  1537. updateNextSong(nextSong);
  1538. autoRequestSong();
  1539. });
  1540. socket.on("event:station.queue.order.changed", res => {
  1541. reorderSongsList(res.data.queueOrder);
  1542. let nextSong = null;
  1543. if (songsList.value[0])
  1544. nextSong = songsList.value[0].mediaSource
  1545. ? songsList.value[0]
  1546. : null;
  1547. updateNextSong(nextSong);
  1548. });
  1549. socket.on("event:station.toggleSkipVote", res => {
  1550. if (currentSong.value)
  1551. updateCurrentSongSkipVotes({
  1552. skipVotes: res.data.voted
  1553. ? currentSong.value.skipVotes + 1
  1554. : currentSong.value.skipVotes - 1,
  1555. skipVotesCurrent: null,
  1556. voted:
  1557. res.data.userId === userId.value
  1558. ? res.data.voted
  1559. : currentSong.value.voted
  1560. });
  1561. });
  1562. socket.on("event:station.updated", async res => {
  1563. const { name, theme, privacy } = res.data.station;
  1564. if (!hasPermission("stations.view") && privacy === "private") {
  1565. router.push({
  1566. path: "/",
  1567. query: {
  1568. toast: "The station you were in was made private."
  1569. }
  1570. });
  1571. } else {
  1572. if (station.value.name !== name) {
  1573. await router.push(
  1574. `${name}?${Object.keys(route.query)
  1575. .map(
  1576. key =>
  1577. `${encodeURIComponent(
  1578. key
  1579. )}=${encodeURIComponent(
  1580. JSON.stringify(route.query[key])
  1581. )}`
  1582. )
  1583. .join("&")}`
  1584. );
  1585. // eslint-disable-next-line no-restricted-globals
  1586. window.history.replaceState(
  1587. { ...window.history.state, ...{} },
  1588. null
  1589. );
  1590. }
  1591. if (station.value.theme !== theme)
  1592. document.getElementsByTagName("html")[0].style.cssText =
  1593. `--primary-color: var(--${theme})`;
  1594. updateStation(res.data.station);
  1595. }
  1596. });
  1597. socket.on("event:station.users.updated", res =>
  1598. updateUsers(res.data.users)
  1599. );
  1600. socket.on("event:station.userCount.updated", res =>
  1601. updateUserCount(res.data.userCount)
  1602. );
  1603. socket.on("event:user.station.favorited", res => {
  1604. if (res.data.stationId === station.value._id)
  1605. updateIfStationIsFavorited({ isFavorited: true });
  1606. });
  1607. socket.on("event:user.station.unfavorited", res => {
  1608. if (res.data.stationId === station.value._id)
  1609. updateIfStationIsFavorited({ isFavorited: false });
  1610. });
  1611. socket.on("event:station.djs.added", res => {
  1612. if (res.data.user._id === userId.value)
  1613. updatePermissions().then(() => {
  1614. if (
  1615. !hasPermission("stations.view") &&
  1616. station.value.privacy === "private"
  1617. )
  1618. router.push({
  1619. path: "/",
  1620. query: {
  1621. toast: "You no longer have access to the station you were in."
  1622. }
  1623. });
  1624. });
  1625. addDj(res.data.user);
  1626. });
  1627. socket.on("event:station.djs.removed", res => {
  1628. if (res.data.user._id === userId.value)
  1629. updatePermissions().then(() => {
  1630. if (
  1631. !hasPermission("stations.view") &&
  1632. station.value.privacy === "private"
  1633. )
  1634. router.push({
  1635. path: "/",
  1636. query: {
  1637. toast: "You no longer have access to the station you were in."
  1638. }
  1639. });
  1640. });
  1641. removeDj(res.data.user);
  1642. });
  1643. socket.on("event:station.history.new", res => {
  1644. addHistoryItem(res.data.historyItem);
  1645. });
  1646. socket.on("keep.event:user.role.updated", () => {
  1647. updatePermissions().then(() => {
  1648. if (
  1649. !hasPermission("stations.view") &&
  1650. station.value.privacy === "private"
  1651. )
  1652. router.push({
  1653. path: "/",
  1654. query: {
  1655. toast: "You no longer have access to the station you were in."
  1656. }
  1657. });
  1658. });
  1659. });
  1660. frontendDevMode.value = process.env.NODE_ENV;
  1661. ms.setListeners(0, {
  1662. play: () => {
  1663. if (hasPermission("stations.playback.toggle")) resumeStation();
  1664. else resumeLocalStation();
  1665. },
  1666. pause: () => {
  1667. if (hasPermission("stations.playback.toggle")) pauseStation();
  1668. else pauseLocalStation();
  1669. },
  1670. nexttrack: () => {
  1671. if (hasPermission("stations.skip")) skipStation();
  1672. else if (!currentSong.value.voted) toggleSkipVote();
  1673. }
  1674. });
  1675. if (JSON.parse(localStorage.getItem("muted"))) {
  1676. muted.value = true;
  1677. volumeSliderValue.value = 0;
  1678. } else {
  1679. let volume = parseFloat(localStorage.getItem("volume"));
  1680. volume =
  1681. typeof volume === "number" && !Number.isNaN(volume) ? volume : 20;
  1682. localStorage.setItem("volume", `${volume}`);
  1683. volumeSliderValue.value = volume;
  1684. }
  1685. changePlayerVolume();
  1686. soundcloudOnTrackStateChange(newState => {
  1687. console.debug(TAG, `New state: ${newState}`);
  1688. if (
  1689. newState === "attempting_to_play" ||
  1690. newState === "failed_to_play" ||
  1691. newState === "sound_unavailable"
  1692. ) {
  1693. if (currentSongMediaType.value !== "soundcloud") return;
  1694. if (newState === "failed_to_play") {
  1695. new Toast(
  1696. "Failed to start SoundCloud player. Please try to manually start it."
  1697. );
  1698. } else if (newState === "sound_unavailable") {
  1699. if (loggedIn.value) autoSkipVote();
  1700. else
  1701. new Toast(
  1702. "This song is unavailable for you, but is playing for everyone else."
  1703. );
  1704. }
  1705. autoPaused.value = true;
  1706. } else if (newState === "paused") {
  1707. if (currentSongMediaType.value !== "soundcloud") return;
  1708. if (!localPaused.value && !stationPaused.value) {
  1709. pauseLocalStation();
  1710. }
  1711. } else if (newState === "playing") {
  1712. if (currentSongMediaType.value !== "soundcloud") {
  1713. soundcloudDestroy();
  1714. return;
  1715. }
  1716. autoPaused.value = false;
  1717. if (localPaused.value) resumeLocalStation();
  1718. if (stationPaused.value) {
  1719. soundcloudPause();
  1720. }
  1721. soundcloudSeekTo(
  1722. (getTimeElapsed() / 1000 + currentSong.value.skipDuration) *
  1723. 1000
  1724. );
  1725. } else if (newState === "error") {
  1726. new Toast(
  1727. "Failed to start SoundCloud player. Please try to manually start it."
  1728. );
  1729. autoPaused.value = true;
  1730. }
  1731. });
  1732. soundcloudBindListener("seek", () => {
  1733. console.debug(TAG, "Bind on seek");
  1734. if (seeking.value) seeking.value = false;
  1735. });
  1736. soundcloudBindListener("error", value => {
  1737. console.debug(TAG, "Bind on error", value);
  1738. });
  1739. console.debug(TAG, "On mounted end");
  1740. });
  1741. onBeforeUnmount(() => {
  1742. document.getElementsByTagName("html")[0].style.cssText =
  1743. `--primary-color: ${primaryColor.value}`;
  1744. if (experimental.value.media_session) {
  1745. ms.removeListeners(0);
  1746. ms.removeMediaSessionData(0);
  1747. }
  1748. /** Reset Songslist */
  1749. updateSongsList([]);
  1750. const shortcutNames = [
  1751. "station.pauseResume",
  1752. "station.skipStation",
  1753. "station.lowerVolumeLarge",
  1754. "station.lowerVolumeSmall",
  1755. "station.increaseVolumeLarge",
  1756. "station.increaseVolumeSmall",
  1757. "station.toggleDebug",
  1758. "station.recalculateSystemTimeDifference"
  1759. ];
  1760. shortcutNames.forEach(shortcutName => {
  1761. keyboardShortcuts.unregisterShortcut(shortcutName);
  1762. });
  1763. mediaModalWatcher.value(); // removes the watcher
  1764. clearInterval(activityWatchMediaDataInterval.value);
  1765. clearTimeout(window.stationNextSongTimeout);
  1766. clearTimeout(persistentToastCheckerInterval.value);
  1767. clearInterval(reportStationStateInterval.value);
  1768. clearTimeout(calculateTimeDifferenceTimeout.value);
  1769. persistentToasts.value.forEach(persistentToast => {
  1770. persistentToast.toast.destroy();
  1771. });
  1772. socket.dispatch("stations.leave", station.value._id, () => {});
  1773. const { allowAutorequest } = station.value.requests;
  1774. if (
  1775. !autoRequestLock.value &&
  1776. allowAutorequest &&
  1777. autoRequest.value.length > 0
  1778. )
  1779. updateAutorequestLocalStorage();
  1780. leaveStation();
  1781. soundcloudUnload();
  1782. // Delete the Pinia store that was created for this station, after all other cleanup tasks are performed
  1783. stationStore.$dispose();
  1784. });
  1785. </script>
  1786. <template>
  1787. <div>
  1788. <page-metadata
  1789. v-if="exists && !loading"
  1790. :title="`${station.displayName}`"
  1791. />
  1792. <page-metadata v-else-if="!exists && !loading" :title="`Not found`" />
  1793. <div id="page-loader-container" v-if="loading">
  1794. <content-loader
  1795. width="1920"
  1796. height="1080"
  1797. :primary-color="nightmode ? '#222' : '#fff'"
  1798. :secondary-color="nightmode ? '#444' : '#ddd'"
  1799. preserve-aspect-ratio="none"
  1800. id="page-loader-content"
  1801. >
  1802. <rect x="55" y="105" rx="5" ry="5" width="670" height="149" />
  1803. <rect x="55" y="283" rx="5" ry="5" width="670" height="640" />
  1804. <rect x="745" y="108" rx="5" ry="5" width="1120" height="672" />
  1805. <rect x="745" y="810" rx="5" ry="5" width="1120" height="110" />
  1806. </content-loader>
  1807. <content-loader
  1808. width="1920"
  1809. height="1080"
  1810. :primary-color="nightmode ? '#222' : '#fff'"
  1811. :secondary-color="nightmode ? '#444' : '#ddd'"
  1812. preserve-aspect-ratio="none"
  1813. id="page-loader-layout"
  1814. >
  1815. <rect x="0" y="0" rx="0" ry="0" width="1920" height="64" />
  1816. <rect x="0" y="980" rx="0" ry="0" width="1920" height="100" />
  1817. </content-loader>
  1818. </div>
  1819. <!-- More simplistic loading animation for mobile users -->
  1820. <div v-show="loading" id="mobile-progress-animation" />
  1821. <ul
  1822. v-if="
  1823. currentSong &&
  1824. (currentSong.mediaSource === 'l9PxOanFjxQ' ||
  1825. currentSong.mediaSource === 'xKVcVSYmesU' ||
  1826. currentSong.mediaSource === '60ItHLz5WEA' ||
  1827. currentSong.mediaSource === 'e6vkFbtSGm0')
  1828. "
  1829. class="bg-bubbles"
  1830. >
  1831. <li></li>
  1832. <li></li>
  1833. <li></li>
  1834. <li></li>
  1835. <li></li>
  1836. <li></li>
  1837. <li></li>
  1838. <li></li>
  1839. <li></li>
  1840. <li></li>
  1841. </ul>
  1842. <div v-show="!loading && exists">
  1843. <main-header />
  1844. <div id="station-outer-container">
  1845. <div
  1846. id="station-inner-container"
  1847. :class="{ 'nothing-here': noSong }"
  1848. >
  1849. <div id="station-left-column" class="column">
  1850. <!-- div with quadrant class -->
  1851. <div class="quadrant">
  1852. <station-info-box
  1853. :station="station"
  1854. :station-paused="stationPaused"
  1855. :show-manage-station="true"
  1856. />
  1857. </div>
  1858. <div id="sidebar-container" class="quadrant">
  1859. <station-sidebar />
  1860. </div>
  1861. </div>
  1862. <div id="station-right-column" class="column">
  1863. <div
  1864. class="experimental-listen-mode-container quadrant"
  1865. v-if="
  1866. experimentalChangableListenModeEnabled &&
  1867. !noSong
  1868. "
  1869. v-show="
  1870. experimentalChangableListenMode ===
  1871. 'participate'
  1872. "
  1873. >
  1874. <button
  1875. class="button is-primary"
  1876. @click="
  1877. experimentalChangableListenModeChange(
  1878. 'listen_and_participate'
  1879. )
  1880. "
  1881. >
  1882. <i class="material-icons icon-with-button"
  1883. >music_note</i
  1884. >
  1885. <span>Listen to music</span>
  1886. </button>
  1887. <button
  1888. v-if="!skipVotesLoaded"
  1889. class="button is-primary disabled"
  1890. content="Skip votes have not been loaded yet"
  1891. v-tippy
  1892. >
  1893. <i class="material-icons icon-with-button"
  1894. >skip_next</i
  1895. >
  1896. Vote to skip the current song
  1897. </button>
  1898. <button
  1899. v-else-if="loggedIn"
  1900. :class="[
  1901. 'button',
  1902. 'is-primary',
  1903. { voted: currentSong.voted }
  1904. ]"
  1905. @click="toggleSkipVote()"
  1906. :content="`${
  1907. currentSong.voted ? 'Remove vote' : 'Vote'
  1908. } to Skip Song`"
  1909. v-tippy
  1910. >
  1911. <i class="material-icons icon-with-button"
  1912. >skip_next</i
  1913. >
  1914. Vote to skip the current song -
  1915. {{ currentSong.skipVotes }} votes
  1916. </button>
  1917. <button
  1918. v-else
  1919. class="button is-primary disabled"
  1920. content="Log in to vote to skip songs"
  1921. v-tippy="{ theme: 'info' }"
  1922. >
  1923. <i class="material-icons icon-with-button"
  1924. >skip_next</i
  1925. >
  1926. Vote to skip the current song -
  1927. {{ currentSong.skipVotes }} votes
  1928. </button>
  1929. <div class="row">
  1930. <!-- Ratings -->
  1931. <div
  1932. class="ratings"
  1933. v-if="ratingsLoaded && ownRatingsLoaded"
  1934. :class="{
  1935. liked: currentSong.liked,
  1936. disliked: currentSong.disliked
  1937. }"
  1938. >
  1939. <!-- Like Song Button -->
  1940. <button
  1941. class="button is-success like-song"
  1942. @click="toggleLike()"
  1943. content="Like Song"
  1944. v-tippy
  1945. >
  1946. <i
  1947. class="material-icons icon-with-button"
  1948. :class="{
  1949. liked: currentSong.liked
  1950. }"
  1951. >thumb_up_alt</i
  1952. >{{ currentSong.likes }}
  1953. </button>
  1954. <!-- Dislike Song Button -->
  1955. <button
  1956. class="button is-danger dislike-song"
  1957. @click="toggleDislike()"
  1958. content="Dislike Song"
  1959. v-tippy
  1960. >
  1961. <i
  1962. class="material-icons icon-with-button"
  1963. :class="{
  1964. disliked: currentSong.disliked
  1965. }"
  1966. >thumb_down_alt</i
  1967. >{{ currentSong.dislikes }}
  1968. </button>
  1969. </div>
  1970. <div id="ratings" class="disabled" v-else>
  1971. <!-- Like Song Button -->
  1972. <button
  1973. class="button is-success like-song disabled"
  1974. content="Ratings have not been loaded yet"
  1975. v-tippy
  1976. >
  1977. <i
  1978. class="material-icons icon-with-button"
  1979. >thumb_up_alt</i
  1980. >
  1981. </button>
  1982. <!-- Dislike Song Button -->
  1983. <button
  1984. class="button is-danger dislike-song disabled"
  1985. content="Ratings have not been loaded yet"
  1986. v-tippy
  1987. >
  1988. <i
  1989. class="material-icons icon-with-button"
  1990. >thumb_down_alt</i
  1991. >
  1992. </button>
  1993. </div>
  1994. <add-to-playlist-dropdown
  1995. :song="currentSong"
  1996. placement="top-end"
  1997. >
  1998. <template #button>
  1999. <div
  2000. id="add-song-to-playlist"
  2001. content="Add Song to Playlist"
  2002. v-tippy
  2003. >
  2004. <div class="control has-addons">
  2005. <button
  2006. class="button is-primary"
  2007. >
  2008. <i class="material-icons">
  2009. playlist_add
  2010. </i>
  2011. </button>
  2012. <button
  2013. class="button"
  2014. id="dropdown-toggle"
  2015. >
  2016. <i class="material-icons">
  2017. {{
  2018. showPlaylistDropdown
  2019. ? "expand_more"
  2020. : "expand_less"
  2021. }}
  2022. </i>
  2023. </button>
  2024. </div>
  2025. </div>
  2026. </template>
  2027. </add-to-playlist-dropdown>
  2028. </div>
  2029. </div>
  2030. <div
  2031. class="player-container quadrant"
  2032. v-show="
  2033. !noSong &&
  2034. (!experimentalChangableListenModeEnabled ||
  2035. experimentalChangableListenMode ===
  2036. 'listen_and_participate')
  2037. "
  2038. >
  2039. <div id="video-container">
  2040. <div
  2041. v-show="currentSongMediaType === 'youtube'"
  2042. >
  2043. <div
  2044. id="youtubeStationPlayer"
  2045. style="
  2046. width: 100%;
  2047. height: 100%;
  2048. min-height: 200px;
  2049. "
  2050. />
  2051. </div>
  2052. <iframe
  2053. v-if="experimental.soundcloud"
  2054. v-show="
  2055. currentSongMediaType === 'soundcloud'
  2056. "
  2057. id="soundcloudStationPlayer"
  2058. ref="soundcloudIframeElement"
  2059. style="
  2060. width: 100%;
  2061. height: 100%;
  2062. min-height: 200px;
  2063. "
  2064. scrolling="no"
  2065. frameborder="no"
  2066. allow="autoplay"
  2067. ></iframe>
  2068. <div
  2069. class="player-fullscreen-message"
  2070. v-if="stationPaused"
  2071. >
  2072. <p>
  2073. This station is currently paused. <br />
  2074. It can only be resumed by a station
  2075. owner, station DJ or a site
  2076. admin/moderator.
  2077. </p>
  2078. </div>
  2079. <div
  2080. class="player-fullscreen-message"
  2081. v-if="!canAutoplay"
  2082. >
  2083. <p>
  2084. Please click anywhere on the screen for
  2085. the video to start
  2086. </p>
  2087. </div>
  2088. </div>
  2089. <div id="seeker-bar-container">
  2090. <div
  2091. id="seeker-bar"
  2092. :class="{
  2093. 'christmas-seeker': christmas,
  2094. nyan:
  2095. currentSong &&
  2096. currentSong.mediaSource ===
  2097. 'youtube:QH2-TGUlwu4'
  2098. }"
  2099. />
  2100. <div
  2101. class="seeker-bar-cover"
  2102. :style="{
  2103. width: `calc(100% - ${seekerbarPercentage}%)`
  2104. }"
  2105. ></div>
  2106. <img
  2107. v-if="
  2108. currentSong &&
  2109. currentSong.mediaSource ===
  2110. 'youtube:QH2-TGUlwu4'
  2111. "
  2112. src="https://freepngimg.com/thumb/nyan_cat/1-2-nyan-cat-free-download-png.png"
  2113. :style="{
  2114. position: 'absolute',
  2115. top: `-10px`,
  2116. left: `${seekerbarPercentage}%`,
  2117. width: '50px'
  2118. }"
  2119. />
  2120. <img
  2121. v-if="
  2122. currentSong &&
  2123. (currentSong.mediaSource ===
  2124. 'youtube:DtVBCG6ThDk' ||
  2125. currentSong.mediaSource ===
  2126. 'youtube:sI66hcu9fIs' ||
  2127. currentSong.mediaSource ===
  2128. 'youtube:iYYRH4apXDo' ||
  2129. currentSong.mediaSource ===
  2130. 'youtube:tRcPA7Fzebw')
  2131. "
  2132. src="/assets/rocket.svg"
  2133. :style="{
  2134. position: 'absolute',
  2135. top: `-21px`,
  2136. left: `calc(${seekerbarPercentage}% - 35px)`,
  2137. width: '50px',
  2138. transform: 'rotate(45deg)'
  2139. }"
  2140. />
  2141. <img
  2142. v-if="
  2143. currentSong &&
  2144. currentSong.mediaSource ===
  2145. 'youtube:jofNR_WkoCE'
  2146. "
  2147. src="/assets/fox.svg"
  2148. :style="{
  2149. position: 'absolute',
  2150. top: `-21px`,
  2151. left: `calc(${seekerbarPercentage}% - 35px)`,
  2152. width: '50px',
  2153. transform: 'scaleX(-1)',
  2154. opacity: 1
  2155. }"
  2156. />
  2157. <img
  2158. v-if="
  2159. currentSong &&
  2160. (currentSong.mediaSource ===
  2161. 'youtube:l9PxOanFjxQ' ||
  2162. currentSong.mediaSource ===
  2163. 'youtube:xKVcVSYmesU' ||
  2164. currentSong.mediaSource ===
  2165. 'youtube:60ItHLz5WEA' ||
  2166. currentSong.mediaSource ===
  2167. 'youtube:e6vkFbtSGm0')
  2168. "
  2169. src="/assets/old_logo.png"
  2170. :style="{
  2171. position: 'absolute',
  2172. top: `-9px`,
  2173. left: `calc(${seekerbarPercentage}% - 22px)`,
  2174. 'background-color': 'rgb(96, 199, 169)',
  2175. width: '25px',
  2176. height: '25px',
  2177. 'border-radius': '25px'
  2178. }"
  2179. />
  2180. <img
  2181. v-if="
  2182. christmas &&
  2183. currentSong &&
  2184. ![
  2185. 'youtube:QH2-TGUlwu4',
  2186. 'youtube:DtVBCG6ThDk',
  2187. 'youtube:sI66hcu9fIs',
  2188. 'youtube:iYYRH4apXDo',
  2189. 'youtube:tRcPA7Fzebw',
  2190. 'youtube:jofNR_WkoCE',
  2191. 'youtube:l9PxOanFjxQ',
  2192. 'youtube:xKVcVSYmesU',
  2193. 'youtube:60ItHLz5WEA',
  2194. 'youtube:e6vkFbtSGm0'
  2195. ].includes(currentSong.mediaSource)
  2196. "
  2197. src="/assets/santa.png"
  2198. :style="{
  2199. position: 'absolute',
  2200. top: `-30px`,
  2201. left: `calc(${seekerbarPercentage}% - 25px)`,
  2202. height: '50px',
  2203. transform: 'scaleX(-1)'
  2204. }"
  2205. />
  2206. </div>
  2207. <div id="control-bar-container">
  2208. <div id="left-buttons">
  2209. <!-- Debug Box -->
  2210. <button
  2211. v-if="frontendDevMode === 'development'"
  2212. class="button is-primary"
  2213. @click="togglePlayerDebugBox()"
  2214. @dblclick="resetPlayerDebugBox()"
  2215. content="Debug"
  2216. v-tippy
  2217. >
  2218. <i
  2219. class="material-icons icon-with-button"
  2220. >
  2221. bug_report
  2222. </i>
  2223. </button>
  2224. <!-- Local Pause/Resume Button -->
  2225. <button
  2226. class="button is-primary"
  2227. @click="resumeLocalStation()"
  2228. id="local-resume"
  2229. v-if="localPaused || autoPaused"
  2230. content="Unpause Playback"
  2231. v-tippy
  2232. >
  2233. <i class="material-icons">play_arrow</i>
  2234. </button>
  2235. <button
  2236. class="button is-primary"
  2237. @click="pauseLocalStation()"
  2238. id="local-pause"
  2239. v-else
  2240. content="Pause Playback"
  2241. v-tippy
  2242. >
  2243. <i class="material-icons">pause</i>
  2244. </button>
  2245. <!-- Vote to Skip Button -->
  2246. <button
  2247. v-if="!skipVotesLoaded"
  2248. class="button is-primary disabled"
  2249. content="Skip votes have not been loaded yet"
  2250. v-tippy
  2251. >
  2252. <i
  2253. class="material-icons icon-with-button"
  2254. >skip_next</i
  2255. >
  2256. </button>
  2257. <button
  2258. v-else-if="loggedIn"
  2259. :class="[
  2260. 'button',
  2261. 'is-primary',
  2262. { voted: currentSong.voted }
  2263. ]"
  2264. @click="toggleSkipVote()"
  2265. :content="`${
  2266. currentSong.voted
  2267. ? 'Remove vote'
  2268. : 'Vote'
  2269. } to Skip Song`"
  2270. v-tippy
  2271. >
  2272. <i
  2273. class="material-icons icon-with-button"
  2274. >skip_next</i
  2275. >
  2276. {{ currentSong.skipVotes }}
  2277. </button>
  2278. <button
  2279. v-else
  2280. class="button is-primary disabled"
  2281. content="Log in to vote to skip songs"
  2282. v-tippy="{ theme: 'info' }"
  2283. >
  2284. <i
  2285. class="material-icons icon-with-button"
  2286. >skip_next</i
  2287. >
  2288. {{ currentSong.skipVotes }}
  2289. </button>
  2290. <!-- Close player window -->
  2291. <button
  2292. v-if="
  2293. experimentalChangableListenModeEnabled
  2294. "
  2295. class="button is-primary"
  2296. content="Close this player window"
  2297. @click="
  2298. experimentalChangableListenModeChange(
  2299. 'participate'
  2300. )
  2301. "
  2302. v-tippy
  2303. >
  2304. <i
  2305. class="material-icons icon-with-button"
  2306. >cancel_presentation</i
  2307. >
  2308. </button>
  2309. </div>
  2310. <div id="duration">
  2311. <p>
  2312. {{ timeElapsed }} /
  2313. {{
  2314. utils.formatTime(
  2315. currentSong.duration
  2316. )
  2317. }}
  2318. </p>
  2319. </div>
  2320. <p id="volume-control" v-if="!isApple">
  2321. <i
  2322. class="material-icons"
  2323. @click="toggleMute()"
  2324. :content="`${
  2325. muted ? 'Unmute' : 'Mute'
  2326. }`"
  2327. v-tippy
  2328. >{{
  2329. muted
  2330. ? "volume_mute"
  2331. : volumeSliderValue >= 50
  2332. ? "volume_up"
  2333. : "volume_down"
  2334. }}</i
  2335. >
  2336. <input
  2337. v-model="volumeSliderValue"
  2338. type="range"
  2339. min="0"
  2340. max="100"
  2341. class="volume-slider active"
  2342. @change="changeVolume()"
  2343. @input="changeVolume()"
  2344. />
  2345. </p>
  2346. <div id="right-buttons" v-if="loggedIn">
  2347. <!-- Ratings (Like/Dislike) Buttons -->
  2348. <div
  2349. id="ratings"
  2350. v-if="ratingsLoaded && ownRatingsLoaded"
  2351. :class="{
  2352. liked: currentSong.liked,
  2353. disliked: currentSong.disliked
  2354. }"
  2355. >
  2356. <!-- Like Song Button -->
  2357. <button
  2358. class="button is-success like-song"
  2359. id="like-song"
  2360. @click="toggleLike()"
  2361. content="Like Song"
  2362. v-tippy
  2363. >
  2364. <i
  2365. class="material-icons icon-with-button"
  2366. :class="{
  2367. liked: currentSong.liked
  2368. }"
  2369. >thumb_up_alt</i
  2370. >{{ currentSong.likes }}
  2371. </button>
  2372. <!-- Dislike Song Button -->
  2373. <button
  2374. class="button is-danger dislike-song"
  2375. id="dislike-song"
  2376. @click="toggleDislike()"
  2377. content="Dislike Song"
  2378. v-tippy
  2379. >
  2380. <i
  2381. class="material-icons icon-with-button"
  2382. :class="{
  2383. disliked:
  2384. currentSong.disliked
  2385. }"
  2386. >thumb_down_alt</i
  2387. >{{ currentSong.dislikes }}
  2388. </button>
  2389. </div>
  2390. <div id="ratings" class="disabled" v-else>
  2391. <!-- Like Song Button -->
  2392. <button
  2393. class="button is-success like-song disabled"
  2394. id="like-song"
  2395. content="Ratings have not been loaded yet"
  2396. v-tippy
  2397. >
  2398. <i
  2399. class="material-icons icon-with-button"
  2400. >thumb_up_alt</i
  2401. >
  2402. </button>
  2403. <!-- Dislike Song Button -->
  2404. <button
  2405. class="button is-danger dislike-song disabled"
  2406. id="dislike-song"
  2407. content="Ratings have not been loaded yet"
  2408. v-tippy
  2409. >
  2410. <i
  2411. class="material-icons icon-with-button"
  2412. >thumb_down_alt</i
  2413. >
  2414. </button>
  2415. </div>
  2416. <!-- Add Song To Playlist Button & Dropdown -->
  2417. <add-to-playlist-dropdown
  2418. :song="currentSong"
  2419. placement="top-end"
  2420. >
  2421. <template #button>
  2422. <div
  2423. id="add-song-to-playlist"
  2424. content="Add Song to Playlist"
  2425. v-tippy
  2426. >
  2427. <div class="control has-addons">
  2428. <button
  2429. class="button is-primary"
  2430. >
  2431. <i
  2432. class="material-icons"
  2433. >
  2434. playlist_add
  2435. </i>
  2436. </button>
  2437. <button
  2438. class="button"
  2439. id="dropdown-toggle"
  2440. >
  2441. <i
  2442. class="material-icons"
  2443. >
  2444. {{
  2445. showPlaylistDropdown
  2446. ? "expand_more"
  2447. : "expand_less"
  2448. }}
  2449. </i>
  2450. </button>
  2451. </div>
  2452. </div>
  2453. </template>
  2454. </add-to-playlist-dropdown>
  2455. </div>
  2456. <div id="right-buttons" v-else>
  2457. <!-- Disabled Ratings (Like/Dislike) Buttons -->
  2458. <div id="ratings" v-if="ratingsLoaded">
  2459. <!-- Disabled Like Song Button -->
  2460. <button
  2461. class="button is-success disabled"
  2462. id="like-song"
  2463. content="Log in to like songs"
  2464. v-tippy="{ theme: 'info' }"
  2465. >
  2466. <i
  2467. class="material-icons icon-with-button"
  2468. >thumb_up_alt</i
  2469. >{{ currentSong.likes }}
  2470. </button>
  2471. <!-- Disabled Dislike Song Button -->
  2472. <button
  2473. class="button is-danger disabled"
  2474. id="dislike-song"
  2475. content="Log in to dislike songs"
  2476. v-tippy="{ theme: 'info' }"
  2477. >
  2478. <i
  2479. class="material-icons icon-with-button"
  2480. >thumb_down_alt</i
  2481. >{{ currentSong.dislikes }}
  2482. </button>
  2483. </div>
  2484. <div id="ratings" v-else>
  2485. <!-- Disabled Like Song Button -->
  2486. <button
  2487. class="button is-success disabled"
  2488. id="like-song"
  2489. content="Ratings have not been loaded yet"
  2490. v-tippy="{ theme: 'info' }"
  2491. >
  2492. <i
  2493. class="material-icons icon-with-button"
  2494. >thumb_up_alt</i
  2495. >
  2496. </button>
  2497. <!-- Disabled Dislike Song Button -->
  2498. <button
  2499. class="button is-danger disabled"
  2500. id="dislike-song"
  2501. content="Ratings have not been loaded yet"
  2502. v-tippy="{ theme: 'info' }"
  2503. >
  2504. <i
  2505. class="material-icons icon-with-button"
  2506. >thumb_down_alt</i
  2507. >
  2508. </button>
  2509. </div>
  2510. <!-- Disabled Add Song To Playlist Button & Dropdown -->
  2511. <div id="add-song-to-playlist">
  2512. <div class="control has-addons">
  2513. <button
  2514. class="button is-primary disabled"
  2515. content="Log in to add songs to playlist"
  2516. v-tippy="{ theme: 'info' }"
  2517. >
  2518. <i class="material-icons"
  2519. >queue</i
  2520. >
  2521. </button>
  2522. </div>
  2523. </div>
  2524. </div>
  2525. </div>
  2526. </div>
  2527. <p
  2528. class="player-container nothing-here-text"
  2529. v-if="noSong"
  2530. >
  2531. No song is currently playing
  2532. </p>
  2533. <div v-if="!noSong" id="current-next-row">
  2534. <div
  2535. id="currently-playing-container"
  2536. class="quadrant"
  2537. :class="{ 'no-currently-playing': noSong }"
  2538. >
  2539. <media-item
  2540. :key="`songItem-currentSong-${currentSong.mediaSource}`"
  2541. :song="currentSong"
  2542. :duration="false"
  2543. :requested-by="true"
  2544. :requested-type="true"
  2545. header="Currently Playing.."
  2546. />
  2547. </div>
  2548. <div
  2549. v-if="nextSong"
  2550. id="next-up-container"
  2551. class="quadrant"
  2552. >
  2553. <media-item
  2554. :key="`songItem-nextSong-${nextSong.mediaSource}`"
  2555. :song="nextSong"
  2556. :duration="false"
  2557. :requested-by="true"
  2558. :requested-type="true"
  2559. header="Next Up.."
  2560. />
  2561. </div>
  2562. </div>
  2563. </div>
  2564. </div>
  2565. </div>
  2566. <main-footer />
  2567. </div>
  2568. <floating-box
  2569. id="player-debug-box"
  2570. ref="playerDebugBox"
  2571. title="Station Debug"
  2572. >
  2573. <template #body>
  2574. <span><b>No song</b>: {{ noSong }}</span>
  2575. <span><b>Song id</b>: {{ currentSong._id }}</span>
  2576. <span><b>Media source</b>: {{ currentSong.mediaSource }}</span>
  2577. <span
  2578. ><b>Media source type</b>: {{ currentSongMediaType }}</span
  2579. >
  2580. <span
  2581. ><b>Media source value</b>:
  2582. {{ currentSongMediaValue }}</span
  2583. >
  2584. <span><b>Duration</b>: {{ currentSong.duration }}</span>
  2585. <span
  2586. ><b>Skip duration</b>: {{ currentSong.skipDuration }}</span
  2587. >
  2588. <span><b>Loading</b>: {{ loading }}</span>
  2589. <span><b>Can autoplay</b>: {{ canAutoplay }}</span>
  2590. <span
  2591. ><b>Youtube player ready</b>: {{ youtubePlayerReady }}</span
  2592. >
  2593. <span
  2594. ><b>Attempts to play video</b>:
  2595. {{ attemptsToPlayVideo }}</span
  2596. >
  2597. <span
  2598. ><b>Last time requested if can autoplay</b>:
  2599. {{ lastTimeRequestedIfCanAutoplay }}</span
  2600. >
  2601. <span><b>Seeking</b>: {{ seeking }}</span>
  2602. <span><b>Playback rate</b>: {{ playbackRate }}</span>
  2603. <span><b>System difference</b>: {{ systemDifference }}</span>
  2604. <span><b>Time before paused</b>: {{ timeBeforePause }}</span>
  2605. <span><b>Time paused</b>: {{ timePaused }}</span>
  2606. <span><b>Time elapsed</b>: {{ timeElapsed }}</span>
  2607. <span><b>Volume slider value</b>: {{ volumeSliderValue }}</span>
  2608. <span><b>Local paused</b>: {{ localPaused }}</span>
  2609. <span><b>Auto paused</b>: {{ autoPaused }}</span>
  2610. <span><b>Station paused</b>: {{ stationPaused }}</span>
  2611. <span :title="new Date(pausedAt).toString()"
  2612. ><b>Paused at</b>: {{ pausedAt }}</span
  2613. >
  2614. <span :title="new Date(startedAt).toString()"
  2615. ><b>Started at</b>: {{ startedAt }}</span
  2616. >
  2617. <span
  2618. ><b>Requests enabled</b>:
  2619. {{ station.requests.enabled }}</span
  2620. >
  2621. <span
  2622. ><b>Requests access</b>: {{ station.requests.access }}</span
  2623. >
  2624. <span><b>Requests limit</b>: {{ station.requests.limit }}</span>
  2625. <span
  2626. ><b>Auto requesting playlists</b>:
  2627. {{
  2628. autoRequest.map(playlist => playlist._id).join(", ")
  2629. }}</span
  2630. >
  2631. <span
  2632. ><b>Autofill enabled</b>:
  2633. {{ station.autofill.enabled }}</span
  2634. >
  2635. <span><b>Autofill limit</b>: {{ station.autofill.limit }}</span>
  2636. <span><b>Autofill mode</b>: {{ station.autofill.mode }}</span>
  2637. <span><b>Skip votes loaded</b>: {{ skipVotesLoaded }}</span>
  2638. <span
  2639. ><b>Skip votes current</b>:
  2640. {{
  2641. currentSong.skipVotesCurrent
  2642. ? currentSong.skipVotesCurrent
  2643. : "N/A"
  2644. }}</span
  2645. >
  2646. <span
  2647. ><b>Skip votes</b>:
  2648. {{ skipVotesLoaded ? currentSong.skipVotes : "N/A" }}</span
  2649. >
  2650. <span><b>Ratings loaded</b>: {{ ratingsLoaded }}</span>
  2651. <span
  2652. ><b>Ratings</b>:
  2653. {{
  2654. ratingsLoaded
  2655. ? `${currentSong.likes} / ${currentSong.dislikes}`
  2656. : "N/A"
  2657. }}</span
  2658. >
  2659. <span><b>Own ratings loaded</b>: {{ ownRatingsLoaded }}</span>
  2660. <span
  2661. ><b>Own ratings</b>:
  2662. {{
  2663. ownRatingsLoaded
  2664. ? `${currentSong.liked} / ${currentSong.disliked}`
  2665. : "N/A"
  2666. }}</span
  2667. >
  2668. </template>
  2669. </floating-box>
  2670. <floating-box
  2671. id="keyboardShortcutsHelper"
  2672. ref="keyboardShortcutsHelper"
  2673. title="Station Keyboard Shortcuts"
  2674. >
  2675. <template #body>
  2676. <div>
  2677. <div
  2678. v-if="
  2679. hasPermission('stations.playback.toggle') ||
  2680. hasPermission('stations.skip')
  2681. "
  2682. >
  2683. <span class="biggest"><b>Owner/DJ</b></span>
  2684. <span><b>Ctrl + Space</b> - Pause/resume station</span>
  2685. <span><b>Ctrl + Numpad right</b> - Skip station</span>
  2686. </div>
  2687. <hr
  2688. v-if="
  2689. hasPermission('stations.playback.toggle') ||
  2690. hasPermission('stations.skip')
  2691. "
  2692. />
  2693. <div>
  2694. <span class="biggest"><b>Volume</b></span>
  2695. <span
  2696. ><b>Ctrl + Numpad up/down</b> - Volume up/down
  2697. 10%</span
  2698. >
  2699. <span
  2700. ><b>Ctrl + Shift + Numpad up/down</b> - Volume
  2701. up/down 10%</span
  2702. >
  2703. </div>
  2704. <hr />
  2705. <div>
  2706. <span class="biggest"><b>Misc</b></span>
  2707. <span
  2708. ><b>Shift + Alt + R</b> - Recalculates the system
  2709. time difference</span
  2710. >
  2711. <span><b>Ctrl + D</b> - Toggles debug box</span>
  2712. <span><b>Ctrl + Shift + D</b> - Resets debug box</span>
  2713. <span
  2714. ><b>Ctrl + /</b> - Toggles keyboard shortcuts
  2715. box</span
  2716. >
  2717. <span
  2718. ><b>Ctrl + Shift + /</b> - Resets keyboard shortcuts
  2719. box</span
  2720. >
  2721. </div>
  2722. </div>
  2723. </template>
  2724. </floating-box>
  2725. <Z404 v-if="!exists"></Z404>
  2726. </div>
  2727. </template>
  2728. <style lang="less">
  2729. #youtubeStationPlayer,
  2730. #soundcloudStationPlayer {
  2731. position: absolute;
  2732. top: 0;
  2733. left: 0;
  2734. width: 100%;
  2735. height: 100%;
  2736. }
  2737. #currently-playing-container,
  2738. #next-up-container {
  2739. .song-item {
  2740. height: 130px !important;
  2741. .thumbnail-and-info .thumbnail {
  2742. min-width: 130px;
  2743. width: 130px;
  2744. }
  2745. }
  2746. }
  2747. #control-bar-container
  2748. #right-buttons
  2749. .tippy-box[data-theme~="dropdown"]
  2750. .nav-dropdown-items {
  2751. padding-bottom: 0 !important;
  2752. }
  2753. </style>
  2754. <style lang="less" scoped>
  2755. #page-loader-container {
  2756. height: inherit;
  2757. #page-loader-content {
  2758. height: inherit;
  2759. position: absolute;
  2760. max-width: 100%;
  2761. width: 1800px;
  2762. transform: translateX(-50%);
  2763. left: 50%;
  2764. }
  2765. #page-loader-layout {
  2766. height: inherit;
  2767. width: 100%;
  2768. }
  2769. }
  2770. #mobile-progress-animation {
  2771. width: 50px;
  2772. animation: rotate 0.8s infinite linear;
  2773. border: 8px solid var(--primary-color);
  2774. border-right-color: transparent;
  2775. border-radius: 50%;
  2776. height: 50px;
  2777. position: absolute;
  2778. top: 50%;
  2779. left: 50%;
  2780. display: none;
  2781. }
  2782. @keyframes rotate {
  2783. 0% {
  2784. transform: rotate(0deg);
  2785. }
  2786. 100% {
  2787. transform: rotate(360deg);
  2788. }
  2789. }
  2790. #keyboardShortcutsHelper {
  2791. .box-body {
  2792. .biggest {
  2793. font-size: 1.4rem;
  2794. }
  2795. > div,
  2796. > div > div {
  2797. display: flex;
  2798. flex-direction: column;
  2799. }
  2800. > div {
  2801. row-gap: 8px;
  2802. }
  2803. }
  2804. }
  2805. .nav,
  2806. .button.is-primary {
  2807. background-color: var(--primary-color) !important;
  2808. }
  2809. .button.is-primary:hover,
  2810. .button.is-primary:focus {
  2811. filter: brightness(90%);
  2812. }
  2813. .night-mode {
  2814. #currently-playing-container,
  2815. #next-up-container,
  2816. #control-bar-container,
  2817. .quadrant:not(#sidebar-container),
  2818. .player-container {
  2819. background-color: var(--dark-grey-3) !important;
  2820. }
  2821. #video-container,
  2822. #control-bar-container,
  2823. .quadrant:not(#sidebar-container),
  2824. .player-container {
  2825. border: 0 !important;
  2826. }
  2827. #seeker-bar-container {
  2828. background-color: var(--dark-grey-3) !important;
  2829. }
  2830. #dropdown-toggle {
  2831. background-color: var(--dark-grey-2) !important;
  2832. border: 0;
  2833. i {
  2834. color: var(--white);
  2835. }
  2836. }
  2837. }
  2838. #station-outer-container {
  2839. margin: 0 auto;
  2840. padding: 20px 40px;
  2841. min-height: calc(100vh - 64px);
  2842. width: 100%;
  2843. max-width: 1800px;
  2844. display: flex;
  2845. #station-inner-container {
  2846. width: 100%;
  2847. min-height: calc(100vh - 428px);
  2848. display: flex;
  2849. flex-direction: row;
  2850. flex-wrap: wrap;
  2851. .row {
  2852. display: flex;
  2853. flex-direction: row;
  2854. max-width: 100%;
  2855. }
  2856. .column {
  2857. display: flex;
  2858. flex-direction: column;
  2859. }
  2860. .quadrant {
  2861. border-radius: @border-radius;
  2862. margin: 10px;
  2863. overflow: hidden;
  2864. }
  2865. .quadrant:not(#sidebar-container) {
  2866. background-color: var(--white);
  2867. border: 1px solid var(--light-grey-3);
  2868. }
  2869. #station-left-column,
  2870. #station-right-column {
  2871. padding: 0;
  2872. }
  2873. #current-next-row {
  2874. display: flex;
  2875. flex-direction: row;
  2876. #currently-playing-container,
  2877. #next-up-container {
  2878. overflow: hidden;
  2879. flex-basis: 50%;
  2880. .song-item {
  2881. border: unset;
  2882. }
  2883. .nothing-here-text {
  2884. height: 100%;
  2885. }
  2886. }
  2887. > div:only-child {
  2888. flex: 1 !important;
  2889. flex-basis: 100% !important;
  2890. }
  2891. }
  2892. .player-container {
  2893. height: inherit;
  2894. background-color: var(--white);
  2895. display: flex;
  2896. flex-direction: column;
  2897. border: 1px solid var(--light-grey-3);
  2898. border-radius: @border-radius;
  2899. overflow: hidden;
  2900. &.nothing-here-text {
  2901. margin: 10px;
  2902. flex: 1;
  2903. min-height: 487px;
  2904. }
  2905. #video-container {
  2906. position: relative;
  2907. aspect-ratio: 16/9;
  2908. overflow: hidden;
  2909. .player-fullscreen-message {
  2910. position: relative;
  2911. width: 100%;
  2912. height: 100%;
  2913. background: var(--primary-color);
  2914. display: flex;
  2915. align-items: center;
  2916. justify-content: center;
  2917. p {
  2918. color: var(--white);
  2919. font-size: 26px;
  2920. text-align: center;
  2921. }
  2922. }
  2923. }
  2924. #seeker-bar-container {
  2925. background-color: var(--white);
  2926. position: relative;
  2927. height: 7px;
  2928. display: block;
  2929. width: 100%;
  2930. #seeker-bar {
  2931. background-color: var(--primary-color);
  2932. top: 0;
  2933. left: 0;
  2934. bottom: 0;
  2935. position: absolute;
  2936. width: 100%;
  2937. }
  2938. .seeker-bar-cover {
  2939. position: absolute;
  2940. top: 0;
  2941. right: 0;
  2942. bottom: 0;
  2943. background-color: inherit;
  2944. }
  2945. }
  2946. #control-bar-container {
  2947. display: flex;
  2948. justify-content: space-around;
  2949. padding: 10px 0;
  2950. width: 100%;
  2951. background: var(--white);
  2952. flex-direction: column;
  2953. flex-flow: wrap;
  2954. .button:not(#dropdown-toggle) {
  2955. width: 75px;
  2956. }
  2957. #left-buttons,
  2958. #right-buttons {
  2959. margin: 3px;
  2960. }
  2961. #left-buttons {
  2962. display: flex;
  2963. .button:not(:first-of-type) {
  2964. margin-left: 5px;
  2965. }
  2966. .disabled {
  2967. filter: grayscale(0.4);
  2968. }
  2969. }
  2970. #duration {
  2971. margin: 3px;
  2972. display: flex;
  2973. align-items: center;
  2974. p {
  2975. font-size: 22px;
  2976. /** prevents duration width slightly varying and shifting other controls slightly */
  2977. width: 150px;
  2978. text-align: center;
  2979. }
  2980. }
  2981. #volume-control {
  2982. margin: 3px;
  2983. margin-top: 0;
  2984. display: flex;
  2985. align-items: center;
  2986. cursor: pointer;
  2987. .volume-slider {
  2988. width: 100%;
  2989. padding: 0 15px;
  2990. background: transparent;
  2991. min-width: 100px;
  2992. }
  2993. input[type="range"] {
  2994. -webkit-appearance: none;
  2995. appearance: none;
  2996. margin: 7.3px 0;
  2997. }
  2998. input[type="range"]:focus {
  2999. outline: none;
  3000. }
  3001. input[type="range"]::-webkit-slider-runnable-track {
  3002. width: 100%;
  3003. height: 5.2px;
  3004. cursor: pointer;
  3005. box-shadow: 0;
  3006. background: var(--light-grey-3);
  3007. border-radius: @border-radius;
  3008. border: 0;
  3009. }
  3010. input[type="range"]::-webkit-slider-thumb {
  3011. box-shadow: 0;
  3012. border: 0;
  3013. height: 19px;
  3014. width: 19px;
  3015. border-radius: 100%;
  3016. background: var(--primary-color);
  3017. cursor: pointer;
  3018. -webkit-appearance: none;
  3019. appearance: none;
  3020. margin-top: -6.5px;
  3021. }
  3022. input[type="range"]::-moz-range-track {
  3023. width: 100%;
  3024. height: 5.2px;
  3025. cursor: pointer;
  3026. box-shadow: 0;
  3027. background: var(--light-grey-3);
  3028. border-radius: @border-radius;
  3029. border: 0;
  3030. }
  3031. input[type="range"]::-moz-range-thumb {
  3032. box-shadow: 0;
  3033. border: 0;
  3034. height: 19px;
  3035. width: 19px;
  3036. border-radius: 100%;
  3037. background: var(--primary-color);
  3038. cursor: pointer;
  3039. -webkit-appearance: none;
  3040. appearance: none;
  3041. margin-top: -6.5px;
  3042. }
  3043. input[type="range"]::-ms-track {
  3044. width: 100%;
  3045. height: 5.2px;
  3046. cursor: pointer;
  3047. box-shadow: 0;
  3048. background: var(--light-grey-3);
  3049. border-radius: @border-radius;
  3050. }
  3051. input[type="range"]::-ms-fill-lower {
  3052. background: var(--light-grey-3);
  3053. border: 0;
  3054. border-radius: 0;
  3055. box-shadow: 0;
  3056. }
  3057. input[type="range"]::-ms-fill-upper {
  3058. background: var(--light-grey-3);
  3059. border: 0;
  3060. border-radius: 0;
  3061. box-shadow: 0;
  3062. }
  3063. input[type="range"]::-ms-thumb {
  3064. box-shadow: 0;
  3065. border: 0;
  3066. height: 15px;
  3067. width: 15px;
  3068. border-radius: 100%;
  3069. background: var(--primary-color);
  3070. cursor: pointer;
  3071. -webkit-appearance: none;
  3072. appearance: none;
  3073. margin-top: 1.5px;
  3074. }
  3075. }
  3076. #right-buttons {
  3077. display: flex;
  3078. #dropdown-toggle {
  3079. width: 35px;
  3080. }
  3081. #dislike-song,
  3082. #add-song-to-playlist .button:not(#dropdown-toggle) {
  3083. margin-left: 5px;
  3084. }
  3085. #ratings {
  3086. display: flex;
  3087. &.liked #dislike-song,
  3088. &.disliked #like-song {
  3089. background-color: var(--grey) !important;
  3090. }
  3091. #like-song.disabled,
  3092. #dislike-song.disabled {
  3093. filter: grayscale(0.4);
  3094. }
  3095. }
  3096. #add-song-to-playlist {
  3097. display: flex;
  3098. flex-direction: column-reverse;
  3099. #nav-dropdown {
  3100. position: absolute;
  3101. margin-left: 4px;
  3102. margin-bottom: 36px;
  3103. .nav-dropdown-items {
  3104. position: relative;
  3105. right: calc(100% - 110px);
  3106. }
  3107. }
  3108. .control {
  3109. width: fit-content;
  3110. margin-bottom: 0 !important;
  3111. button.disabled {
  3112. filter: grayscale(0.4);
  3113. border-radius: @border-radius;
  3114. &::after {
  3115. margin-right: 100%;
  3116. }
  3117. }
  3118. }
  3119. }
  3120. }
  3121. }
  3122. }
  3123. #sidebar-container {
  3124. border-top: 0;
  3125. position: relative;
  3126. height: inherit;
  3127. flex-grow: 1;
  3128. min-height: 350px;
  3129. }
  3130. }
  3131. }
  3132. .footer {
  3133. margin-top: 30px;
  3134. }
  3135. .nyan {
  3136. background: linear-gradient(
  3137. 90deg,
  3138. magenta 0%,
  3139. red 15%,
  3140. orange 30%,
  3141. yellow 45%,
  3142. lime 60%,
  3143. cyan 75%,
  3144. blue 90%,
  3145. magenta 100%
  3146. );
  3147. background-size: 200%;
  3148. animation: nyanMoving 4s linear infinite;
  3149. }
  3150. @keyframes nyanMoving {
  3151. 0% {
  3152. background-position: 0% 0%;
  3153. }
  3154. 100% {
  3155. background-position: -200% 0%;
  3156. }
  3157. }
  3158. .christmas-seeker {
  3159. background: repeating-linear-gradient(
  3160. -45deg,
  3161. var(--white) 0 1rem,
  3162. var(--dark-red) 1rem 2rem
  3163. );
  3164. background-size: 200% 100%;
  3165. animation: christmas 20s linear infinite;
  3166. }
  3167. @keyframes christmas {
  3168. 100% {
  3169. background-position: 80% 100%;
  3170. }
  3171. }
  3172. .bg-bubbles {
  3173. top: 0;
  3174. left: 0;
  3175. width: 100%;
  3176. height: 100%;
  3177. position: absolute;
  3178. z-index: -1;
  3179. margin: 0px;
  3180. pointer-events: none;
  3181. }
  3182. .bg-bubbles li {
  3183. position: absolute;
  3184. list-style: none;
  3185. display: block;
  3186. width: 40px;
  3187. height: 40px;
  3188. border-radius: 100px;
  3189. background-color: var(--primary-color);
  3190. opacity: 0.15;
  3191. bottom: 0px;
  3192. -webkit-animation: square 25s infinite;
  3193. animation: square 25s infinite;
  3194. -webkit-transition-timing-function: linear;
  3195. transition-timing-function: linear;
  3196. }
  3197. .bg-bubbles li:nth-child(1) {
  3198. left: 10%;
  3199. }
  3200. .bg-bubbles li:nth-child(2) {
  3201. left: 20%;
  3202. width: 80px;
  3203. height: 80px;
  3204. -webkit-animation-delay: 2s;
  3205. animation-delay: 2s;
  3206. -webkit-animation-duration: 17s;
  3207. animation-duration: 17s;
  3208. }
  3209. .bg-bubbles li:nth-child(3) {
  3210. left: 25%;
  3211. -webkit-animation-delay: 4s;
  3212. animation-delay: 4s;
  3213. }
  3214. .bg-bubbles li:nth-child(4) {
  3215. left: 40%;
  3216. width: 60px;
  3217. height: 60px;
  3218. -webkit-animation-duration: 22s;
  3219. animation-duration: 22s;
  3220. background-color: var(--primary-color);
  3221. opacity: 0.25;
  3222. }
  3223. .bg-bubbles li:nth-child(5) {
  3224. left: 70%;
  3225. }
  3226. .bg-bubbles li:nth-child(6) {
  3227. left: 80%;
  3228. width: 120px;
  3229. height: 120px;
  3230. -webkit-animation-delay: 3s;
  3231. animation-delay: 3s;
  3232. background-color: var(--primary-color);
  3233. opacity: 0.2;
  3234. }
  3235. .bg-bubbles li:nth-child(7) {
  3236. left: 32%;
  3237. width: 160px;
  3238. height: 160px;
  3239. -webkit-animation-delay: 7s;
  3240. animation-delay: 7s;
  3241. }
  3242. .bg-bubbles li:nth-child(8) {
  3243. left: 55%;
  3244. width: 20px;
  3245. height: 20px;
  3246. -webkit-animation-delay: 15s;
  3247. animation-delay: 15s;
  3248. -webkit-animation-duration: 40s;
  3249. animation-duration: 40s;
  3250. }
  3251. .bg-bubbles li:nth-child(9) {
  3252. left: 25%;
  3253. width: 10px;
  3254. height: 10px;
  3255. -webkit-animation-delay: 2s;
  3256. animation-delay: 2s;
  3257. -webkit-animation-duration: 40s;
  3258. animation-duration: 40s;
  3259. background-color: var(--primary-color);
  3260. opacity: 0.3;
  3261. }
  3262. .bg-bubbles li:nth-child(10) {
  3263. left: 80%;
  3264. width: 160px;
  3265. height: 160px;
  3266. -webkit-animation-delay: 11s;
  3267. animation-delay: 11s;
  3268. }
  3269. .experimental-listen-mode-container {
  3270. display: flex;
  3271. flex-direction: column;
  3272. justify-content: center;
  3273. row-gap: 16px;
  3274. padding: 16px 16px;
  3275. .row {
  3276. display: flex;
  3277. flex-direction: row;
  3278. column-gap: 16px;
  3279. .ratings {
  3280. flex: 2;
  3281. display: flex;
  3282. flex-direction: row;
  3283. column-gap: 16px;
  3284. button {
  3285. flex: 1;
  3286. }
  3287. }
  3288. .addToPlaylistDropdown {
  3289. flex: 1;
  3290. .button.is-primary {
  3291. flex: 1;
  3292. }
  3293. }
  3294. }
  3295. }
  3296. /* Tablet view fix */
  3297. @media (max-width: 768px) {
  3298. .bg-bubbles li:nth-child(10) {
  3299. display: none;
  3300. }
  3301. .experimental-listen-mode-container {
  3302. row-gap: 8px;
  3303. .row {
  3304. column-gap: 8px;
  3305. .ratings {
  3306. column-gap: 8px;
  3307. }
  3308. }
  3309. }
  3310. }
  3311. @-webkit-keyframes square {
  3312. 0% {
  3313. -webkit-transform: translateY(0);
  3314. transform: translateY(0);
  3315. }
  3316. 100% {
  3317. -webkit-transform: translateY(-700px) rotate(600deg);
  3318. transform: translateY(-700px) rotate(600deg);
  3319. }
  3320. }
  3321. @keyframes square {
  3322. 0% {
  3323. -webkit-transform: translateY(0);
  3324. transform: translateY(0);
  3325. }
  3326. 100% {
  3327. -webkit-transform: translateY(-700px) rotate(600deg);
  3328. transform: translateY(-700px) rotate(600deg);
  3329. }
  3330. }
  3331. :deep(.nothing-here-text) {
  3332. display: flex;
  3333. align-items: center;
  3334. justify-content: center;
  3335. }
  3336. @media (min-width: 1500px) {
  3337. #station-left-column {
  3338. max-width: 650px;
  3339. }
  3340. #station-right-column {
  3341. max-width: calc(100% - 650px);
  3342. }
  3343. }
  3344. @media (max-width: 1700px) {
  3345. #current-next-row {
  3346. flex-direction: column !important;
  3347. > div {
  3348. flex: 1 !important;
  3349. }
  3350. }
  3351. }
  3352. @media (max-width: 1500px) {
  3353. #mobile-progress-animation {
  3354. display: block;
  3355. }
  3356. #page-loader-container {
  3357. display: none;
  3358. }
  3359. #station-outer-container {
  3360. max-width: 1500px;
  3361. #station-inner-container {
  3362. flex-direction: row;
  3363. #station-left-column {
  3364. #about-station-container #admin-buttons {
  3365. flex-wrap: wrap;
  3366. }
  3367. #sidebar-container {
  3368. min-height: 350px;
  3369. }
  3370. }
  3371. #station-right-column {
  3372. overflow: hidden;
  3373. #current-next-row {
  3374. flex-direction: column;
  3375. }
  3376. #control-bar-container {
  3377. #duration,
  3378. #volume-control,
  3379. #right-buttons,
  3380. #left-buttons {
  3381. margin-bottom: 5px;
  3382. justify-content: center;
  3383. }
  3384. #duration {
  3385. order: 1;
  3386. }
  3387. #volume-control {
  3388. order: 2;
  3389. max-width: 400px;
  3390. }
  3391. #right-buttons {
  3392. order: 3;
  3393. flex-wrap: wrap;
  3394. #ratings {
  3395. flex-wrap: wrap;
  3396. }
  3397. }
  3398. #left-buttons {
  3399. order: 4;
  3400. flex-wrap: wrap;
  3401. }
  3402. }
  3403. }
  3404. }
  3405. }
  3406. }
  3407. @media (max-width: 1200px) {
  3408. #station-outer-container {
  3409. max-width: 900px;
  3410. padding: 0;
  3411. #station-inner-container {
  3412. flex-direction: column-reverse;
  3413. flex-wrap: nowrap;
  3414. #station-right-column {
  3415. overflow: initial;
  3416. }
  3417. }
  3418. }
  3419. }
  3420. @media (max-width: 990px) {
  3421. #station-outer-container {
  3422. min-height: calc(
  3423. 100vh - 256px
  3424. ); // Height of nav (64px) + height of footer (190px)
  3425. }
  3426. }
  3427. </style>