index.vue 60 KB

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