index.vue 70 KB

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