Station.vue 33 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174
  1. <template>
  2. <official-header v-if='type == "official"'></official-header>
  3. <community-header v-if='type == "community"'></community-header>
  4. <song-queue v-if='modals.addSongToQueue'></song-queue>
  5. <add-to-playlist v-if='modals.addSongToPlaylist'></add-to-playlist>
  6. <edit-playlist v-if='modals.editPlaylist'></edit-playlist>
  7. <create-playlist v-if='modals.createPlaylist'></create-playlist>
  8. <edit-station v-show='modals.editStation'></edit-station>
  9. <report v-if='modals.report'></report>
  10. <songs-list-sidebar v-if='sidebars.songslist'></songs-list-sidebar>
  11. <playlist-sidebar v-if='sidebars.playlist'></playlist-sidebar>
  12. <users-sidebar v-if='sidebars.users'></users-sidebar>
  13. <div class='progress' v-show='!ready'></div>
  14. <div class='station' v-show="ready">
  15. <div v-show="noSong" class="no-song">
  16. <h1>No song is currently playing</h1>
  17. <h4 v-if='type === "community" && station.partyMode && (!station.locked || (station.locked && $parent.loggedIn && $parent.userId === station.owner))'>
  18. <a href='#' class='no-song' @click='modals.addSongToQueue = true'>Add a song to the queue</a>
  19. </h4>
  20. <h4 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && !station.privatePlaylist'>
  21. <a href='#' class='no-song' @click='sidebars.playlist = true'>Play a private playlist</a>
  22. </h4>
  23. <h1 v-if='type === "community" && !station.partyMode && $parent.userId === station.owner && station.privatePlaylist'>Maybe you can add some songs to your selected private playlist and then press the skip button</h1>
  24. </div>
  25. <div class="columns" v-show="!noSong">
  26. <div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
  27. <div class="video-container">
  28. <div id="player"></div>
  29. </div>
  30. <div class="seeker-bar-container white" id="preview-progress">
  31. <div class="seeker-bar light-blue" style="width: 0%;"></div>
  32. </div>
  33. </div>
  34. <div class="desktop-only column is-3-desktop card playlistCard experimental">
  35. <div class='title' v-if='type === "community"'>Queue</div>
  36. <div class='title' v-else>Playlist</div>
  37. <article class="media" v-if="!noSong">
  38. <figure class="media-left">
  39. <p class="image is-64x64">
  40. <img :src="currentSong.thumbnail" onerror="this.src='/assets/notes-transparent.png'">
  41. </p>
  42. </figure>
  43. <div class="media-content">
  44. <div class="content">
  45. <p>
  46. Current Song:
  47. <br>
  48. <strong>{{ currentSong.title }}</strong>
  49. <br>
  50. <small>{{ currentSong.artists }}</small>
  51. </p>
  52. </div>
  53. </div>
  54. <div class="media-right">
  55. {{ formatTime(currentSong.duration) }}
  56. </div>
  57. </article>
  58. <p v-if="noSong" class="center">There is currently no song playing.</p>
  59. <article class="media" v-for='song in songsList'>
  60. <div class="media-content">
  61. <div class="content">
  62. <strong class="songTitle">{{ song.title }}</strong>
  63. <br>
  64. <small>{{ song.artists.join(', ') }}</small>
  65. <br>
  66. <div v-if="station.partyMode">
  67. <br>
  68. <small>Requested by <b>{{this.$parent.$parent.getUsernameFromId(song.requestedBy)}} {{this.userIdMap[song.requestedBy]}}</b></small>
  69. <button class="button" @click="removeFromQueue(song.songId)" v-if="isOwnerOnly() || isAdminOnly()">REMOVE</button>
  70. </div>
  71. </div>
  72. </div>
  73. <div class="media-right">
  74. {{ $parent.formatTime(song.duration) }}
  75. </div>
  76. </article>
  77. <a class='button add-to-queue' href='#' @click='modals.addSongToQueue = !modals.addSongToQueue' v-if="type === 'community' && $parent.loggedIn">Add Song to Queue</a>
  78. </div>
  79. </div>
  80. <div class="desktop-only columns is-mobile" v-show="!noSong">
  81. <div class="column is-8-desktop is-offset-2-desktop is-12-mobile">
  82. <div class="columns is-mobile">
  83. <div class="column is-12-desktop">
  84. <h4 id="time-display">{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h4>
  85. <h3>{{currentSong.title}}</h3>
  86. <h4 class="thin" style="margin-left: 0">{{currentSong.artists}}</h4>
  87. <div class="columns is-mobile">
  88. <form style="margin-top: 12px; margin-bottom: 0;" action="#" class="column is-7-desktop is-4-mobile">
  89. <p class='volume-slider-wrapper'>
  90. <i class="material-icons" @click='toggleMute()' v-if='muted'>volume_mute</i>
  91. <i class="material-icons" @click='toggleMute()' v-else>volume_down</i>
  92. <input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
  93. <i class="material-icons" @click='increaseVolume()'>volume_up</i>
  94. </p>
  95. </form>
  96. <div class="column is-8-mobile is-5-desktop" style="float: right;">
  97. <ul id="ratings" v-if="currentSong.likes !== -1 && currentSong.dislikes !== -1">
  98. <li id="like" class="right" @click="toggleLike()">
  99. <span class="flow-text">{{currentSong.likes}} </span>
  100. <i id="thumbs_up" class="material-icons grey-text" v-bind:class="{ liked: liked }">thumb_up</i>
  101. <a class='absolute-a behind' @click="toggleLike()" href='#'></a>
  102. </li>
  103. <li style="margin-right: 10px;" id="dislike" class="right" @click="toggleDislike()">
  104. <span class="flow-text">{{currentSong.dislikes}} </span>
  105. <i id="thumbs_down" class="material-icons grey-text" v-bind:class="{ disliked: disliked }">thumb_down</i>
  106. <a class='absolute-a behind' @click="toggleDislike()" href='#'></a>
  107. </li>
  108. </ul>
  109. </div>
  110. </div>
  111. </div>
  112. <div class="column is-3-desktop experimental" v-if="!simpleSong">
  113. <img class="image" :src="currentSong.thumbnail" alt="Song Thumbnail" onerror="this.src='/assets/notes-transparent.png'" />
  114. </div>
  115. </div>
  116. </div>
  117. </div>
  118. <div class="mobile-only" v-show="!noSong">
  119. <div>
  120. <div>
  121. <div>
  122. <h3>{{currentSong.title}}</h3>
  123. <h4 class="thin">{{currentSong.artists}}</h4>
  124. <h5>{{timeElapsed}} / {{formatTime(currentSong.duration)}}</h5>
  125. <div>
  126. <form class="columns" action="#">
  127. <p class='column is-11-mobile volume-slider-wrapper'>
  128. <i class="material-icons" @click='toggleMute()' v-if='muted'>volume_mute</i>
  129. <i class="material-icons" @click='toggleMute()' v-else>volume_down</i>
  130. <input type="range" id="volumeSlider" min="0" max="100" class="active" v-on:change="changeVolume()" v-on:input="changeVolume()">
  131. <i class="material-icons" @click='increaseVolume()'>volume_up</i>
  132. </p>
  133. </form>
  134. <div>
  135. <ul id="ratings" style="display: inline-block;" v-if="currentSong.likes !== -1 && currentSong.dislikes !== -1">
  136. <li id="dislike" style="display: inline-block;margin-right: 10px;" @click="toggleDislike()">
  137. <span class="flow-text">{{currentSong.dislikes}} </span>
  138. <i id="thumbs_down" class="material-icons grey-text" v-bind:class="{ disliked: disliked }">thumb_down</i>
  139. <a class='absolute-a behind' @click="toggleDislike()" href='#'></a>
  140. </li>
  141. <li id="like" style="display: inline-block;" @click="toggleLike()">
  142. <span class="flow-text">{{currentSong.likes}} </span>
  143. <i id="thumbs_up" class="material-icons grey-text" v-bind:class="{ liked: liked }">thumb_up</i>
  144. <a class='absolute-a behind' @click="toggleLike()" href='#'></a>
  145. </li>
  146. </ul>
  147. </div>
  148. </div>
  149. </div>
  150. </div>
  151. </div>
  152. </div>
  153. </div>
  154. </template>
  155. <script>
  156. import { Toast } from 'vue-roaster';
  157. import SongQueue from '../Modals/AddSongToQueue.vue';
  158. import AddToPlaylist from '../Modals/AddSongToPlaylist.vue';
  159. import EditPlaylist from '../Modals/Playlists/Edit.vue';
  160. import CreatePlaylist from '../Modals/Playlists/Create.vue';
  161. import EditStation from '../Modals/EditStation.vue';
  162. import Report from '../Modals/Report.vue';
  163. import SongsListSidebar from '../Sidebars/SongsList.vue';
  164. import PlaylistSidebar from '../Sidebars/Playlist.vue';
  165. import UsersSidebar from '../Sidebars/UsersList.vue';
  166. import OfficialHeader from './OfficialHeader.vue';
  167. import CommunityHeader from './CommunityHeader.vue';
  168. import MainFooter from '../MainFooter.vue';
  169. import io from '../../io';
  170. import auth from '../../auth';
  171. export default {
  172. data() {
  173. return {
  174. ready: false,
  175. type: '',
  176. playerReady: false,
  177. previousSong: null,
  178. currentSong: {},
  179. player: undefined,
  180. timePaused: 0,
  181. paused: false,
  182. muted: false,
  183. timeElapsed: '0:00',
  184. liked: false,
  185. disliked: false,
  186. modals: {
  187. addSongToQueue: false,
  188. addSongToPlaylist: false,
  189. editPlaylist: false,
  190. createPlaylist: false,
  191. editStation: false,
  192. report: false
  193. },
  194. sidebars: {
  195. songslist: false,
  196. users: false,
  197. playlist: false
  198. },
  199. noSong: false,
  200. simpleSong: false,
  201. songsList: [],
  202. timeBeforePause: 0,
  203. station: {},
  204. skipVotes: 0,
  205. privatePlaylistQueueSelected: null,
  206. automaticallyRequestedSongId: null,
  207. systemDifference: 0,
  208. users: [],
  209. userCount: 0,
  210. userIdMap: this.$parent.userIdMap
  211. }
  212. },
  213. methods: {
  214. isOwnerOnly: function () {
  215. return this.$parent.loggedIn && this.$parent.userId === this.station.owner;
  216. },
  217. isAdminOnly: function() {
  218. return this.$parent.loggedIn && this.$parent.role === 'admin';
  219. },
  220. removeFromQueue: function(songId) {
  221. socket.emit('stations.removeFromQueue', this.station._id, songId, res => {
  222. if (res.status === 'success') {
  223. Toast.methods.addToast('Successfully removed song from the queue.', 4000);
  224. } else Toast.methods.addToast(res.message, 8000);
  225. });
  226. },
  227. editPlaylist: function (id) {
  228. this.playlistBeingEdited = id;
  229. this.modals.editPlaylist = !this.modals.editPlaylist;
  230. },
  231. editStation: function () {
  232. let _this = this;
  233. this.$broadcast('editStation', {
  234. _id: _this.station._id,
  235. name: _this.station.name,
  236. type: _this.type,
  237. partyMode: _this.station.partyMode,
  238. description: _this.station.description,
  239. privacy: _this.station.privacy,
  240. displayName: _this.station.displayName
  241. });
  242. },
  243. toggleSidebar: function (type) {
  244. Object.keys(this.sidebars).forEach(sidebar => {
  245. if (sidebar !== type) this.sidebars[sidebar] = false;
  246. else this.sidebars[type] = !this.sidebars[type];
  247. });
  248. },
  249. youtubeReady: function() {
  250. let local = this;
  251. if (!local.player) {
  252. local.player = new YT.Player("player", {
  253. height: 270,
  254. width: 480,
  255. videoId: local.currentSong.songId,
  256. startSeconds: local.getTimeElapsed() / 1000 + local.currentSong.skipDuration,
  257. playerVars: {controls: 0, iv_load_policy: 3, rel: 0, showinfo: 0},
  258. events: {
  259. 'onReady': function (event) {
  260. local.playerReady = true;
  261. let volume = parseInt(localStorage.getItem("volume"));
  262. volume = (typeof volume === "number") ? volume : 20;
  263. local.player.setVolume(volume);
  264. if (volume > 0) local.player.unMute();
  265. local.playVideo();
  266. },
  267. 'onError': function(err) {
  268. console.log("iframe error", err);
  269. local.voteSkipStation();
  270. },
  271. 'onStateChange': function (event) {
  272. if (event.data === 1 && local.videoLoading === true) {
  273. local.videoLoading = false;
  274. local.player.seekTo(local.getTimeElapsed() / 1000 + local.currentSong.skipDuration, true);
  275. if (local.paused) local.player.pauseVideo();
  276. } else if (event.data === 1 && local.paused) {
  277. local.player.seekTo(local.timeBeforePause / 1000, true);
  278. local.player.pauseVideo();
  279. }
  280. if (event.data === 2 && !local.paused && !local.noSong && (local.player.getDuration() / 1000) < local.currentSong.duration) {
  281. local.player.seekTo(local.getTimeElapsed() / 1000 + local.currentSong.skipDuration, true);
  282. local.player.playVideo();
  283. }
  284. }
  285. }
  286. });
  287. }
  288. },
  289. getTimeElapsed: function() {
  290. let local = this;
  291. if (local.currentSong) return Date.currently() - local.startedAt - local.timePaused;
  292. else return 0;
  293. },
  294. playVideo: function() {
  295. let local = this;
  296. if (local.playerReady) {
  297. local.videoLoading = true;
  298. local.player.loadVideoById(local.currentSong.songId, local.getTimeElapsed() / 1000 + local.currentSong.skipDuration);
  299. if (local.currentSong.artists) local.currentSong.artists = local.currentSong.artists.join(", ");
  300. if (window.stationInterval !== 0) clearInterval(window.stationInterval);
  301. window.stationInterval = setInterval(function () {
  302. local.resizeSeekerbar();
  303. local.calculateTimeElapsed();
  304. }, 250);
  305. }
  306. },
  307. resizeSeekerbar: function() {
  308. let local = this;
  309. if (!local.paused) {
  310. $(".seeker-bar").width(parseInt(((local.getTimeElapsed() / 1000) / local.currentSong.duration * 100)) + "%");
  311. }
  312. },
  313. formatTime: function(duration) {
  314. let d = moment.duration(duration, 'seconds');
  315. if (duration < 0) return "0:00";
  316. return ((d.hours() > 0) ? (d.hours() < 10 ? ("0" + d.hours() + ":") : (d.hours() + ":")) : "") + (d.minutes() + ":") + (d.seconds() < 10 ? ("0" + d.seconds()) : d.seconds());
  317. },
  318. calculateTimeElapsed: function() {
  319. let local = this;
  320. let currentTime = Date.currently();
  321. if (local.currentTime !== undefined && local.paused) {
  322. local.timePaused += (Date.currently() - local.currentTime);
  323. local.currentTime = undefined;
  324. }
  325. let duration = (Date.currently() - local.startedAt - local.timePaused) / 1000;
  326. let songDuration = local.currentSong.duration;
  327. if (songDuration <= duration) local.player.pauseVideo();
  328. if ((!local.paused) && duration <= songDuration) local.timeElapsed = local.formatTime(duration);
  329. },
  330. toggleLock: function () {
  331. let _this = this;
  332. socket.emit('stations.toggleLock', this.station._id, res => {
  333. if (res.status === 'success') {
  334. Toast.methods.addToast('Successfully toggled the queue lock.', 4000);
  335. } else Toast.methods.addToast(res.message, 8000);
  336. });
  337. },
  338. changeVolume: function() {
  339. let local = this;
  340. let volume = $("#volumeSlider").val();
  341. localStorage.setItem("volume", volume);
  342. if (local.playerReady) {
  343. local.player.setVolume(volume);
  344. if (volume > 0) local.player.unMute();
  345. }
  346. },
  347. resumeLocalStation: function() {
  348. this.paused = false;
  349. if (!this.noSong) {
  350. if (this.playerReady) {
  351. this.player.seekTo(this.getTimeElapsed() / 1000 + this.currentSong.skipDuration);
  352. this.player.playVideo();
  353. }
  354. }
  355. },
  356. pauseLocalStation: function() {
  357. this.paused = true;
  358. if (!this.noSong) {
  359. this.timeBeforePause = this.getTimeElapsed();
  360. if (this.playerReady) this.player.pauseVideo();
  361. }
  362. },
  363. skipStation: function () {
  364. let _this = this;
  365. _this.socket.emit('stations.forceSkip', _this.station._id, data => {
  366. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  367. else Toast.methods.addToast('Successfully skipped the station\'s current song.', 4000);
  368. });
  369. },
  370. voteSkipStation: function () {
  371. let _this = this;
  372. _this.socket.emit('stations.voteSkip', _this.station._id, data => {
  373. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  374. else Toast.methods.addToast('Successfully voted to skip the current song.', 4000);
  375. });
  376. },
  377. resumeStation: function () {
  378. let _this = this;
  379. _this.socket.emit('stations.resume', _this.station._id, data => {
  380. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  381. else Toast.methods.addToast('Successfully resumed the station.', 4000);
  382. });
  383. },
  384. pauseStation: function () {
  385. let _this = this;
  386. _this.socket.emit('stations.pause', _this.station._id, data => {
  387. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  388. else Toast.methods.addToast('Successfully paused the station.', 4000);
  389. });
  390. },
  391. toggleMute: function () {
  392. if (this.playerReady) {
  393. let previousVolume = parseInt(localStorage.getItem("volume"));
  394. let volume = this.player.getVolume() <= 0 ? previousVolume : 0;
  395. this.muted = !this.muted;
  396. $("#volumeSlider").val(volume);
  397. this.player.setVolume(volume);
  398. if (!this.muted) localStorage.setItem("volume", volume);
  399. }
  400. },
  401. increaseVolume: function () {
  402. if (this.playerReady) {
  403. let previousVolume = parseInt(localStorage.getItem("volume"));
  404. let volume = previousVolume + 5;
  405. if (previousVolume === 0) this.muted = false;
  406. if (volume > 100) volume = 100;
  407. $("#volumeSlider").val(volume);
  408. this.player.setVolume(volume);
  409. localStorage.setItem("volume", volume);
  410. }
  411. },
  412. toggleLike: function() {
  413. let _this = this;
  414. if (_this.liked) _this.socket.emit('songs.unlike', _this.currentSong.songId, data => {
  415. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  416. }); else _this.socket.emit('songs.like', _this.currentSong.songId, data => {
  417. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  418. });
  419. },
  420. toggleDislike: function() {
  421. let _this = this;
  422. if (_this.disliked) return _this.socket.emit('songs.undislike', _this.currentSong.songId, data => {
  423. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  424. });
  425. _this.socket.emit('songs.dislike', _this.currentSong.songId, data => {
  426. if (data.status !== 'success') Toast.methods.addToast(`Error: ${data.message}`, 8000);
  427. });
  428. },
  429. addFirstPrivatePlaylistSongToQueue: function() {
  430. let _this = this;
  431. let isInQueue = false;
  432. let userId = _this.$parent.userId;
  433. if (_this.type === 'community') {
  434. _this.songsList.forEach((queueSong) => {
  435. if (queueSong.requestedBy === userId) isInQueue = true;
  436. });
  437. if (!isInQueue && _this.privatePlaylistQueueSelected) {
  438. _this.socket.emit('playlists.getFirstSong', _this.privatePlaylistQueueSelected, data => {
  439. if (data.status === 'success') {
  440. if (data.song.duration < 15 * 60) {
  441. console.log(data.song);
  442. let songId = data.song._id;
  443. _this.automaticallyRequestedSongId = data.song.songId;
  444. _this.socket.emit('stations.addToQueue', _this.station._id, data.song.songId, data2 => {
  445. if (data2.status === 'success') {
  446. _this.socket.emit('playlists.moveSongToBottom', _this.privatePlaylistQueueSelected, data.song.songId, data3 => {
  447. if (data3.status === 'success') {
  448. }
  449. });
  450. }
  451. });
  452. } else {
  453. Toast.methods.addToast(`Top song in playlist was too long to be added.`, 3000);
  454. _this.socket.emit('playlists.moveSongToBottom', _this.privatePlaylistQueueSelected, data.song.songId, data3 => {
  455. if (data3.status === 'success') {
  456. setTimeout(() => {
  457. this.addFirstPrivatePlaylistSongToQueue();
  458. }, 3000);
  459. }
  460. });
  461. }
  462. }
  463. });
  464. }
  465. }
  466. },
  467. joinStation: function () {
  468. let _this = this;
  469. _this.socket.emit('stations.join', _this.stationName, res => {
  470. if (res.status === 'success') {
  471. _this.station = {
  472. _id: res.data._id,
  473. name: _this.stationName,
  474. displayName: res.data.displayName,
  475. description: res.data.description,
  476. privacy: res.data.privacy,
  477. locked: res.data.locked,
  478. partyMode: res.data.partyMode,
  479. owner: res.data.owner,
  480. privatePlaylist: res.data.privatePlaylist
  481. };
  482. _this.currentSong = (res.data.currentSong) ? res.data.currentSong : {};
  483. _this.type = res.data.type;
  484. _this.startedAt = res.data.startedAt;
  485. _this.paused = res.data.paused;
  486. _this.timePaused = res.data.timePaused;
  487. _this.userCount = res.data.userCount;
  488. _this.users = res.data.users;
  489. if (res.data.currentSong) {
  490. _this.noSong = false;
  491. _this.simpleSong = (res.data.currentSong.likes === -1 && res.data.currentSong.dislikes === -1);
  492. if (_this.simpleSong) {
  493. _this.currentSong.skipDuration = 0;
  494. }
  495. _this.youtubeReady();
  496. _this.playVideo();
  497. _this.socket.emit('songs.getOwnSongRatings', res.data.currentSong.songId, data => {
  498. if (_this.currentSong.songId === data.songId) {
  499. _this.liked = data.liked;
  500. _this.disliked = data.disliked;
  501. }
  502. });
  503. } else {
  504. if (_this.playerReady) _this.player.pauseVideo();
  505. _this.noSong = true;
  506. }
  507. if (_this.type === 'community') {
  508. _this.socket.emit('stations.getQueue', _this.station._id, data => {
  509. if (data.status === 'success') _this.songsList = data.queue;
  510. });
  511. }
  512. if (_this.type === 'official') {
  513. _this.socket.emit('stations.getPlaylist', _this.station._id, res => {
  514. if (res.status == 'success') _this.songsList = res.data;
  515. });
  516. }
  517. } else {
  518. _this.$router.go('/404');
  519. Toast.methods.addToast(res.message, 3000);
  520. }
  521. // UNIX client time before ping
  522. let beforePing = Date.now();
  523. _this.socket.emit('apis.ping', res => {
  524. // UNIX client time after ping
  525. let afterPing = Date.now();
  526. // Average time in MS it took between the server responding and the client receiving
  527. let connectionLatency = (afterPing - beforePing) / 2;
  528. console.log(connectionLatency, beforePing - afterPing);
  529. // UNIX server time
  530. let serverDate = res.date;
  531. // Difference between the server UNIX time and the client UNIX time after ping, with the connectionLatency added to the server UNIX time
  532. let difference = (serverDate + connectionLatency) - afterPing;
  533. console.log("Difference: ", difference);
  534. if (difference > 3000 || difference < -3000) {
  535. console.log("System time difference is bigger than 3 seconds.");
  536. }
  537. _this.systemDifference = difference;
  538. });
  539. });
  540. }
  541. },
  542. ready: function() {
  543. let _this = this;
  544. Date.currently = () => {
  545. return new Date().getTime() + _this.systemDifference;
  546. };
  547. _this.stationName = _this.$route.params.id;
  548. window.stationInterval = 0;
  549. io.getSocket(socket => {
  550. _this.socket = socket;
  551. io.removeAllListeners();
  552. if (_this.socket.connected) _this.joinStation();
  553. io.onConnect(() => {
  554. _this.joinStation();
  555. });
  556. _this.socket.emit('stations.findByName', _this.stationName, res => {
  557. if (res.status === 'error') {
  558. _this.$router.go('/404');
  559. Toast.methods.addToast(res.message, 3000);
  560. } else {
  561. _this.ready = true;
  562. }
  563. });
  564. _this.socket.on('event:songs.next', data => {
  565. _this.previousSong = (_this.currentSong.songId) ? _this.currentSong : null;
  566. _this.currentSong = (data.currentSong) ? data.currentSong : {};
  567. _this.startedAt = data.startedAt;
  568. _this.paused = data.paused;
  569. _this.timePaused = data.timePaused;
  570. if (data.currentSong) {
  571. _this.noSong = false;
  572. _this.simpleSong = (data.currentSong.likes === -1 && data.currentSong.dislikes === -1);
  573. if (_this.simpleSong) _this.currentSong.skipDuration = 0;
  574. if (!_this.playerReady) _this.youtubeReady();
  575. else _this.playVideo();
  576. _this.socket.emit('songs.getOwnSongRatings', data.currentSong.songId, (data) => {
  577. if (_this.currentSong.songId === data.songId) {
  578. _this.liked = data.liked;
  579. _this.disliked = data.disliked;
  580. }
  581. });
  582. } else {
  583. if (_this.playerReady) _this.player.pauseVideo();
  584. _this.noSong = true;
  585. }
  586. let isInQueue = false;
  587. let userId = _this.$parent.userId;
  588. _this.songsList.forEach((queueSong) => {
  589. if (queueSong.requestedBy === userId) isInQueue = true;
  590. });
  591. if (!isInQueue && _this.privatePlaylistQueueSelected && (_this.automaticallyRequestedSongId !== _this.currentSong.songId || !_this.currentSong.songId)) {
  592. _this.addFirstPrivatePlaylistSongToQueue();
  593. }
  594. });
  595. _this.socket.on('event:stations.pause', data => {
  596. _this.pauseLocalStation();
  597. });
  598. _this.socket.on('event:stations.resume', data => {
  599. _this.timePaused = data.timePaused;
  600. _this.resumeLocalStation();
  601. });
  602. _this.socket.on('event:stations.remove', () => {
  603. location.href = '/';
  604. });
  605. _this.socket.on('event:song.like', data => {
  606. if (!this.noSong) {
  607. if (data.songId === _this.currentSong.songId) {
  608. _this.currentSong.dislikes = data.dislikes;
  609. _this.currentSong.likes = data.likes;
  610. }
  611. }
  612. });
  613. _this.socket.on('event:song.dislike', data => {
  614. if (!this.noSong) {
  615. if (data.songId === _this.currentSong.songId) {
  616. _this.currentSong.dislikes = data.dislikes;
  617. _this.currentSong.likes = data.likes;
  618. }
  619. }
  620. });
  621. _this.socket.on('event:song.unlike', data => {
  622. if (!this.noSong) {
  623. if (data.songId === _this.currentSong.songId) {
  624. _this.currentSong.dislikes = data.dislikes;
  625. _this.currentSong.likes = data.likes;
  626. }
  627. }
  628. });
  629. _this.socket.on('event:song.undislike', data => {
  630. if (!this.noSong) {
  631. if (data.songId === _this.currentSong.songId) {
  632. _this.currentSong.dislikes = data.dislikes;
  633. _this.currentSong.likes = data.likes;
  634. }
  635. }
  636. });
  637. _this.socket.on('event:song.newRatings', data => {
  638. if (!this.noSong) {
  639. if (data.songId === _this.currentSong.songId) {
  640. _this.liked = data.liked;
  641. _this.disliked = data.disliked;
  642. }
  643. }
  644. });
  645. _this.socket.on('event:queue.update', queue => {
  646. if (this.type === 'community') this.songsList = queue;
  647. });
  648. _this.socket.on('event:song.voteSkipSong', () => {
  649. if (this.currentSong) this.currentSong.skipVotes++;
  650. });
  651. _this.socket.on('event:privatePlaylist.selected', (playlistId) => {
  652. if (this.type === 'community') {
  653. this.station.privatePlaylist = playlistId;
  654. }
  655. });
  656. _this.socket.on('event:partyMode.updated', (partyMode) => {
  657. if (this.type === 'community') {
  658. this.station.partyMode = partyMode;
  659. }
  660. });
  661. _this.socket.on('event:newOfficialPlaylist', (playlist) => {
  662. console.log(playlist);
  663. if (this.type === 'official') {
  664. this.songsList = playlist;
  665. }
  666. });
  667. _this.socket.on('event:users.updated', users => {
  668. _this.users = users;
  669. });
  670. _this.socket.on('event:userCount.updated', userCount => {
  671. _this.userCount = userCount;
  672. });
  673. _this.socket.on('event:queueLockToggled', locked => {
  674. _this.station.locked = locked;
  675. });
  676. });
  677. let volume = parseInt(localStorage.getItem("volume"));
  678. volume = (typeof volume === "number" && !isNaN(volume)) ? volume : 20;
  679. localStorage.setItem("volume", volume);
  680. $("#volumeSlider").val(volume);
  681. },
  682. components: {
  683. OfficialHeader,
  684. CommunityHeader,
  685. SongQueue,
  686. AddToPlaylist,
  687. EditPlaylist,
  688. CreatePlaylist,
  689. EditStation,
  690. Report,
  691. SongsListSidebar,
  692. PlaylistSidebar,
  693. UsersSidebar,
  694. MainFooter
  695. }
  696. }
  697. </script>
  698. <style lang="scss">
  699. .no-song {
  700. color: #03A9F4;
  701. text-align: center;
  702. }
  703. #volumeSlider {
  704. padding: 0 15px;
  705. background: transparent;
  706. }
  707. .volume-slider-wrapper {
  708. margin-top: 0;
  709. position: relative;
  710. display: flex;
  711. align-items: center;
  712. .material-icons { user-select: none; }
  713. }
  714. .material-icons { cursor: pointer; }
  715. .stationDisplayName {
  716. color: white !important;
  717. }
  718. .add-to-playlist {
  719. display: flex;
  720. align-items: center;
  721. justify-content: center;
  722. }
  723. .slideout {
  724. top: 50px;
  725. height: 100%;
  726. position: fixed;
  727. right: 0;
  728. width: 350px;
  729. background-color: white;
  730. box-shadow: 0 2px 5px 0 rgba(0, 0, 0, 0.16), 0 2px 10px 0 rgba(0, 0, 0, 0.12);
  731. .slideout-header {
  732. text-align: center;
  733. background-color: rgb(3, 169, 244) !important;
  734. margin: 0;
  735. padding-top: 5px;
  736. padding-bottom: 7px;
  737. color: white;
  738. }
  739. .slideout-content {
  740. height: 100%;
  741. }
  742. }
  743. .modal-large {
  744. width: 75%;
  745. }
  746. .station {
  747. flex: 1 0 auto;
  748. padding-top: 0.5vw;
  749. transition: all 0.1s;
  750. margin: 0 auto;
  751. max-width: 100%;
  752. width: 90%;
  753. @media only screen and (min-width: 993px) {
  754. width: 70%;
  755. }
  756. @media only screen and (min-width: 601px) {
  757. width: 85%;
  758. }
  759. @media (min-width: 999px) {
  760. .mobile-only {
  761. display: none;
  762. }
  763. .desktop-only {
  764. display: block;
  765. }
  766. }
  767. @media (max-width: 998px) {
  768. .mobile-only {
  769. display: block;
  770. }
  771. .desktop-only {
  772. display: none;
  773. visibility: hidden;
  774. }
  775. }
  776. .mobile-only {
  777. text-align: center;
  778. }
  779. .playlistCard {
  780. margin: 10px;
  781. position: relative;
  782. padding-bottom: calc(31.25% + 7px);
  783. height: 0;
  784. overflow-y: scroll;
  785. .title {
  786. background-color: rgb(3, 169, 244);
  787. text-align: center;
  788. padding: 10px;
  789. color: white;
  790. font-weight: 600;
  791. }
  792. .media { padding: 0 25px; }
  793. .media-content .content {
  794. min-height: 64px;
  795. max-height: 64px;
  796. display: flex;
  797. align-items: center;
  798. }
  799. .content p strong { word-break: break-word; }
  800. .content p small { word-break: break-word; }
  801. .add-to-queue {
  802. width: 100%;
  803. margin-top: 25px;
  804. height: 40px;
  805. border-radius: 0;
  806. background: rgb(3, 169, 244);
  807. color: #fff !important;
  808. border: 0;
  809. &:active, &:focus { border: 0; }
  810. }
  811. .add-to-queue:focus { background: #029ce3; }
  812. .media-right { line-height: 64px; }
  813. .songTitle {
  814. word-wrap: break-word;
  815. overflow: hidden;
  816. text-overflow: ellipsis;
  817. display: -webkit-box;
  818. -webkit-box-orient: vertical;
  819. -webkit-line-clamp: 2;
  820. line-height: 20px;
  821. max-height: 40px;
  822. width: 100%;
  823. }
  824. }
  825. input[type=range] {
  826. -webkit-appearance: none;
  827. width: 100%;
  828. margin: 7.3px 0;
  829. }
  830. input[type=range]:focus {
  831. outline: none;
  832. }
  833. input[type=range]::-webkit-slider-runnable-track {
  834. width: 100%;
  835. height: 5.2px;
  836. cursor: pointer;
  837. box-shadow: 0;
  838. background: #c2c0c2;
  839. border-radius: 0;
  840. border: 0;
  841. }
  842. input[type=range]::-webkit-slider-thumb {
  843. box-shadow: 0;
  844. border: 0;
  845. height: 19px;
  846. width: 19px;
  847. border-radius: 15px;
  848. background: #03a9f4;
  849. cursor: pointer;
  850. -webkit-appearance: none;
  851. margin-top: -6.5px;
  852. }
  853. input[type=range]::-moz-range-track {
  854. width: 100%;
  855. height: 5.2px;
  856. cursor: pointer;
  857. box-shadow: 0;
  858. background: #c2c0c2;
  859. border-radius: 0;
  860. border: 0;
  861. }
  862. input[type=range]::-moz-range-thumb {
  863. box-shadow: 0;
  864. border: 0;
  865. height: 19px;
  866. width: 19px;
  867. border-radius: 15px;
  868. background: #03a9f4;
  869. cursor: pointer;
  870. -webkit-appearance: none;
  871. margin-top: -6.5px;
  872. }
  873. input[type=range]::-ms-track {
  874. width: 100%;
  875. height: 5.2px;
  876. cursor: pointer;
  877. box-shadow: 0;
  878. background: #c2c0c2;
  879. border-radius: 1.3px;
  880. }
  881. input[type=range]::-ms-fill-lower {
  882. background: #c2c0c2;
  883. border: 0;
  884. border-radius: 0;
  885. box-shadow: 0;
  886. }
  887. input[type=range]::-ms-fill-upper {
  888. background: #c2c0c2;
  889. border: 0;
  890. border-radius: 0;
  891. box-shadow: 0;
  892. }
  893. input[type=range]::-ms-thumb {
  894. box-shadow: 0;
  895. border: 0;
  896. height: 15px;
  897. width: 15px;
  898. border-radius: 15px;
  899. background: #03a9f4;
  900. cursor: pointer;
  901. -webkit-appearance: none;
  902. margin-top: 1.5px;
  903. }
  904. .video-container {
  905. position: relative;
  906. padding-bottom: 56.25%;
  907. height: 0;
  908. overflow: hidden;
  909. iframe {
  910. position: absolute;
  911. top: 0;
  912. left: 0;
  913. width: 100%;
  914. height: 100%;
  915. }
  916. }
  917. .video-col {
  918. padding-right: 0.75rem;
  919. padding-left: 0.75rem;
  920. }
  921. }
  922. .room-title {
  923. left: 50%;
  924. -webkit-transform: translateX(-50%);
  925. transform: translateX(-50%);
  926. font-size: 2.1em;
  927. }
  928. #ratings {
  929. span {
  930. font-size: 1.68rem;
  931. }
  932. i {
  933. color: #9e9e9e !important;
  934. cursor: pointer;
  935. transition: 0.1s color;
  936. }
  937. }
  938. #time-display {
  939. margin-top: 30px;
  940. float: right;
  941. }
  942. #thumbs_up:hover, #thumbs_up.liked {
  943. color: #87D37C !important;
  944. }
  945. #thumbs_down:hover, #thumbs_down.disliked {
  946. color: #EC644B !important;
  947. }
  948. #song-thumbnail {
  949. max-width: 100%;
  950. width: 85%;
  951. }
  952. .seeker-bar-container {
  953. position: relative;
  954. height: 7px;
  955. display: block;
  956. width: 100%;
  957. overflow: hidden;
  958. }
  959. .seeker-bar {
  960. top: 0;
  961. left: 0;
  962. bottom: 0;
  963. position: absolute;
  964. }
  965. ul {
  966. list-style: none;
  967. margin: 0;
  968. display: block;
  969. }
  970. h1, h2, h3, h4, h5, h6 {
  971. font-weight: 400;
  972. line-height: 1.1;
  973. }
  974. h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
  975. font-weight: inherit;
  976. }
  977. h1 {
  978. font-size: 4.2rem;
  979. line-height: 110%;
  980. margin: 2.1rem 0 1.68rem 0;
  981. }
  982. h2 {
  983. font-size: 3.56rem;
  984. line-height: 110%;
  985. margin: 1.78rem 0 1.424rem 0;
  986. }
  987. h3 {
  988. font-size: 2.92rem;
  989. line-height: 110%;
  990. margin: 1.46rem 0 1.168rem 0;
  991. }
  992. h4 {
  993. font-size: 2.28rem;
  994. line-height: 110%;
  995. margin: 1.14rem 0 0.912rem 0;
  996. }
  997. h5 {
  998. font-size: 1.64rem;
  999. line-height: 110%;
  1000. margin: 0.82rem 0 0.656rem 0;
  1001. }
  1002. h6 {
  1003. font-size: 1rem;
  1004. line-height: 110%;
  1005. margin: 0.5rem 0 0.4rem 0;
  1006. }
  1007. .thin {
  1008. font-weight: 200;
  1009. }
  1010. .left {
  1011. float: left !important;
  1012. }
  1013. .right {
  1014. float: right !important;
  1015. }
  1016. .light-blue {
  1017. background-color: #03a9f4 !important;
  1018. }
  1019. .white {
  1020. background-color: #FFFFFF !important;
  1021. }
  1022. .btn-search {
  1023. font-size: 14px;
  1024. }
  1025. .menu { padding: 0 10px; }
  1026. .menu-list li a:hover { color: #000 !important; }
  1027. .menu-list li {
  1028. display: flex;
  1029. justify-content: space-between;
  1030. }
  1031. .menu-list a {
  1032. /*padding: 0 10px !important;*/
  1033. }
  1034. .menu-list a:hover {
  1035. background-color : transparent;
  1036. }
  1037. .icons-group { display: flex; }
  1038. #like, #dislike {
  1039. position: relative;
  1040. }
  1041. .behind {
  1042. z-index: -1;
  1043. }
  1044. .behind:focus {
  1045. z-index: 0;
  1046. }
  1047. .progress {
  1048. width: 50px;
  1049. animation: rotate 0.8s infinite linear;
  1050. border: 8px solid #03A9F4;
  1051. border-right-color: transparent;
  1052. height: 50px;
  1053. position: absolute;
  1054. top: 50%;
  1055. left: 50%;
  1056. }
  1057. @keyframes rotate {
  1058. 0% { transform: rotate(0deg); }
  1059. 100% { transform: rotate(360deg); }
  1060. }
  1061. .experimental {
  1062. display: none !important;
  1063. }
  1064. </style>