index.vue 70 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834183518361837183818391840184118421843184418451846184718481849185018511852185318541855185618571858185918601861186218631864186518661867186818691870187118721873187418751876187718781879188018811882188318841885188618871888188918901891189218931894189518961897189818991900190119021903190419051906190719081909191019111912191319141915191619171918191919201921192219231924192519261927192819291930193119321933193419351936193719381939194019411942194319441945194619471948194919501951195219531954195519561957195819591960196119621963196419651966196719681969197019711972197319741975197619771978197919801981198219831984198519861987198819891990199119921993199419951996199719981999200020012002200320042005200620072008200920102011201220132014201520162017201820192020202120222023202420252026202720282029203020312032203320342035203620372038203920402041204220432044204520462047204820492050205120522053205420552056205720582059206020612062206320642065206620672068206920702071207220732074207520762077207820792080208120822083208420852086208720882089209020912092209320942095209620972098209921002101210221032104210521062107210821092110211121122113211421152116211721182119212021212122212321242125212621272128212921302131213221332134213521362137213821392140214121422143214421452146214721482149215021512152215321542155215621572158215921602161216221632164216521662167216821692170217121722173217421752176217721782179218021812182218321842185218621872188218921902191219221932194219521962197219821992200220122022203220422052206220722082209221022112212221322142215221622172218221922202221222222232224222522262227222822292230223122322233223422352236223722382239224022412242224322442245224622472248224922502251225222532254225522562257225822592260226122622263226422652266226722682269227022712272227322742275227622772278227922802281228222832284228522862287228822892290229122922293229422952296229722982299230023012302230323042305230623072308230923102311231223132314231523162317231823192320232123222323232423252326232723282329233023312332233323342335233623372338233923402341234223432344234523462347234823492350235123522353235423552356235723582359236023612362236323642365236623672368236923702371237223732374237523762377237823792380238123822383238423852386238723882389239023912392239323942395239623972398239924002401240224032404240524062407240824092410241124122413241424152416241724182419242024212422242324242425242624272428242924302431243224332434243524362437243824392440244124422443244424452446244724482449245024512452245324542455245624572458245924602461246224632464246524662467246824692470247124722473247424752476247724782479248024812482248324842485248624872488248924902491249224932494249524962497249824992500250125022503250425052506250725082509251025112512251325142515251625172518251925202521252225232524252525262527252825292530253125322533253425352536253725382539254025412542254325442545254625472548254925502551255225532554255525562557255825592560256125622563256425652566256725682569257025712572257325742575257625772578257925802581258225832584258525862587258825892590259125922593259425952596259725982599260026012602260326042605260626072608260926102611261226132614261526162617261826192620262126222623262426252626262726282629263026312632263326342635263626372638263926402641264226432644264526462647264826492650265126522653265426552656265726582659266026612662266326642665266626672668266926702671267226732674267526762677267826792680268126822683268426852686268726882689269026912692269326942695269626972698269927002701270227032704270527062707270827092710271127122713271427152716271727182719272027212722272327242725272627272728272927302731273227332734273527362737273827392740274127422743274427452746274727482749275027512752275327542755275627572758275927602761276227632764276527662767276827692770277127722773277427752776277727782779278027812782278327842785278627872788278927902791279227932794279527962797279827992800280128022803280428052806280728082809281028112812281328142815281628172818281928202821282228232824282528262827282828292830283128322833283428352836283728382839284028412842284328442845284628472848284928502851285228532854285528562857285828592860286128622863286428652866286728682869287028712872287328742875287628772878287928802881288228832884288528862887288828892890289128922893289428952896289728982899290029012902290329042905290629072908290929102911291229132914291529162917291829192920292129222923292429252926292729282929293029312932293329342935293629372938293929402941294229432944294529462947294829492950295129522953295429552956295729582959296029612962296329642965296629672968296929702971297229732974297529762977297829792980298129822983298429852986298729882989299029912992299329942995299629972998299930003001300230033004300530063007
  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 => state.modals.editSong.video.paused,
  883. paused => {
  884. if (paused && !this.beforeEditSongModalLocalPaused) {
  885. this.resumeLocalStation();
  886. } else if (!paused) {
  887. this.beforeEditSongModalLocalPaused = this.localPaused;
  888. this.pauseLocalStation();
  889. }
  890. }
  891. );
  892. window.scrollTo(0, 0);
  893. Date.currently = () => new Date().getTime() + this.systemDifference;
  894. this.stationIdentifier = this.$route.params.id;
  895. window.stationInterval = 0;
  896. this.activityWatchVideoDataInterval = setInterval(() => {
  897. this.sendActivityWatchVideoData();
  898. }, 1000);
  899. this.persistentToastCheckerInterval = setInterval(() => {
  900. this.persistentToasts.filter(
  901. persistentToast => !persistentToast.checkIfCanRemove()
  902. );
  903. }, 1000);
  904. if (this.socket.readyState === 1) this.join();
  905. ws.onConnect(() => {
  906. this.socketConnected = true;
  907. clearTimeout(window.stationNextSongTimeout);
  908. this.join();
  909. });
  910. ws.onDisconnect(true, () => {
  911. this.socketConnected = false;
  912. const { currentSong } = this.currentSong;
  913. if (this.nextSong)
  914. this.setNextCurrentSong(
  915. {
  916. currentSong: this.nextSong,
  917. startedAt: Date.now() + this.getTimeRemaining(),
  918. paused: false,
  919. timePaused: 0
  920. },
  921. true
  922. );
  923. else
  924. this.setNextCurrentSong(
  925. {
  926. currentSong: null,
  927. startedAt: 0,
  928. paused: false,
  929. timePaused: 0,
  930. pausedAt: 0
  931. },
  932. true
  933. );
  934. window.stationNextSongTimeout = setTimeout(() => {
  935. if (!this.noSong && this.currentSong._id === currentSong._id)
  936. this.skipSong("window.stationNextSongTimeout 2");
  937. }, this.getTimeRemaining());
  938. });
  939. this.frontendDevMode = await lofig.get("mode");
  940. this.mediasession = await lofig.get("siteSettings.mediasession");
  941. this.christmas = await lofig.get("siteSettings.christmas");
  942. this.sitename = await lofig.get("siteSettings.sitename");
  943. this.socket.dispatch(
  944. "stations.existsByName",
  945. this.stationIdentifier,
  946. res => {
  947. if (res.status === "error" || !res.data.exists) {
  948. // station identifier may be using stationid instead
  949. this.socket.dispatch(
  950. "stations.existsById",
  951. this.stationIdentifier,
  952. res => {
  953. if (res.status === "error" || !res.data.exists) {
  954. this.loading = false;
  955. this.exists = false;
  956. }
  957. }
  958. );
  959. }
  960. }
  961. );
  962. ms.setListeners(0, {
  963. play: () => {
  964. if (this.isOwnerOrAdmin()) this.resumeStation();
  965. else this.resumeLocalStation();
  966. },
  967. pause: () => {
  968. if (this.isOwnerOrAdmin()) this.pauseStation();
  969. else this.pauseLocalStation();
  970. },
  971. nexttrack: () => {
  972. if (this.isOwnerOrAdmin()) this.skipStation();
  973. else this.voteSkipStation();
  974. }
  975. });
  976. this.socket.on("event:station.nextSong", res => {
  977. const { currentSong, startedAt, paused, timePaused } = res.data;
  978. this.setCurrentSong({
  979. currentSong,
  980. startedAt,
  981. paused,
  982. timePaused,
  983. pausedAt: 0
  984. });
  985. });
  986. this.socket.on("event:station.pause", res => {
  987. this.pausedAt = res.data.pausedAt;
  988. this.updateStationPaused(true);
  989. this.pauseLocalPlayer();
  990. clearTimeout(window.stationNextSongTimeout);
  991. });
  992. this.socket.on("event:station.resume", res => {
  993. this.timePaused = res.data.timePaused;
  994. this.updateStationPaused(false);
  995. if (!this.localPaused) this.resumeLocalPlayer();
  996. });
  997. this.socket.on("event:station.deleted", () => {
  998. window.location.href = "/?msg=The station you were in was deleted.";
  999. return true;
  1000. });
  1001. this.socket.on("event:song.liked", res => {
  1002. if (!this.noSong) {
  1003. if (res.data.youtubeId === this.currentSong.youtubeId) {
  1004. this.updateCurrentSongRatings(res.data);
  1005. }
  1006. }
  1007. });
  1008. this.socket.on("event:song.disliked", res => {
  1009. if (!this.noSong) {
  1010. if (res.data.youtubeId === this.currentSong.youtubeId) {
  1011. this.updateCurrentSongRatings(res.data);
  1012. }
  1013. }
  1014. });
  1015. this.socket.on("event:song.unliked", res => {
  1016. if (!this.noSong) {
  1017. if (res.data.youtubeId === this.currentSong.youtubeId) {
  1018. this.updateCurrentSongRatings(res.data);
  1019. }
  1020. }
  1021. });
  1022. this.socket.on("event:song.undisliked", res => {
  1023. if (!this.noSong) {
  1024. if (res.data.youtubeId === this.currentSong.youtubeId) {
  1025. this.updateCurrentSongRatings(res.data);
  1026. }
  1027. }
  1028. });
  1029. this.socket.on("event:song.ratings.updated", res => {
  1030. if (!this.noSong) {
  1031. if (res.data.youtubeId === this.currentSong.youtubeId) {
  1032. this.updateOwnCurrentSongRatings(res.data);
  1033. }
  1034. }
  1035. });
  1036. this.socket.on("event:station.queue.updated", res => {
  1037. this.updateSongsList(res.data.queue);
  1038. let nextSong = null;
  1039. if (this.songsList[0])
  1040. nextSong = this.songsList[0].youtubeId
  1041. ? this.songsList[0]
  1042. : null;
  1043. this.updateNextSong(nextSong);
  1044. });
  1045. this.socket.on("event:station.queue.song.repositioned", res => {
  1046. this.repositionSongInList(res.data.song);
  1047. let nextSong = null;
  1048. if (this.songsList[0])
  1049. nextSong = this.songsList[0].youtubeId
  1050. ? this.songsList[0]
  1051. : null;
  1052. this.updateNextSong(nextSong);
  1053. });
  1054. this.socket.on("event:station.voteSkipSong", () => {
  1055. if (this.currentSong)
  1056. this.updateCurrentSongSkipVotes({
  1057. skipVotes: this.currentSong.skipVotes + 1,
  1058. skipVotesCurrent: null
  1059. });
  1060. });
  1061. this.socket.on("event:station.updated", async res => {
  1062. const { name, theme, privacy } = res.data.station;
  1063. if (!this.isOwnerOrAdmin() && privacy === "private") {
  1064. window.location.href =
  1065. "/?msg=The station you were in was made private.";
  1066. } else {
  1067. if (this.station.name !== name) {
  1068. await this.$router.push(
  1069. `${name}?${Object.keys(this.$route.query)
  1070. .map(
  1071. key =>
  1072. `${encodeURIComponent(
  1073. key
  1074. )}=${encodeURIComponent(
  1075. this.$route.query[key]
  1076. )}`
  1077. )
  1078. .join("&")}`
  1079. );
  1080. // eslint-disable-next-line no-restricted-globals
  1081. history.replaceState({ ...history.state, ...{} }, null);
  1082. }
  1083. if (this.station.theme !== theme)
  1084. document.getElementsByTagName(
  1085. "html"
  1086. )[0].style.cssText = `--primary-color: var(--${theme})`;
  1087. this.updateStation(res.data.station);
  1088. }
  1089. });
  1090. this.socket.on("event:station.users.updated", res =>
  1091. this.updateUsers(res.data.users)
  1092. );
  1093. this.socket.on("event:station.userCount.updated", res =>
  1094. this.updateUserCount(res.data.userCount)
  1095. );
  1096. this.socket.on("event:user.station.favorited", res => {
  1097. if (res.data.stationId === this.station._id)
  1098. this.updateIfStationIsFavorited({ isFavorited: true });
  1099. });
  1100. this.socket.on("event:user.station.unfavorited", res => {
  1101. if (res.data.stationId === this.station._id)
  1102. this.updateIfStationIsFavorited({ isFavorited: false });
  1103. });
  1104. if (JSON.parse(localStorage.getItem("muted"))) {
  1105. this.muted = true;
  1106. this.player.setVolume(0);
  1107. this.volumeSliderValue = 0;
  1108. } else {
  1109. let volume = parseFloat(localStorage.getItem("volume"));
  1110. volume =
  1111. typeof volume === "number" && !Number.isNaN(volume)
  1112. ? volume
  1113. : 20;
  1114. localStorage.setItem("volume", volume);
  1115. this.volumeSliderValue = volume;
  1116. }
  1117. },
  1118. beforeUnmount() {
  1119. document.getElementsByTagName("html")[0].style.cssText = "";
  1120. if (this.mediasession) {
  1121. ms.removeListeners(0);
  1122. ms.removeMediaSessionData(0);
  1123. }
  1124. /** Reset Songslist */
  1125. this.updateSongsList([]);
  1126. const shortcutNames = [
  1127. "station.pauseResume",
  1128. "station.skipStation",
  1129. "station.lowerVolumeLarge",
  1130. "station.lowerVolumeSmall",
  1131. "station.increaseVolumeLarge",
  1132. "station.increaseVolumeSmall",
  1133. "station.toggleDebug"
  1134. ];
  1135. shortcutNames.forEach(shortcutName => {
  1136. keyboardShortcuts.unregisterShortcut(shortcutName);
  1137. });
  1138. this.editSongModalWatcher(); // removes the watcher
  1139. clearInterval(this.activityWatchVideoDataInterval);
  1140. clearTimeout(window.stationNextSongTimeout);
  1141. clearTimeout(this.persistentToastCheckerInterval);
  1142. this.persistentToasts.forEach(persistentToast => {
  1143. persistentToast.toast.destroy();
  1144. });
  1145. this.socket.dispatch("stations.leave", this.station._id, () => {});
  1146. this.leaveStation();
  1147. },
  1148. methods: {
  1149. isOwnerOnly() {
  1150. return this.loggedIn && this.userId === this.station.owner;
  1151. },
  1152. isAdminOnly() {
  1153. return this.loggedIn && this.role === "admin";
  1154. },
  1155. isOwnerOrAdmin() {
  1156. return this.isOwnerOnly() || this.isAdminOnly();
  1157. },
  1158. updateMediaSessionData(currentSong) {
  1159. if (currentSong) {
  1160. ms.setMediaSessionData(
  1161. 0,
  1162. !this.localPaused && !this.stationPaused, // This should be improved later
  1163. this.currentSong.title,
  1164. this.currentSong.artists.join(", "),
  1165. null,
  1166. this.currentSong.thumbnail
  1167. );
  1168. } else ms.removeMediaSessionData(0);
  1169. },
  1170. removeFromQueue(youtubeId) {
  1171. window.socket.dispatch(
  1172. "stations.removeFromQueue",
  1173. this.station._id,
  1174. youtubeId,
  1175. res => {
  1176. if (res.status === "success") {
  1177. new Toast("Successfully removed song from the queue.");
  1178. } else new Toast(res.message);
  1179. }
  1180. );
  1181. },
  1182. setNextCurrentSong(nextCurrentSong, skipSkipCheck = false) {
  1183. this.nextCurrentSong = nextCurrentSong;
  1184. // If skipSkipCheck is true, it won't try to skip the song
  1185. if (this.getTimeRemaining() <= 0 && !skipSkipCheck) {
  1186. this.skipSong();
  1187. }
  1188. },
  1189. skipSong() {
  1190. if (this.nextCurrentSong && this.nextCurrentSong.currentSong) {
  1191. const songsList = this.songsList.concat([]);
  1192. if (
  1193. songsList.length > 0 &&
  1194. songsList[0].youtubeId ===
  1195. this.nextCurrentSong.currentSong.youtubeId
  1196. ) {
  1197. songsList.splice(0, 1);
  1198. this.updateSongsList(songsList);
  1199. }
  1200. this.setCurrentSong(this.nextCurrentSong);
  1201. } else {
  1202. this.setCurrentSong({
  1203. currentSong: null,
  1204. startedAt: 0,
  1205. paused: this.stationPaused,
  1206. timePaused: 0,
  1207. pausedAt: 0
  1208. });
  1209. }
  1210. },
  1211. setCurrentSong(data) {
  1212. const { currentSong, startedAt, paused, timePaused, pausedAt } =
  1213. data;
  1214. if (currentSong) {
  1215. if (!currentSong.skipDuration || currentSong.skipDuration < 0)
  1216. currentSong.skipDuration = 0;
  1217. if (!currentSong.duration || currentSong.duration < 0)
  1218. currentSong.duration = 0;
  1219. }
  1220. this.updateCurrentSong(currentSong || {});
  1221. let nextSong = null;
  1222. if (this.songsList[0])
  1223. nextSong = this.songsList[0].youtubeId
  1224. ? this.songsList[0]
  1225. : null;
  1226. this.updateNextSong(nextSong);
  1227. this.setNextCurrentSong(
  1228. {
  1229. currentSong: null,
  1230. startedAt: 0,
  1231. paused,
  1232. timePaused: 0,
  1233. pausedAt: 0
  1234. },
  1235. true
  1236. );
  1237. clearTimeout(window.stationNextSongTimeout);
  1238. if (this.mediasession) this.updateMediaSessionData(currentSong);
  1239. this.startedAt = startedAt;
  1240. this.updateStationPaused(paused);
  1241. this.timePaused = timePaused;
  1242. this.pausedAt = pausedAt;
  1243. if (currentSong) {
  1244. this.updateNoSong(false);
  1245. if (!this.playerReady) this.youtubeReady();
  1246. else this.playVideo();
  1247. // 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
  1248. if (!this.stationPaused && !this.socketConnected) {
  1249. if (this.nextSong)
  1250. this.setNextCurrentSong(
  1251. {
  1252. currentSong: this.nextSong,
  1253. startedAt: Date.now() + this.getTimeRemaining(),
  1254. paused: false,
  1255. timePaused: 0
  1256. },
  1257. true
  1258. );
  1259. else
  1260. this.setNextCurrentSong(
  1261. {
  1262. currentSong: null,
  1263. startedAt: 0,
  1264. paused: false,
  1265. timePaused: 0,
  1266. pausedAt: 0
  1267. },
  1268. true
  1269. );
  1270. window.stationNextSongTimeout = setTimeout(() => {
  1271. if (
  1272. !this.noSong &&
  1273. this.currentSong._id === currentSong._id
  1274. )
  1275. this.skipSong("window.stationNextSongTimeout 1");
  1276. }, this.getTimeRemaining());
  1277. }
  1278. const currentSongId = this.currentSong._id;
  1279. this.socket.dispatch(
  1280. "stations.getSkipVotes",
  1281. this.station._id,
  1282. currentSongId,
  1283. res => {
  1284. if (res.status === "success") {
  1285. const { skipVotes, skipVotesCurrent } = res.data;
  1286. if (
  1287. !this.noSong &&
  1288. this.currentSong._id === currentSongId
  1289. ) {
  1290. this.updateCurrentSongSkipVotes({
  1291. skipVotes,
  1292. skipVotesCurrent
  1293. });
  1294. }
  1295. }
  1296. }
  1297. );
  1298. this.socket.dispatch(
  1299. "songs.getSongRatings",
  1300. currentSong._id,
  1301. res => {
  1302. if (currentSong._id === this.currentSong._id) {
  1303. const { likes, dislikes } = res.data;
  1304. this.updateCurrentSongRatings({ likes, dislikes });
  1305. }
  1306. }
  1307. );
  1308. if (this.loggedIn) {
  1309. this.socket.dispatch(
  1310. "songs.getOwnSongRatings",
  1311. currentSong.youtubeId,
  1312. res => {
  1313. console.log("getOwnSongRatings", res);
  1314. if (
  1315. res.status === "success" &&
  1316. this.currentSong.youtubeId ===
  1317. res.data.youtubeId
  1318. ) {
  1319. this.updateOwnCurrentSongRatings(res.data);
  1320. if (
  1321. this.autoSkipDisliked &&
  1322. res.data.disliked === true
  1323. ) {
  1324. this.voteSkipStation();
  1325. new Toast(
  1326. "Automatically voted to skip disliked song."
  1327. );
  1328. }
  1329. }
  1330. }
  1331. );
  1332. }
  1333. } else {
  1334. if (this.playerReady) this.player.stopVideo();
  1335. this.updateNoSong(true);
  1336. }
  1337. this.calculateTimeElapsed();
  1338. this.resizeSeekerbar();
  1339. },
  1340. youtubeReady() {
  1341. if (!this.player) {
  1342. ms.setYTReady(false);
  1343. this.player = new window.YT.Player("stationPlayer", {
  1344. height: 270,
  1345. width: 480,
  1346. videoId: this.currentSong.youtubeId,
  1347. host: "https://www.youtube-nocookie.com",
  1348. startSeconds:
  1349. this.getTimeElapsed() / 1000 +
  1350. this.currentSong.skipDuration,
  1351. playerVars: {
  1352. controls: 0,
  1353. iv_load_policy: 3,
  1354. rel: 0,
  1355. showinfo: 0,
  1356. disablekb: 1,
  1357. playsinline: 1
  1358. },
  1359. events: {
  1360. onReady: () => {
  1361. this.playerReady = true;
  1362. ms.setYTReady(true);
  1363. let volume = parseFloat(
  1364. localStorage.getItem("volume")
  1365. );
  1366. volume = typeof volume === "number" ? volume : 20;
  1367. this.player.setVolume(volume);
  1368. if (volume > 0) this.player.unMute();
  1369. if (this.muted) this.player.mute();
  1370. this.playVideo();
  1371. // on ios, playback will be forcibly paused locally
  1372. if (this.isApple) {
  1373. this.updateLocalPaused(true);
  1374. new Toast(
  1375. `Please click play manually to use ${this.sitename} on iOS.`
  1376. );
  1377. }
  1378. },
  1379. onError: err => {
  1380. console.log("error with youtube video", err);
  1381. if (err.data === 150 && this.loggedIn) {
  1382. new Toast(
  1383. "Automatically voted to skip as this song isn't available for you."
  1384. );
  1385. // automatically vote to skip
  1386. this.voteSkipStation();
  1387. // persistent message while song is playing
  1388. const persistentToast = new Toast({
  1389. content:
  1390. "This song is unavailable for you, but is playing for everyone else.",
  1391. persistent: true
  1392. });
  1393. // save current song id
  1394. const erroredYoutubeId =
  1395. this.currentSong.youtubeId;
  1396. this.persistentToasts.push({
  1397. toast: persistentToast,
  1398. checkIfCanRemove: () => {
  1399. if (
  1400. this.currentSong.youtubeId !==
  1401. erroredYoutubeId
  1402. ) {
  1403. persistentToast.destroy();
  1404. return true;
  1405. }
  1406. return false;
  1407. }
  1408. });
  1409. } else {
  1410. new Toast(
  1411. "There has been an error with the YouTube Embed"
  1412. );
  1413. }
  1414. },
  1415. onStateChange: event => {
  1416. if (
  1417. event.data === window.YT.PlayerState.PLAYING &&
  1418. this.videoLoading === true
  1419. ) {
  1420. this.videoLoading = false;
  1421. this.player.seekTo(
  1422. this.getTimeElapsed() / 1000 +
  1423. this.currentSong.skipDuration,
  1424. true
  1425. );
  1426. this.canAutoplay = true;
  1427. if (this.localPaused || this.stationPaused)
  1428. this.player.pauseVideo();
  1429. } else if (
  1430. event.data === window.YT.PlayerState.PLAYING &&
  1431. (this.localPaused || this.stationPaused)
  1432. ) {
  1433. this.player.seekTo(
  1434. this.timeBeforePause / 1000,
  1435. true
  1436. );
  1437. this.player.pauseVideo();
  1438. } else if (
  1439. event.data === window.YT.PlayerState.PLAYING &&
  1440. this.seeking === true
  1441. )
  1442. this.seeking = false;
  1443. if (
  1444. event.data === window.YT.PlayerState.PAUSED &&
  1445. !this.localPaused &&
  1446. !this.stationPaused &&
  1447. !this.noSong &&
  1448. this.player.getDuration() / 1000 <
  1449. this.currentSong.duration
  1450. ) {
  1451. this.player.seekTo(
  1452. this.getTimeElapsed() / 1000 +
  1453. this.currentSong.skipDuration,
  1454. true
  1455. );
  1456. this.player.playVideo();
  1457. }
  1458. }
  1459. }
  1460. });
  1461. }
  1462. },
  1463. getTimeElapsed() {
  1464. if (this.currentSong) {
  1465. let { timePaused } = this;
  1466. if (this.stationPaused)
  1467. timePaused += Date.currently() - this.pausedAt;
  1468. return Date.currently() - this.startedAt - timePaused;
  1469. }
  1470. return 0;
  1471. },
  1472. getTimeRemaining() {
  1473. if (this.currentSong) {
  1474. return this.currentSong.duration * 1000 - this.getTimeElapsed();
  1475. }
  1476. return 0;
  1477. },
  1478. playVideo() {
  1479. if (this.playerReady) {
  1480. this.videoLoading = true;
  1481. this.player.loadVideoById(
  1482. this.currentSong.youtubeId,
  1483. this.getTimeElapsed() / 1000 + this.currentSong.skipDuration
  1484. );
  1485. if (window.stationInterval !== 0)
  1486. clearInterval(window.stationInterval);
  1487. window.stationInterval = setInterval(() => {
  1488. if (!this.stationPaused) {
  1489. this.resizeSeekerbar();
  1490. this.calculateTimeElapsed();
  1491. }
  1492. }, 150);
  1493. }
  1494. },
  1495. resizeSeekerbar() {
  1496. this.seekerbarPercentage = parseFloat(
  1497. (this.getTimeElapsed() / 1000 / this.currentSong.duration) * 100
  1498. );
  1499. },
  1500. calculateTimeElapsed() {
  1501. if (
  1502. this.playerReady &&
  1503. !this.noSong &&
  1504. this.currentSong &&
  1505. this.player.getPlayerState() === -1
  1506. ) {
  1507. if (!this.canAutoplay) {
  1508. if (
  1509. Date.now() - this.lastTimeRequestedIfCanAutoplay >
  1510. 2000
  1511. ) {
  1512. this.lastTimeRequestedIfCanAutoplay = Date.now();
  1513. canAutoPlay.video().then(({ result }) => {
  1514. if (result) {
  1515. this.attemptsToPlayVideo = 0;
  1516. this.canAutoplay = true;
  1517. } else {
  1518. this.canAutoplay = false;
  1519. }
  1520. });
  1521. }
  1522. } else {
  1523. this.player.playVideo();
  1524. this.attemptsToPlayVideo += 1;
  1525. }
  1526. }
  1527. if (
  1528. !this.stationPaused &&
  1529. !this.localPaused &&
  1530. this.playerReady &&
  1531. !this.isApple
  1532. ) {
  1533. const timeElapsed = this.getTimeElapsed();
  1534. const currentPlayerTime =
  1535. Math.max(
  1536. this.player.getCurrentTime() -
  1537. this.currentSong.skipDuration,
  1538. 0
  1539. ) * 1000;
  1540. const difference = timeElapsed - currentPlayerTime;
  1541. let playbackRate = 1;
  1542. if (difference < -2000) {
  1543. if (!this.seeking) {
  1544. this.seeking = true;
  1545. this.player.seekTo(
  1546. this.getTimeElapsed() / 1000 +
  1547. this.currentSong.skipDuration
  1548. );
  1549. }
  1550. } else if (difference < -200) {
  1551. playbackRate = 0.8;
  1552. } else if (difference < -50) {
  1553. playbackRate = 0.9;
  1554. } else if (difference < -25) {
  1555. playbackRate = 0.95;
  1556. } else if (difference > 2000) {
  1557. if (!this.seeking) {
  1558. this.seeking = true;
  1559. this.player.seekTo(
  1560. this.getTimeElapsed() / 1000 +
  1561. this.currentSong.skipDuration
  1562. );
  1563. }
  1564. } else if (difference > 200) {
  1565. playbackRate = 1.2;
  1566. } else if (difference > 50) {
  1567. playbackRate = 1.1;
  1568. } else if (difference > 25) {
  1569. playbackRate = 1.05;
  1570. } else if (this.player.getPlaybackRate !== 1.0) {
  1571. this.player.setPlaybackRate(1.0);
  1572. }
  1573. if (this.playbackRate !== playbackRate) {
  1574. this.player.setPlaybackRate(playbackRate);
  1575. this.playbackRate = playbackRate;
  1576. }
  1577. }
  1578. let { timePaused } = this;
  1579. if (this.stationPaused)
  1580. timePaused += Date.currently() - this.pausedAt;
  1581. const duration =
  1582. (Date.currently() - this.startedAt - timePaused) / 1000;
  1583. const songDuration = this.currentSong.duration;
  1584. if (songDuration <= duration) this.player.pauseVideo();
  1585. if (duration <= songDuration)
  1586. this.timeElapsed = utils.formatTime(duration);
  1587. },
  1588. changeVolume() {
  1589. const volume = this.volumeSliderValue;
  1590. localStorage.setItem("volume", volume);
  1591. if (this.playerReady) {
  1592. this.player.setVolume(volume);
  1593. if (volume > 0) {
  1594. this.player.unMute();
  1595. localStorage.setItem("muted", false);
  1596. this.muted = false;
  1597. }
  1598. }
  1599. },
  1600. resumeLocalStation() {
  1601. this.updateLocalPaused(false);
  1602. if (!this.stationPaused) this.resumeLocalPlayer();
  1603. },
  1604. pauseLocalStation() {
  1605. this.updateLocalPaused(true);
  1606. this.pauseLocalPlayer();
  1607. },
  1608. resumeLocalPlayer() {
  1609. if (this.mediasession)
  1610. this.updateMediaSessionData(this.currentSong);
  1611. if (!this.noSong) {
  1612. if (this.playerReady) {
  1613. this.player.seekTo(
  1614. this.getTimeElapsed() / 1000 +
  1615. this.currentSong.skipDuration
  1616. );
  1617. this.player.playVideo();
  1618. }
  1619. }
  1620. },
  1621. pauseLocalPlayer() {
  1622. if (this.mediasession)
  1623. this.updateMediaSessionData(this.currentSong);
  1624. if (!this.noSong) {
  1625. this.timeBeforePause = this.getTimeElapsed();
  1626. if (this.playerReady) this.player.pauseVideo();
  1627. }
  1628. },
  1629. skipStation() {
  1630. this.socket.dispatch(
  1631. "stations.forceSkip",
  1632. this.station._id,
  1633. data => {
  1634. if (data.status !== "success")
  1635. new Toast(`Error: ${data.message}`);
  1636. else
  1637. new Toast(
  1638. "Successfully skipped the station's current song."
  1639. );
  1640. }
  1641. );
  1642. },
  1643. voteSkipStation() {
  1644. this.socket.dispatch(
  1645. "stations.voteSkip",
  1646. this.station._id,
  1647. data => {
  1648. if (data.status !== "success")
  1649. new Toast(`Error: ${data.message}`);
  1650. else
  1651. new Toast(
  1652. "Successfully voted to skip the current song."
  1653. );
  1654. }
  1655. );
  1656. },
  1657. resumeStation() {
  1658. this.socket.dispatch("stations.resume", this.station._id, data => {
  1659. if (data.status !== "success")
  1660. new Toast(`Error: ${data.message}`);
  1661. else new Toast("Successfully resumed the station.");
  1662. });
  1663. },
  1664. pauseStation() {
  1665. this.socket.dispatch("stations.pause", this.station._id, data => {
  1666. if (data.status !== "success")
  1667. new Toast(`Error: ${data.message}`);
  1668. else new Toast("Successfully paused the station.");
  1669. });
  1670. },
  1671. toggleMute() {
  1672. if (this.playerReady) {
  1673. const previousVolume = parseFloat(
  1674. localStorage.getItem("volume")
  1675. );
  1676. const volume =
  1677. this.player.getVolume() <= 0 ? previousVolume : 0;
  1678. this.muted = !this.muted;
  1679. localStorage.setItem("muted", this.muted);
  1680. this.volumeSliderValue = volume;
  1681. this.player.setVolume(volume);
  1682. if (!this.muted) localStorage.setItem("volume", volume);
  1683. }
  1684. },
  1685. increaseVolume() {
  1686. if (this.playerReady) {
  1687. const previousVolume = parseFloat(
  1688. localStorage.getItem("volume")
  1689. );
  1690. let volume = previousVolume + 5;
  1691. if (previousVolume === 0) {
  1692. this.muted = false;
  1693. localStorage.setItem("muted", false);
  1694. }
  1695. if (volume > 100) volume = 100;
  1696. this.volumeSliderValue = volume;
  1697. this.player.setVolume(volume);
  1698. localStorage.setItem("volume", volume);
  1699. }
  1700. },
  1701. toggleLike() {
  1702. if (this.currentSong.liked)
  1703. this.socket.dispatch(
  1704. "songs.unlike",
  1705. this.currentSong.youtubeId,
  1706. res => {
  1707. if (res.status !== "success")
  1708. new Toast(`Error: ${res.message}`);
  1709. }
  1710. );
  1711. else
  1712. this.socket.dispatch(
  1713. "songs.like",
  1714. this.currentSong.youtubeId,
  1715. res => {
  1716. if (res.status !== "success")
  1717. new Toast(`Error: ${res.message}`);
  1718. }
  1719. );
  1720. },
  1721. toggleDislike() {
  1722. if (this.currentSong.disliked)
  1723. return this.socket.dispatch(
  1724. "songs.undislike",
  1725. this.currentSong.youtubeId,
  1726. res => {
  1727. if (res.status !== "success")
  1728. new Toast(`Error: ${res.message}`);
  1729. }
  1730. );
  1731. return this.socket.dispatch(
  1732. "songs.dislike",
  1733. this.currentSong.youtubeId,
  1734. res => {
  1735. if (res.status !== "success")
  1736. new Toast(`Error: ${res.message}`);
  1737. }
  1738. );
  1739. },
  1740. togglePlayerDebugBox() {
  1741. this.$refs.playerDebugBox.toggleBox();
  1742. },
  1743. resetPlayerDebugBox() {
  1744. this.$refs.playerDebugBox.resetBox();
  1745. },
  1746. toggleKeyboardShortcutsHelper() {
  1747. this.$refs.keyboardShortcutsHelper.toggleBox();
  1748. },
  1749. resetKeyboardShortcutsHelper() {
  1750. this.$refs.keyboardShortcutsHelper.resetBox();
  1751. },
  1752. join() {
  1753. this.socket.dispatch(
  1754. "stations.join",
  1755. this.stationIdentifier,
  1756. res => {
  1757. if (res.status === "success") {
  1758. setTimeout(() => {
  1759. this.loading = false;
  1760. }, 1000); // prevents popping in of youtube embed etc.
  1761. const {
  1762. _id,
  1763. displayName,
  1764. name,
  1765. description,
  1766. privacy,
  1767. owner,
  1768. autofill,
  1769. blacklist,
  1770. type,
  1771. isFavorited,
  1772. theme,
  1773. requests
  1774. } = res.data;
  1775. // change url to use station name instead of station id
  1776. if (name !== this.stationIdentifier) {
  1777. // eslint-disable-next-line no-restricted-globals
  1778. this.$router.replace(name);
  1779. }
  1780. this.joinStation({
  1781. _id,
  1782. name,
  1783. displayName,
  1784. description,
  1785. privacy,
  1786. owner,
  1787. autofill,
  1788. blacklist,
  1789. type,
  1790. isFavorited,
  1791. theme,
  1792. requests
  1793. });
  1794. document.getElementsByTagName(
  1795. "html"
  1796. )[0].style.cssText = `--primary-color: var(--${res.data.theme})`;
  1797. this.setCurrentSong({
  1798. currentSong: res.data.currentSong,
  1799. startedAt: res.data.startedAt,
  1800. paused: res.data.paused,
  1801. timePaused: res.data.timePaused,
  1802. pausedAt: res.data.pausedAt
  1803. });
  1804. this.updateUserCount(res.data.userCount);
  1805. this.updateUsers(res.data.users);
  1806. this.socket.dispatch(
  1807. "stations.getStationAutofillPlaylistsById",
  1808. this.station._id,
  1809. res => {
  1810. if (res.status === "success") {
  1811. this.setAutofillPlaylists(
  1812. res.data.playlists
  1813. );
  1814. }
  1815. }
  1816. );
  1817. this.socket.dispatch(
  1818. "stations.getStationBlacklistById",
  1819. this.station._id,
  1820. res => {
  1821. if (res.status === "success") {
  1822. this.setBlacklist(res.data.playlists);
  1823. }
  1824. }
  1825. );
  1826. this.socket.dispatch("stations.getQueue", _id, res => {
  1827. if (res.status === "success") {
  1828. const { queue } = res.data;
  1829. this.updateSongsList(queue);
  1830. const [nextSong] = queue;
  1831. this.updateNextSong(nextSong);
  1832. }
  1833. });
  1834. if (this.isOwnerOrAdmin()) {
  1835. keyboardShortcuts.registerShortcut(
  1836. "station.pauseResume",
  1837. {
  1838. keyCode: 32, // Spacebar
  1839. shift: false,
  1840. ctrl: true,
  1841. preventDefault: true,
  1842. handler: () => {
  1843. if (this.aModalIsOpen) return;
  1844. if (this.stationPaused)
  1845. this.resumeStation();
  1846. else this.pauseStation();
  1847. }
  1848. }
  1849. );
  1850. keyboardShortcuts.registerShortcut(
  1851. "station.skipStation",
  1852. {
  1853. keyCode: 39, // Right arrow key
  1854. shift: false,
  1855. ctrl: true,
  1856. preventDefault: true,
  1857. handler: () => {
  1858. if (this.aModalIsOpen) return;
  1859. this.skipStation();
  1860. }
  1861. }
  1862. );
  1863. }
  1864. keyboardShortcuts.registerShortcut(
  1865. "station.lowerVolumeLarge",
  1866. {
  1867. keyCode: 40, // Down arrow key
  1868. shift: false,
  1869. ctrl: true,
  1870. preventDefault: true,
  1871. handler: () => {
  1872. if (this.aModalIsOpen) return;
  1873. this.volumeSliderValue -= 10;
  1874. this.changeVolume();
  1875. }
  1876. }
  1877. );
  1878. keyboardShortcuts.registerShortcut(
  1879. "station.lowerVolumeSmall",
  1880. {
  1881. keyCode: 40, // Down arrow key
  1882. shift: true,
  1883. ctrl: true,
  1884. preventDefault: true,
  1885. handler: () => {
  1886. if (this.aModalIsOpen) return;
  1887. this.volumeSliderValue -= 1;
  1888. this.changeVolume();
  1889. }
  1890. }
  1891. );
  1892. keyboardShortcuts.registerShortcut(
  1893. "station.increaseVolumeLarge",
  1894. {
  1895. keyCode: 38, // Up arrow key
  1896. shift: false,
  1897. ctrl: true,
  1898. preventDefault: true,
  1899. handler: () => {
  1900. if (this.aModalIsOpen) return;
  1901. this.volumeSliderValue += 10;
  1902. this.changeVolume();
  1903. }
  1904. }
  1905. );
  1906. keyboardShortcuts.registerShortcut(
  1907. "station.increaseVolumeSmall",
  1908. {
  1909. keyCode: 38, // Up arrow key
  1910. shift: true,
  1911. ctrl: true,
  1912. preventDefault: true,
  1913. handler: () => {
  1914. if (this.aModalIsOpen) return;
  1915. this.volumeSliderValue += 1;
  1916. this.changeVolume();
  1917. }
  1918. }
  1919. );
  1920. keyboardShortcuts.registerShortcut(
  1921. "station.toggleDebug",
  1922. {
  1923. keyCode: 68, // D key
  1924. shift: false,
  1925. ctrl: true,
  1926. preventDefault: true,
  1927. handler: () => {
  1928. if (this.aModalIsOpen) return;
  1929. this.togglePlayerDebugBox();
  1930. }
  1931. }
  1932. );
  1933. keyboardShortcuts.registerShortcut(
  1934. "station.toggleKeyboardShortcutsHelper",
  1935. {
  1936. keyCode: 191, // '/' key
  1937. ctrl: true,
  1938. preventDefault: true,
  1939. handler: () => {
  1940. if (this.aModalIsOpen) return;
  1941. this.toggleKeyboardShortcutsHelper();
  1942. }
  1943. }
  1944. );
  1945. keyboardShortcuts.registerShortcut(
  1946. "station.resetKeyboardShortcutsHelper",
  1947. {
  1948. keyCode: 191, // '/' key
  1949. ctrl: true,
  1950. shift: true,
  1951. preventDefault: true,
  1952. handler: () => {
  1953. if (this.aModalIsOpen) return;
  1954. this.resetKeyboardShortcutsHelper();
  1955. }
  1956. }
  1957. );
  1958. // UNIX client time before ping
  1959. const beforePing = Date.now();
  1960. this.socket.dispatch("apis.ping", res => {
  1961. if (res.status === "success") {
  1962. // UNIX client time after ping
  1963. const afterPing = Date.now();
  1964. // Average time in MS it took between the server responding and the client receiving
  1965. const connectionLatency =
  1966. (afterPing - beforePing) / 2;
  1967. console.log(
  1968. connectionLatency,
  1969. beforePing - afterPing
  1970. );
  1971. // UNIX server time
  1972. const serverDate = res.data.date;
  1973. // Difference between the server UNIX time and the client UNIX time after ping, with the connectionLatency added to the server UNIX time
  1974. const difference =
  1975. serverDate + connectionLatency - afterPing;
  1976. console.log("Difference: ", difference);
  1977. if (difference > 3000 || difference < -3000) {
  1978. console.log(
  1979. "System time difference is bigger than 3 seconds."
  1980. );
  1981. }
  1982. this.systemDifference = difference;
  1983. }
  1984. });
  1985. } else {
  1986. this.loading = false;
  1987. this.exists = false;
  1988. }
  1989. }
  1990. );
  1991. },
  1992. favoriteStation() {
  1993. this.socket.dispatch(
  1994. "stations.favoriteStation",
  1995. this.station._id,
  1996. res => {
  1997. if (res.status === "success") {
  1998. new Toast("Successfully favorited station.");
  1999. } else new Toast(res.message);
  2000. }
  2001. );
  2002. },
  2003. unfavoriteStation() {
  2004. this.socket.dispatch(
  2005. "stations.unfavoriteStation",
  2006. this.station._id,
  2007. res => {
  2008. if (res.status === "success") {
  2009. new Toast("Successfully unfavorited station.");
  2010. } else new Toast(res.message);
  2011. }
  2012. );
  2013. },
  2014. sendActivityWatchVideoData() {
  2015. if (!this.stationPaused && !this.localPaused && !this.noSong) {
  2016. if (this.activityWatchVideoLastStatus !== "playing") {
  2017. this.activityWatchVideoLastStatus = "playing";
  2018. this.activityWatchVideoLastStartDuration =
  2019. this.currentSong.skipDuration + this.getTimeElapsed();
  2020. }
  2021. if (
  2022. this.activityWatchVideoLastYouTubeId !==
  2023. this.currentSong.youtubeId
  2024. ) {
  2025. this.activityWatchVideoLastYouTubeId =
  2026. this.currentSong.youtubeId;
  2027. this.activityWatchVideoLastStartDuration =
  2028. this.currentSong.skipDuration + this.getTimeElapsed();
  2029. }
  2030. const videoData = {
  2031. title: this.currentSong ? this.currentSong.title : null,
  2032. artists:
  2033. this.currentSong && this.currentSong.artists
  2034. ? this.currentSong.artists.join(", ")
  2035. : null,
  2036. youtubeId: this.currentSong.youtubeId,
  2037. muted: this.muted,
  2038. volume: this.volumeSliderValue,
  2039. startedDuration:
  2040. this.activityWatchVideoLastStartDuration <= 0
  2041. ? 0
  2042. : Math.floor(
  2043. this.activityWatchVideoLastStartDuration /
  2044. 1000
  2045. ),
  2046. source: `station#${this.station.name}`,
  2047. hostname: window.location.hostname
  2048. };
  2049. aw.sendVideoData(videoData);
  2050. } else {
  2051. this.activityWatchVideoLastStatus = "not_playing";
  2052. }
  2053. },
  2054. ...mapActions("modalVisibility", ["openModal"]),
  2055. ...mapActions("station", [
  2056. "joinStation",
  2057. "leaveStation",
  2058. "updateStation",
  2059. "updateUserCount",
  2060. "updateUsers",
  2061. "updateCurrentSong",
  2062. "updateNextSong",
  2063. "updateSongsList",
  2064. "repositionSongInList",
  2065. "updateStationPaused",
  2066. "updateLocalPaused",
  2067. "updateNoSong",
  2068. "updateIfStationIsFavorited",
  2069. "setAutofillPlaylists",
  2070. "setBlacklist",
  2071. "updateCurrentSongRatings",
  2072. "updateOwnCurrentSongRatings",
  2073. "updateCurrentSongSkipVotes"
  2074. ]),
  2075. ...mapActions("modals/editSong", ["stopVideo"])
  2076. }
  2077. };
  2078. </script>
  2079. <style lang="less">
  2080. #stationPlayer {
  2081. position: absolute;
  2082. top: 0;
  2083. left: 0;
  2084. width: 100%;
  2085. height: 100%;
  2086. }
  2087. #currently-playing-container,
  2088. #next-up-container {
  2089. .song-item {
  2090. .thumbnail {
  2091. min-width: 130px;
  2092. width: 130px;
  2093. height: 130px;
  2094. }
  2095. }
  2096. }
  2097. #control-bar-container
  2098. #right-buttons
  2099. .tippy-box[data-theme~="dropdown"]
  2100. .nav-dropdown-items {
  2101. padding-bottom: 0 !important;
  2102. }
  2103. </style>
  2104. <style lang="less" scoped>
  2105. #page-loader-container {
  2106. height: inherit;
  2107. #page-loader-content {
  2108. height: inherit;
  2109. position: absolute;
  2110. max-width: 100%;
  2111. width: 1800px;
  2112. transform: translateX(-50%);
  2113. left: 50%;
  2114. }
  2115. #page-loader-layout {
  2116. height: inherit;
  2117. width: 100%;
  2118. }
  2119. }
  2120. #mobile-progress-animation {
  2121. width: 50px;
  2122. animation: rotate 0.8s infinite linear;
  2123. border: 8px solid var(--primary-color);
  2124. border-right-color: transparent;
  2125. border-radius: 50%;
  2126. height: 50px;
  2127. position: absolute;
  2128. top: 50%;
  2129. left: 50%;
  2130. display: none;
  2131. }
  2132. @keyframes rotate {
  2133. 0% {
  2134. transform: rotate(0deg);
  2135. }
  2136. 100% {
  2137. transform: rotate(360deg);
  2138. }
  2139. }
  2140. .nav,
  2141. .button.is-primary {
  2142. background-color: var(--primary-color) !important;
  2143. }
  2144. .button.is-primary:hover,
  2145. .button.is-primary:focus {
  2146. filter: brightness(90%);
  2147. }
  2148. .night-mode {
  2149. #currently-playing-container,
  2150. #next-up-container,
  2151. #about-station-container,
  2152. #control-bar-container,
  2153. .player-container {
  2154. background-color: var(--dark-grey-3) !important;
  2155. }
  2156. #video-container,
  2157. #control-bar-container,
  2158. .quadrant:not(#sidebar-container),
  2159. .player-container {
  2160. border: 0 !important;
  2161. }
  2162. #seeker-bar-container {
  2163. background-color: var(--dark-grey-3) !important;
  2164. }
  2165. #dropdown-toggle {
  2166. background-color: var(--dark-grey-2) !important;
  2167. border: 0;
  2168. i {
  2169. color: var(--white);
  2170. }
  2171. }
  2172. }
  2173. #station-outer-container {
  2174. margin: 0 auto;
  2175. padding: 20px 40px;
  2176. min-height: calc(100vh - 64px);
  2177. width: 100%;
  2178. max-width: 1800px;
  2179. display: flex;
  2180. #station-inner-container {
  2181. width: 100%;
  2182. min-height: calc(100vh - 428px);
  2183. display: flex;
  2184. flex-direction: row;
  2185. flex-wrap: wrap;
  2186. .row {
  2187. display: flex;
  2188. flex-direction: row;
  2189. max-width: 100%;
  2190. }
  2191. .column {
  2192. display: flex;
  2193. flex-direction: column;
  2194. }
  2195. .quadrant {
  2196. border-radius: @border-radius;
  2197. margin: 10px;
  2198. }
  2199. .quadrant:not(#sidebar-container) {
  2200. background-color: var(--white);
  2201. border: 1px solid var(--light-grey-3);
  2202. }
  2203. #station-left-column,
  2204. #station-right-column {
  2205. padding: 0;
  2206. }
  2207. #about-station-container {
  2208. padding: 20px;
  2209. display: flex;
  2210. flex-direction: column;
  2211. flex-grow: unset;
  2212. #station-info {
  2213. #station-name {
  2214. flex-direction: row !important;
  2215. h1 {
  2216. margin: 0;
  2217. font-size: 36px;
  2218. line-height: 0.8;
  2219. text-overflow: ellipsis;
  2220. overflow: hidden;
  2221. }
  2222. i {
  2223. margin-left: 10px;
  2224. font-size: 30px;
  2225. color: var(--yellow);
  2226. &.stationMode {
  2227. padding-left: 10px;
  2228. margin-left: auto;
  2229. color: var(--primary-color);
  2230. }
  2231. }
  2232. .verified-station {
  2233. color: var(--primary-color);
  2234. }
  2235. }
  2236. p {
  2237. display: -webkit-box;
  2238. max-width: 700px;
  2239. margin-bottom: 10px;
  2240. overflow: hidden;
  2241. text-overflow: ellipsis;
  2242. -webkit-box-orient: vertical;
  2243. -webkit-line-clamp: 3;
  2244. }
  2245. }
  2246. #admin-buttons {
  2247. display: flex;
  2248. .button {
  2249. margin: 3px;
  2250. }
  2251. }
  2252. }
  2253. #current-next-row {
  2254. display: flex;
  2255. flex-direction: row;
  2256. #currently-playing-container,
  2257. #next-up-container {
  2258. overflow: hidden;
  2259. flex-basis: 50%;
  2260. .song-item {
  2261. border: unset;
  2262. }
  2263. .nothing-here-text {
  2264. height: 100%;
  2265. }
  2266. }
  2267. > div:only-child {
  2268. flex: 1 !important;
  2269. flex-basis: 100% !important;
  2270. }
  2271. }
  2272. .player-container {
  2273. height: inherit;
  2274. background-color: var(--white);
  2275. display: flex;
  2276. flex-direction: column;
  2277. border: 1px solid var(--light-grey-3);
  2278. border-radius: @border-radius;
  2279. overflow: hidden;
  2280. &.nothing-here-text {
  2281. margin: 10px;
  2282. flex: 1;
  2283. min-height: 487px;
  2284. }
  2285. #video-container {
  2286. position: relative;
  2287. padding-bottom: 56.25%; /* proportion value to aspect ratio 16:9 (9 / 16 = 0.5625 or 56.25%) */
  2288. height: 0;
  2289. overflow: hidden;
  2290. .player-cannot-autoplay {
  2291. position: relative;
  2292. width: 100%;
  2293. height: 100%;
  2294. bottom: calc(100% + 5px);
  2295. background: var(--primary-color);
  2296. display: flex;
  2297. align-items: center;
  2298. justify-content: center;
  2299. p {
  2300. color: var(--white);
  2301. font-size: 26px;
  2302. text-align: center;
  2303. }
  2304. }
  2305. }
  2306. #seeker-bar-container {
  2307. background-color: var(--white);
  2308. position: relative;
  2309. height: 7px;
  2310. display: block;
  2311. width: 100%;
  2312. #seeker-bar {
  2313. background-color: var(--primary-color);
  2314. top: 0;
  2315. left: 0;
  2316. bottom: 0;
  2317. position: absolute;
  2318. width: 100%;
  2319. }
  2320. .seeker-bar-cover {
  2321. position: absolute;
  2322. top: 0;
  2323. right: 0;
  2324. bottom: 0;
  2325. background-color: inherit;
  2326. }
  2327. }
  2328. #control-bar-container {
  2329. display: flex;
  2330. justify-content: space-around;
  2331. padding: 10px 0;
  2332. width: 100%;
  2333. background: var(--white);
  2334. flex-direction: column;
  2335. flex-flow: wrap;
  2336. .button:not(#dropdown-toggle) {
  2337. width: 75px;
  2338. }
  2339. #left-buttons,
  2340. #right-buttons {
  2341. margin: 3px;
  2342. }
  2343. #left-buttons {
  2344. display: flex;
  2345. .button:not(:first-of-type) {
  2346. margin-left: 5px;
  2347. }
  2348. .disabled {
  2349. filter: grayscale(0.4);
  2350. }
  2351. }
  2352. #duration {
  2353. margin: 3px;
  2354. display: flex;
  2355. align-items: center;
  2356. p {
  2357. font-size: 22px;
  2358. /** prevents duration width slightly varying and shifting other controls slightly */
  2359. width: 150px;
  2360. text-align: center;
  2361. }
  2362. }
  2363. #volume-control {
  2364. margin: 3px;
  2365. margin-top: 0;
  2366. display: flex;
  2367. align-items: center;
  2368. cursor: pointer;
  2369. .volume-slider {
  2370. width: 100%;
  2371. padding: 0 15px;
  2372. background: transparent;
  2373. min-width: 100px;
  2374. }
  2375. input[type="range"] {
  2376. -webkit-appearance: none;
  2377. margin: 7.3px 0;
  2378. }
  2379. input[type="range"]:focus {
  2380. outline: none;
  2381. }
  2382. input[type="range"]::-webkit-slider-runnable-track {
  2383. width: 100%;
  2384. height: 5.2px;
  2385. cursor: pointer;
  2386. box-shadow: 0;
  2387. background: var(--light-grey-3);
  2388. border-radius: @border-radius;
  2389. border: 0;
  2390. }
  2391. input[type="range"]::-webkit-slider-thumb {
  2392. box-shadow: 0;
  2393. border: 0;
  2394. height: 19px;
  2395. width: 19px;
  2396. border-radius: 100%;
  2397. background: var(--primary-color);
  2398. cursor: pointer;
  2399. -webkit-appearance: none;
  2400. margin-top: -6.5px;
  2401. }
  2402. input[type="range"]::-moz-range-track {
  2403. width: 100%;
  2404. height: 5.2px;
  2405. cursor: pointer;
  2406. box-shadow: 0;
  2407. background: var(--light-grey-3);
  2408. border-radius: @border-radius;
  2409. border: 0;
  2410. }
  2411. input[type="range"]::-moz-range-thumb {
  2412. box-shadow: 0;
  2413. border: 0;
  2414. height: 19px;
  2415. width: 19px;
  2416. border-radius: 100%;
  2417. background: var(--primary-color);
  2418. cursor: pointer;
  2419. -webkit-appearance: none;
  2420. margin-top: -6.5px;
  2421. }
  2422. input[type="range"]::-ms-track {
  2423. width: 100%;
  2424. height: 5.2px;
  2425. cursor: pointer;
  2426. box-shadow: 0;
  2427. background: var(--light-grey-3);
  2428. border-radius: @border-radius;
  2429. }
  2430. input[type="range"]::-ms-fill-lower {
  2431. background: var(--light-grey-3);
  2432. border: 0;
  2433. border-radius: 0;
  2434. box-shadow: 0;
  2435. }
  2436. input[type="range"]::-ms-fill-upper {
  2437. background: var(--light-grey-3);
  2438. border: 0;
  2439. border-radius: 0;
  2440. box-shadow: 0;
  2441. }
  2442. input[type="range"]::-ms-thumb {
  2443. box-shadow: 0;
  2444. border: 0;
  2445. height: 15px;
  2446. width: 15px;
  2447. border-radius: 100%;
  2448. background: var(--primary-color);
  2449. cursor: pointer;
  2450. -webkit-appearance: none;
  2451. margin-top: 1.5px;
  2452. }
  2453. }
  2454. #right-buttons {
  2455. display: flex;
  2456. #dropdown-toggle {
  2457. width: 35px;
  2458. }
  2459. #dislike-song,
  2460. #add-song-to-playlist .button:not(#dropdown-toggle) {
  2461. margin-left: 5px;
  2462. }
  2463. #ratings {
  2464. display: flex;
  2465. &.liked #dislike-song,
  2466. &.disliked #like-song {
  2467. background-color: var(--grey) !important;
  2468. }
  2469. #like-song.disabled,
  2470. #dislike-song.disabled {
  2471. filter: grayscale(0.4);
  2472. }
  2473. }
  2474. #add-song-to-playlist {
  2475. display: flex;
  2476. flex-direction: column-reverse;
  2477. #nav-dropdown {
  2478. position: absolute;
  2479. margin-left: 4px;
  2480. margin-bottom: 36px;
  2481. .nav-dropdown-items {
  2482. position: relative;
  2483. right: calc(100% - 110px);
  2484. }
  2485. }
  2486. .control {
  2487. width: fit-content;
  2488. margin-bottom: 0 !important;
  2489. button.disabled {
  2490. filter: grayscale(0.4);
  2491. border-radius: @border-radius;
  2492. &::after {
  2493. margin-right: 100%;
  2494. }
  2495. }
  2496. }
  2497. }
  2498. }
  2499. }
  2500. }
  2501. #sidebar-container {
  2502. border-top: 0;
  2503. position: relative;
  2504. height: inherit;
  2505. flex-grow: 1;
  2506. min-height: 350px;
  2507. }
  2508. }
  2509. }
  2510. .footer {
  2511. margin-top: 30px;
  2512. }
  2513. .nyan {
  2514. background: linear-gradient(
  2515. 90deg,
  2516. magenta 0%,
  2517. red 15%,
  2518. orange 30%,
  2519. yellow 45%,
  2520. lime 60%,
  2521. cyan 75%,
  2522. blue 90%,
  2523. magenta 100%
  2524. );
  2525. background-size: 200%;
  2526. animation: nyanMoving 4s linear infinite;
  2527. }
  2528. @keyframes nyanMoving {
  2529. 0% {
  2530. background-position: 0% 0%;
  2531. }
  2532. 100% {
  2533. background-position: -200% 0%;
  2534. }
  2535. }
  2536. .christmas-seeker {
  2537. background: repeating-linear-gradient(
  2538. -45deg,
  2539. var(--white) 0 1rem,
  2540. var(--dark-red) 1rem 2rem
  2541. );
  2542. background-size: 200% 200%;
  2543. animation: christmas 20s linear infinite;
  2544. }
  2545. @keyframes christmas {
  2546. 100% {
  2547. background-position: 80% 100%;
  2548. }
  2549. }
  2550. .bg-bubbles {
  2551. top: 0;
  2552. left: 0;
  2553. width: 100%;
  2554. height: 100%;
  2555. position: absolute;
  2556. z-index: -1;
  2557. margin: 0px;
  2558. pointer-events: none;
  2559. }
  2560. .bg-bubbles li {
  2561. position: absolute;
  2562. list-style: none;
  2563. display: block;
  2564. width: 40px;
  2565. height: 40px;
  2566. border-radius: 100px;
  2567. background-color: var(--primary-color);
  2568. opacity: 0.15;
  2569. bottom: 0px;
  2570. -webkit-animation: square 25s infinite;
  2571. animation: square 25s infinite;
  2572. -webkit-transition-timing-function: linear;
  2573. transition-timing-function: linear;
  2574. }
  2575. .bg-bubbles li:nth-child(1) {
  2576. left: 10%;
  2577. }
  2578. .bg-bubbles li:nth-child(2) {
  2579. left: 20%;
  2580. width: 80px;
  2581. height: 80px;
  2582. -webkit-animation-delay: 2s;
  2583. animation-delay: 2s;
  2584. -webkit-animation-duration: 17s;
  2585. animation-duration: 17s;
  2586. }
  2587. .bg-bubbles li:nth-child(3) {
  2588. left: 25%;
  2589. -webkit-animation-delay: 4s;
  2590. animation-delay: 4s;
  2591. }
  2592. .bg-bubbles li:nth-child(4) {
  2593. left: 40%;
  2594. width: 60px;
  2595. height: 60px;
  2596. -webkit-animation-duration: 22s;
  2597. animation-duration: 22s;
  2598. background-color: var(--primary-color);
  2599. opacity: 0.25;
  2600. }
  2601. .bg-bubbles li:nth-child(5) {
  2602. left: 70%;
  2603. }
  2604. .bg-bubbles li:nth-child(6) {
  2605. left: 80%;
  2606. width: 120px;
  2607. height: 120px;
  2608. -webkit-animation-delay: 3s;
  2609. animation-delay: 3s;
  2610. background-color: var(--primary-color);
  2611. opacity: 0.2;
  2612. }
  2613. .bg-bubbles li:nth-child(7) {
  2614. left: 32%;
  2615. width: 160px;
  2616. height: 160px;
  2617. -webkit-animation-delay: 7s;
  2618. animation-delay: 7s;
  2619. }
  2620. .bg-bubbles li:nth-child(8) {
  2621. left: 55%;
  2622. width: 20px;
  2623. height: 20px;
  2624. -webkit-animation-delay: 15s;
  2625. animation-delay: 15s;
  2626. -webkit-animation-duration: 40s;
  2627. animation-duration: 40s;
  2628. }
  2629. .bg-bubbles li:nth-child(9) {
  2630. left: 25%;
  2631. width: 10px;
  2632. height: 10px;
  2633. -webkit-animation-delay: 2s;
  2634. animation-delay: 2s;
  2635. -webkit-animation-duration: 40s;
  2636. animation-duration: 40s;
  2637. background-color: var(--primary-color);
  2638. opacity: 0.3;
  2639. }
  2640. .bg-bubbles li:nth-child(10) {
  2641. left: 80%;
  2642. width: 160px;
  2643. height: 160px;
  2644. -webkit-animation-delay: 11s;
  2645. animation-delay: 11s;
  2646. }
  2647. /* Tablet view fix */
  2648. @media (max-width: 768px) {
  2649. .bg-bubbles li:nth-child(10) {
  2650. display: none;
  2651. }
  2652. }
  2653. @-webkit-keyframes square {
  2654. 0% {
  2655. -webkit-transform: translateY(0);
  2656. transform: translateY(0);
  2657. }
  2658. 100% {
  2659. -webkit-transform: translateY(-700px) rotate(600deg);
  2660. transform: translateY(-700px) rotate(600deg);
  2661. }
  2662. }
  2663. @keyframes square {
  2664. 0% {
  2665. -webkit-transform: translateY(0);
  2666. transform: translateY(0);
  2667. }
  2668. 100% {
  2669. -webkit-transform: translateY(-700px) rotate(600deg);
  2670. transform: translateY(-700px) rotate(600deg);
  2671. }
  2672. }
  2673. :deep(.nothing-here-text) {
  2674. display: flex;
  2675. align-items: center;
  2676. justify-content: center;
  2677. }
  2678. @media (min-width: 1500px) {
  2679. #station-left-column {
  2680. max-width: 650px;
  2681. }
  2682. #station-right-column {
  2683. max-width: calc(100% - 650px);
  2684. }
  2685. }
  2686. @media (max-width: 1700px) {
  2687. #current-next-row {
  2688. flex-direction: column !important;
  2689. > div {
  2690. flex: 1 !important;
  2691. }
  2692. }
  2693. }
  2694. @media (max-width: 1500px) {
  2695. #mobile-progress-animation {
  2696. display: block;
  2697. }
  2698. #page-loader-container {
  2699. display: none;
  2700. }
  2701. #station-outer-container {
  2702. max-width: 1500px;
  2703. #station-inner-container {
  2704. flex-direction: row;
  2705. #station-left-column {
  2706. #about-station-container #admin-buttons {
  2707. flex-wrap: wrap;
  2708. }
  2709. #sidebar-container {
  2710. min-height: 350px;
  2711. }
  2712. }
  2713. #station-right-column {
  2714. #current-next-row {
  2715. flex-direction: column;
  2716. }
  2717. #control-bar-container {
  2718. #duration,
  2719. #volume-control,
  2720. #right-buttons,
  2721. #left-buttons {
  2722. margin-bottom: 5px;
  2723. justify-content: center;
  2724. }
  2725. #duration {
  2726. order: 1;
  2727. }
  2728. #volume-control {
  2729. order: 2;
  2730. max-width: 400px;
  2731. }
  2732. #right-buttons {
  2733. order: 3;
  2734. flex-wrap: wrap;
  2735. #ratings {
  2736. flex-wrap: wrap;
  2737. }
  2738. }
  2739. #left-buttons {
  2740. order: 4;
  2741. flex-wrap: wrap;
  2742. }
  2743. }
  2744. }
  2745. }
  2746. }
  2747. }
  2748. @media (max-width: 1200px) {
  2749. #station-outer-container {
  2750. max-width: 900px;
  2751. padding: 0;
  2752. #station-inner-container {
  2753. flex-direction: column-reverse;
  2754. flex-wrap: nowrap;
  2755. }
  2756. }
  2757. }
  2758. @media (max-width: 990px) {
  2759. #station-outer-container {
  2760. min-height: calc(
  2761. 100vh - 256px
  2762. ); // Height of nav (64px) + height of footer (190px)
  2763. }
  2764. }
  2765. </style>